From 2e76ebde1ac400f3634e219e7c7f7232af965fe1 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Wed, 26 Aug 2026 18:22:19 +0200 Subject: [PATCH 01/12] feat(confidential): escrow the sender-auditor secret in sponge lane 2 Widens the sender-auditor sponge from two lanes to three and adds an auditor-side escrow of the delegation viewing key. Lane 2 carries the new spendable blinding on the checkpoint operations (W_a5, T_a9, S_a6) and dvk_i on spender transfers (O_a9); S14 escrows dvk_i to the owner's auditor at set_spender under a new domain tag. The auditor can now recover the full Pedersen opening of C_spend and C_a rather than the value alone. --- packages/tokens/src/confidential/CLAUDE.md | 8 +- .../src/confidential/circuits/CLAUDE.md | 10 +- .../circuits/constraints.baseline | 8 +- .../src/confidential/circuits/lib/src/lib.nr | 99 ++++++++++++++++--- .../confidential/circuits/lib/src/tests.nr | 86 +++++++++++++++- .../lib/testdata/encrypt_esc_dvk_auditor.json | 15 +++ .../lib/testdata/sponge_squeeze_3.json | 15 +++ .../circuits/set_spender/src/main.nr | 60 +++++++++-- .../circuits/set_spender/src/tests.nr | 85 ++++++++++++++-- .../circuits/spender_transfer/src/main.nr | 42 ++++++-- .../circuits/spender_transfer/src/tests.nr | 58 ++++++++--- .../circuits/transfer/src/main.nr | 30 ++++-- .../circuits/transfer/src/tests.nr | 57 +++++++++-- .../circuits/vks/set_spender.vk.json | 2 +- .../circuits/vks/spender_transfer.vk.json | 2 +- .../circuits/vks/transfer.vk.json | 2 +- .../circuits/vks/withdraw.vk.json | 2 +- .../circuits/withdraw/src/main.nr | 47 ++++++--- .../circuits/withdraw/src/tests.nr | 32 +++++- .../src/confidential/compliance/test.rs | 5 + .../tokens/src/confidential/docs/DESIGN.md | 53 +++++----- .../src/confidential/docs/DESIGN_cont.md | 12 ++- .../tokens/src/confidential/docs/INDEXER.md | 2 +- .../tokens/src/confidential/docs/OVERVIEW.md | 4 +- packages/tokens/src/confidential/docs/SDK.md | 13 +-- .../confidential/docs/SELECTIVE_DISCLOSURE.md | 18 ++-- packages/tokens/src/confidential/mod.rs | 15 +++ packages/tokens/src/confidential/storage.rs | 45 +++++++-- packages/tokens/src/confidential/test.rs | 6 ++ 29 files changed, 678 insertions(+), 155 deletions(-) create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json diff --git a/packages/tokens/src/confidential/CLAUDE.md b/packages/tokens/src/confidential/CLAUDE.md index f31e0d6d9..adf058640 100644 --- a/packages/tokens/src/confidential/CLAUDE.md +++ b/packages/tokens/src/confidential/CLAUDE.md @@ -51,7 +51,7 @@ Beyond the root guide's conventions: Everything else defers by citation: `SDK.md`, `SELECTIVE_DISCLOSURE.md`, `INDEXER.md`, `COMPLIANCE.md`, and the non-normative `OVERVIEW.md`. Two exceptions run the other way — `circuits/lib/src/lib.nr` outranks the docs wherever they disagree about a primitive (`SDK.md` §4 says so explicitly), and the contract's `#[contracttype]`s are authoritative for their own shape. -`DESIGN.md` is already at roughly 768 expressions, over its own stated budget. Do not add math to §1–§7; put it in `DESIGN_cont.md`. +`DESIGN.md` is already at roughly 778 expressions, over its own stated budget. Do not add math to §1–§7; put it in `DESIGN_cont.md`. ### Duplicated tables that drift @@ -59,13 +59,13 @@ Five things exist in more than one file. Changing the normative copy means grepp | Content | Normative source | Copies live in | |:---|:---|:---| -| The 16 domain-separation tags | `DESIGN_cont.md` §13 | `SDK.md` §4.8, referenced by `SELECTIVE_DISCLOSURE.md` | -| Sponge lane assignment (lane 0 = amount mask, lane 1 = balance/allowance/randomness) | `DESIGN.md` §2.5 | `SDK.md` §4.3 and §11 | +| The 17 domain-separation tags | `DESIGN_cont.md` §13 | `SDK.md` §4.8, referenced by `SELECTIVE_DISCLOSURE.md` | +| Sponge lane assignment (lane 0 = amount mask, lane 1 = balance/allowance/randomness, lane 2 = sender-auditor secret-escrow slot) | `DESIGN.md` §2.5 | `SDK.md` §4.3 and §11 | | Per-circuit scalar-multiplication counts | `DESIGN_cont.md` §10.3 | `OVERVIEW.md` | | Checkpoint event set (`Withdraw`, `Transfer` sender side, `SetSpender`, `RevokeSpender`) | `DESIGN.md` §5.2 | `INDEXER.md`, `SDK.md` | | Replay-window anchor `T₀` | `DESIGN.md` §5.2 | `INDEXER.md`, `OVERVIEW.md` | -The tags are a cross-language wire contract. `DESIGN_cont.md` §13 assigns all sixteen and no other document may; `circuits/lib/src/lib.nr` implements only 1–13, because 14 is derived off-circuit and 15–16 belong to the off-chain disclosure layer. That gap is intentional. Changing any assigned value is a new deployment, not an upgrade. +The tags are a cross-language wire contract. `DESIGN_cont.md` §13 assigns all seventeen and no other document may; `circuits/lib/src/lib.nr` implements 1–13 and 17, because 14 is derived off-circuit and 15–16 belong to the off-chain disclosure layer. That gap is intentional. Changing any assigned value is a new deployment, not an upgrade. ### Editing rules diff --git a/packages/tokens/src/confidential/circuits/CLAUDE.md b/packages/tokens/src/confidential/circuits/CLAUDE.md index 74060ec76..94c1855ee 100644 --- a/packages/tokens/src/confidential/circuits/CLAUDE.md +++ b/packages/tokens/src/confidential/circuits/CLAUDE.md @@ -26,7 +26,7 @@ It appears in all fourteen `Nargo.toml` files and looks like a mistake. Nargo re ### Do not prune unused public inputs -`_acct_f` in `register/src/main.nr` is referenced by no gate and looks like dead code. It is the replay binding: UltraHonk absorbs every public input into the transcript, so a proof produced for one account fails when the contract assembles the blob for another. Removing it lets anyone replay a legitimate registration's published proof and payload to mint duplicate-key accounts. Each operation circuit declares its exact public-input count in a header comment — withdraw 15, revoke_spender 19, transfer / set_spender / spender_transfer 24 — and the count is part of the contract with the on-chain assembler. +`_acct_f` in `register/src/main.nr` is referenced by no gate and looks like dead code. It is the replay binding: UltraHonk absorbs every public input into the transcript, so a proof produced for one account fails when the contract assembles the blob for another. Removing it lets anyone replay a legitimate registration's published proof and payload to mint duplicate-key accounts. Each operation circuit declares its exact public-input count in a header comment — withdraw 16, revoke_spender 19, transfer / spender_transfer 25, set_spender 26 — and the count is part of the contract with the on-chain assembler. ### Package names are load-bearing @@ -36,7 +36,9 @@ Directory `transfer/` is package `circuit_transfer`; `gadgets/commit/` is `gadge `poseidon_with_domain` is the only Poseidon entry point in `lib/src/lib.nr`; calling the underlying hash directly is a violation of the library contract. The domain tag is always the first absorbed element. The numeric tag values are the cross-language contract with the SDK — see `../CLAUDE.md` and `docs/DESIGN_cont.md` §13, which is their only authoritative source. -Sponge parameters: width 4, rate 3, capacity 1, `iv = len · 2^64`. Empty input still applies the squeeze permutation, matching the on-chain sponge. The two-lane squeeze order is fixed — **index 0 is always an amount mask, index 1 always a balance/allowance/randomness mask** — and `encrypt_auditor_sender_balance` deliberately takes the second lane so a balance checkpoint can never share a pad with an amount ciphertext under `(r_e, σ)` reuse. `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`. +Sponge parameters: width 4, rate 3, capacity 1, `iv = len · 2^64`. Empty input still applies the squeeze permutation, matching the on-chain sponge. The squeeze order is fixed — **lane 0 is always an amount mask, lane 1 always a balance/allowance/randomness mask, lane 2 always the sender-auditor secret-escrow slot** — and `encrypt_auditor_sender_balance` deliberately takes lane 1 so a balance checkpoint can never share a pad with an amount ciphertext under `(r_e, σ)` reuse. `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, and `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)` — the absorb fits one rate-3 block, so both read the same permutation, and a divergence would silently change every existing mask. + +Only `AUDITOR_SENDER` (11) is squeezed three-wide; `AUDITOR_RECIPIENT` (12) stays at two lanes. Lane 2 carries a *different plaintext per operation* — the new spendable blinding on checkpoints, `dvk_i` on spender transfers — which is not pad reuse, because the pad is keyed by a per-operation-fresh `(s_{a,s}, σ)`. ECDH must absorb both `S.x` and `S.y`; x-only extraction collapses `P` and `-P`. @@ -56,7 +58,7 @@ LC_ALL=C nargo info | grep '^|' | LC_ALL=C sort > constraints.baseline `LC_ALL=C` is mandatory on **both** sides of the pipe — byte order is the only ordering stable between macOS and the Ubuntu runner. The redirect overwrites the file's header comments; re-paste them, because CI's failure message asks for them. -Two non-obvious consequences: adding or removing a **gadget** changes the baseline even when no circuit logic changed, and the ACIR opcode counts are quoted in prose at `../docs/DESIGN_cont.md` §10.3 (Register 33, Withdraw 94, RevokeSpender 123, SetSpender 131, Transfer 133, SpenderTransfer 135). Nothing enforces that second copy — update it in the same PR. +Two non-obvious consequences: adding or removing a **gadget** changes the baseline even when no circuit logic changed, and the ACIR opcode counts are quoted in prose at `../docs/DESIGN_cont.md` §10.3 (Register 33, Withdraw 95, RevokeSpender 123, Transfer 134, SetSpender 135, SpenderTransfer 136). Nothing enforces that second copy — update it in the same PR. ### `vks/` @@ -76,7 +78,7 @@ Fixtures are not auto-generated. Changing a primitive is a three-step lockstep: 2. Update the matching `testdata/*.json` 3. Update the hardcoded expected values in the `fixtures_match_testdata` test in `lib/src/tests.nr` -`fixtures_match_testdata` is the in-Noir guard that fails CI. The sponge vectors are additionally hoisted into `global SPONGE_SQUEEZE_2_*` constants in the same file — a fourth site. +`fixtures_match_testdata` is the in-Noir guard that fails CI. The sponge vectors are additionally hoisted into `global SPONGE_SQUEEZE_2_*` / `SPONGE_SQUEEZE_3_*` constants in the same file — a fourth site. `address_to_field.json` is the exception. That derivation has no Noir implementation at all (circuits take `addr_f` as an opaque public input), so it is the one primitive with two independent implementations. Its guard is the Rust test `address_to_field_matches_testdata_vectors` in `../test.rs`, which **transcribes the hex values as string literals** rather than reading the JSON — update both together or neither. Its inputs are 56-character SEP-23 strkeys, and the lo/hi 28-byte limbs are little-endian. diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index fa1ae7894..0c7af34d5 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -29,19 +29,19 @@ | circuit_set_spender | decompose_hint | N/A | N/A | 30 | | circuit_set_spender | directive_invert | N/A | N/A | 9 | | circuit_set_spender | lte_hint | N/A | N/A | 33 | -| circuit_set_spender | main | Bounded { width: 4 } | 131 | 72 | +| circuit_set_spender | main | Bounded { width: 4 } | 135 | 72 | | circuit_spender_transfer | decompose_hint | N/A | N/A | 30 | | circuit_spender_transfer | directive_invert | N/A | N/A | 9 | | circuit_spender_transfer | lte_hint | N/A | N/A | 33 | -| circuit_spender_transfer | main | Bounded { width: 4 } | 135 | 72 | +| circuit_spender_transfer | main | Bounded { width: 4 } | 136 | 72 | | circuit_transfer | decompose_hint | N/A | N/A | 30 | | circuit_transfer | directive_invert | N/A | N/A | 9 | | circuit_transfer | lte_hint | N/A | N/A | 33 | -| circuit_transfer | main | Bounded { width: 4 } | 133 | 72 | +| circuit_transfer | main | Bounded { width: 4 } | 134 | 72 | | circuit_withdraw | decompose_hint | N/A | N/A | 30 | | circuit_withdraw | directive_invert | N/A | N/A | 9 | | circuit_withdraw | lte_hint | N/A | N/A | 33 | -| circuit_withdraw | main | Bounded { width: 4 } | 94 | 72 | +| circuit_withdraw | main | Bounded { width: 4 } | 95 | 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/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index 2091c9620..ef83cd765 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -90,6 +90,10 @@ global POSEIDON2_IV_BASE: Field = 18446744073709551616; // 2^64 /// | `delta_aud_s` | 11 | `AUDITOR_SENDER` | /// | `delta_aud_r` | 12 | `AUDITOR_RECIPIENT` | /// | `delta_ecdh` | 13 | `ECDH_SHARED_SECRET` | +/// | `delta_esc_dvk_aud` | 17 | `ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR` | +/// +/// Tags 14-16 are deliberately absent here: 14 is derived off-circuit and +/// 15-16 belong to the off-chain disclosure layer. pub mod domain { /// Soroban Address compression into a single `F_r` Field: /// `address_to_field(a) = Poseidon2(ADDRESS, lo(a), hi(a))`. @@ -128,9 +132,21 @@ pub mod domain { /// Constraint S12, Section 7.11 (`delta_esc_dvk`). 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`). + /// (Section 2.5, Section 8.1). **Three lanes**, and the only tag squeezed + /// three-wide: lane 0 is the amount mask (where applicable), lane 1 the + /// balance/allowance checkpoint mask, lane 2 the secret-escrow slot -- + /// the new spendable blinding on checkpoints, the delegation viewing key + /// on spender transfers. Constraints W_a3 / T_a6 / S_a3 / O_a6 + /// (`delta_aud_s`). + /// + /// RevokeSpender (V_a3) still reads two lanes: it writes no checkpoint + /// that needs an escrow slot, and the two widths agree on every lane they + /// share (see [`sponge_squeeze_3`]). + /// + /// The tag stays in a single sponge mode per Section 2.5: + /// `sponge_squeeze_2(d, s, sigma)[0]` is the same field element as + /// `poseidon_with_domain(d, [s, sigma])`, so a tag used in both modes + /// would collapse one onto the other. 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 @@ -141,6 +157,16 @@ pub mod domain { /// `s = Poseidon2(ECDH_SHARED_SECRET, S.x, S.y)`. /// Section 2.4 (`delta_ecdh`). See [`ecdh`]. pub global ECDH_SHARED_SECRET: Field = 13; + /// Auditor-side delegation-key escrow mask (owner-auditor ECDH): + /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, s_a_s, op_i)`. + /// Constraint S14 (`delta_esc_dvk_aud`). + /// + /// A separate tag from `ESCROWED_DELEGATION_VIEWING_KEY` = 10 even though + /// reuse would be secure -- the two shared scalars differ, since `Y_op` + /// and `K_aud_s` are independent points. Sharing the tag would drop one + /// of the three legs of Section 5.3's "why reusing r_e is safe" argument; + /// a fresh tag keeps that argument intact. + pub global ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR: Field = 17; } // ################## CORE PRIMITIVES ################## @@ -304,6 +330,23 @@ 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-side delegation-key escrow at `set_spender`. Constraint S14. +/// `dvk_cipher_aud = dvk + Poseidon2(ESC_DVK_AUD, s_a_s, op_i)` where +/// `s_a_s = ecdh(r_e, K_aud_s)` is the *owner-auditor* shared scalar. +/// +/// A single-output Poseidon rather than a third sponge lane, because at +/// `set_spender` lane 2 of the owner-auditor sponge is already taken by S_a6. +/// Costs one Poseidon2 permutation and zero scalar multiplications; the pad's +/// freshness comes from `s_a_s`, which is fresh per operation because `r_e` +/// is, and `op_i` is a nonce by parallel with S12. +/// +/// This is what gives the auditor the opening of `C_a`: with `dvk_i` it can +/// derive `r_a = derive_allow_r(dvk_i, sigma_a)`. Any fold of `C_a` into +/// `C_spend` therefore leaves the auditor's accumulator in sync. +pub fn encrypt_esc_dvk_auditor(dvk: Field, s_a_s: Field, op_i: Field) -> Field { + dvk + poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, [s_a_s, op_i]) +} + // ################## AUDITOR-CHANNEL SPONGE ################## /// Poseidon2 sponge with two squeezes. Absorbs `(d, s, sigma)` -- where @@ -324,19 +367,49 @@ pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { [state[0], state[1]] } -/// Sender-auditor encrypted balance checkpoint for circuits whose auditor -/// block emits a single ciphertext (Withdraw W_a3/W_a4). +/// Poseidon2 sponge with three squeezes. Absorbs `(d, s, sigma)` and returns +/// the first three rate-elements of the post-permutation state in order. +/// +/// Implements `SpongeSqueeze_3(delta, s, sigma)` from Section 2.5. Lane 0 is +/// the amount mask and lane 1 the balance / allowance / randomness mask, as in +/// [`sponge_squeeze_2`]. **Lane 2 is the sender-auditor secret-escrow slot**: +/// it carries the new spendable blinding on the three checkpoint operations +/// (W_a5 / T_a9 / S_a6) and the delegation viewing key on spender transfers +/// (O_a9). +/// +/// `sponge_squeeze_3(d, s, sigma)[0..2] == sponge_squeeze_2(d, s, sigma)`, by +/// construction: the absorb fits one rate-3 block, so all three outputs are +/// served from the same permutation and the two functions differ only in how +/// much of the state they expose. Widening therefore changes no existing mask +/// value and costs no extra constraints. The capacity lane at `state[3]` stays +/// untouched. +/// +/// Two different plaintexts share lane 2 across operations, which is not pad +/// reuse: the pad is fixed by `(s, sigma)` or `(s, sigma_a)`, both fresh per +/// operation, so no pad ever masks two values. +pub fn sponge_squeeze_3(d: Field, s: Field, sigma: Field) -> [Field; 3] { + let iv: Field = 3 * POSEIDON2_IV_BASE; + let state = poseidon2_permutation([d, s, sigma, iv], 4); + [state[0], state[1], state[2]] +} + +/// Sender-auditor encrypted balance checkpoint, packaged as a single call. /// /// `b_tilde_aud_s = v_new + sponge_squeeze_2(AUDITOR_SENDER, s_a_s, sigma)[1]`, /// where `s_a_s` is the sender-auditor ECDH shared-secret scalar produced by -/// [`ecdh`]. -/// The pad is the sponge's *second* squeeze -- the slot every two-ciphertext -/// sender channel (Transfer T_a6-T_a8, SetSpender S_a3-S_a5, RevokeSpender -/// V_a3-V_a5, SpenderTransfer O_a6-O_a8) uses for its balance/allowance mask. -/// The first squeeze is reserved for amount masks, so a balance checkpoint -/// never shares a pad with an amount ciphertext even if a `(r_e, sigma)` pair -/// is mistakenly reused across operations (the pair must be unique per proof; -/// DESIGN Section 2.5). +/// [`ecdh`]. The pad is lane 1 -- the slot every sender channel (Withdraw +/// W_a4, Transfer T_a8, SetSpender S_a5, SpenderTransfer O_a8, RevokeSpender +/// V_a5) uses for its balance/allowance mask. Lane 0 is reserved for amount masks, so a balance +/// checkpoint never shares a pad with an amount ciphertext even if a +/// `(r_e, sigma)` pair is mistakenly reused across operations (the pair must +/// be unique per proof; DESIGN Section 2.5). +/// +/// No operation circuit calls this today: every sender-auditor channel now +/// squeezes lane 2 as well (see [`sponge_squeeze_3`]) and reads its lanes from +/// one array. It is kept as the named, fixture-pinned form of the lane-1 +/// balance ciphertext for SDK consumers, and because +/// `sponge_squeeze_3(..)[1] == sponge_squeeze_2(..)[1]` makes the two +/// interchangeable. pub fn encrypt_auditor_sender_balance(v_new: Field, s_a_s: Field, sigma: Field) -> Field { v_new + sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, sigma)[1] } diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 91f12440a..1a5a6099a 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_non_identity, commit, derive_allow_r, derive_spend_r, derive_transfer_blind, domain, dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_amount, encrypt_auditor_sender_balance, - encrypt_balance, encrypt_esc_dvk, G, H, poseidon_with_domain, pvk_from_vk, scalar_mul, sponge, - sponge_squeeze_2, vk_from_sk, + encrypt_balance, encrypt_esc_dvk, encrypt_esc_dvk_auditor, G, H, poseidon_with_domain, + pvk_from_vk, scalar_mul, sponge, sponge_squeeze_2, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; use std::hash::{derive_generators, pedersen_commitment, poseidon2_permutation}; @@ -239,6 +239,53 @@ fn sponge_squeeze_2_first_matches_poseidon_with_domain() { assert(m2[0] == poseidon_with_domain(d, [s, sigma])); } +#[test] +fn sponge_squeeze_3_agrees_with_squeeze_2_on_shared_lanes() { + // The widening from two lanes to three must be observationally identical + // on lanes 0 and 1 for every domain tag: both squeezes read the same + // post-permutation state, and the absorb occupies a single rate-3 block. + // This is what lets `delta_aud_s` be widened everywhere it appears + // without renumbering a single existing constraint. + let s: Field = 0x12345; + let sigma: Field = 0x6789; + for d in [domain::AUDITOR_SENDER, domain::AUDITOR_RECIPIENT] { + let m2 = sponge_squeeze_2(d, s, sigma); + let m3 = sponge_squeeze_3(d, s, sigma); + assert(m3[0] == m2[0]); + assert(m3[1] == m2[1]); + } +} + +#[test] +fn sponge_squeeze_3_outputs_distinct() { + // Lane 2 comes from a third rate position of the same permutation and + // must differ from both others. A collision would let an observer who + // recovers the balance mask also recover the secret-escrow pad -- and + // with it the account's spendable blinding, which is exactly what the + // slot exists to keep between the sender and its auditor. + let m = sponge_squeeze_3(domain::AUDITOR_SENDER, 0x42, 0x07); + assert(m[0] != m[1]); + assert(m[0] != m[2]); + assert(m[1] != m[2]); +} + +#[test] +fn esc_dvk_auditor_is_domain_separated_from_the_spender_escrow() { + // Tags 10 and 17 escrow the same plaintext (dvk_i) to two different + // parties. Reusing one tag for both would be secure in practice -- the + // shared scalars differ because Y_op and K_aud_s are independent points + // -- but the separation is what keeps Section 5.3's r_e-reuse argument + // whole, so it must be observable here. + let dvk: Field = 0xd0d0; + let s: Field = 0x12345; + let op_i: Field = 0xabcd; + assert(encrypt_esc_dvk(dvk, s, op_i) != encrypt_esc_dvk_auditor(dvk, s, op_i)); + + // Both are additive one-time-pad encryptions and must round-trip. + let mask = poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, [s, op_i]); + assert(encrypt_esc_dvk_auditor(dvk, s, op_i) - mask == dvk); +} + #[test] fn sponge_squeeze_2_outputs_distinct() { // The two squeezed masks come from different rate positions of the same @@ -364,6 +411,19 @@ fn print_fixtures() { println(f"sponge_squeeze_2_AUDITOR_RECIPIENT_0 = {ss2_r0}"); println(f"sponge_squeeze_2_AUDITOR_RECIPIENT_1 = {ss2_r1}"); + // sponge_squeeze_3(AUDITOR_SENDER, s, sigma) -- (amount, balance, escrow) + let ss3_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_ecdh, sigma); + let ss3_s0 = ss3_s[0]; + let ss3_s1 = ss3_s[1]; + let ss3_s2 = ss3_s[2]; + println(f"sponge_squeeze_3_AUDITOR_SENDER_0 = {ss3_s0}"); + println(f"sponge_squeeze_3_AUDITOR_SENDER_1 = {ss3_s1}"); + println(f"sponge_squeeze_3_AUDITOR_SENDER_2 = {ss3_s2}"); + + // encrypt_esc_dvk_auditor(dvk, s, op_i) + let esc_aud = encrypt_esc_dvk_auditor(dvk, s_ecdh, op_i); + println(f"encrypt_esc_dvk_auditor = {esc_aud}"); + // poseidon_with_domain(VK, [sk, addr_f]) -- direct funnel access, equals vk_from_sk let pwd = poseidon_with_domain(domain::VIEWING_KEY, [sk, addr_f]); println(f"poseidon_with_domain_VK_2 = {pwd}"); @@ -464,6 +524,24 @@ fn fixtures_match_testdata() { == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0, ); + // The three-lane squeeze must agree with the two-lane one on lanes 0-1: + // the absorb fits one rate-3 block, so both are served from the same + // permutation. If this diverges, widening the sender-auditor channel + // silently changed every existing mask and every published ciphertext + // became undecryptable. + let ss3_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_ecdh, sigma); + assert(ss3_s[0] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0); + assert(ss3_s[1] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_1); + assert(ss3_s[2] == SPONGE_SQUEEZE_3_AUDITOR_SENDER_2); + let ss3_r = sponge_squeeze_3(domain::AUDITOR_RECIPIENT, s_ecdh, sigma); + assert(ss3_r[0] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0); + assert(ss3_r[1] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1); + + assert( + encrypt_esc_dvk_auditor(dvk, s_ecdh, op_i) + == 0x0c3e3560bd6c62d83e90b731e346ac30eb4b87bee1a3218b2bb71b0645e4f09c, + ); + // poseidon_with_domain funnel is the same operation that backs vk_from_sk. assert(poseidon_with_domain(domain::VIEWING_KEY, [sk, addr_f]) == vk); } @@ -479,6 +557,10 @@ global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0: Field = 0x0801e6d7184cea11e6225fcfb4e88a90674cb085da8ebc04918c57cc44b9c25d; global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1: Field = 0x131187e2ac296c1c54c2be2a5cbe614644f01c754108ce25359212d67b36ecc3; +// Lane 2 of the same permutation -- the sender-auditor secret-escrow slot. +// Mirrored in `testdata/sponge_squeeze_3.json`. +global SPONGE_SQUEEZE_3_AUDITOR_SENDER_2: Field = + 0x1c4e67103733b87be6672e0af91fc5416b6d582bf99d5a0f78ea6aaf5fab20b0; #[test] fn derive_helpers_deterministic_and_distinct() { diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json new file mode 100644 index 000000000..5bdd648a2 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json @@ -0,0 +1,15 @@ +{ + "primitive": "encrypt_esc_dvk_auditor", + "design_doc_refs": ["Section 7.11", "Constraint S14"], + "description": "Auditor-side delegation-key escrow: dvk_cipher_aud = dvk + Poseidon2(ESC_DVK_AUD, s_a_s, op_i) where s_a_s = ecdh(r_e, K_aud_s) is the owner-auditor shared scalar. Same plaintext and same shape as encrypt_esc_dvk, under a distinct tag (17 rather than 10) and a distinct shared scalar: this one goes to the owner's auditor, the other to the spender. Holding dvk_i is what lets the auditor derive r_a = derive_allow_r(dvk_i, sigma_a) and therefore open the allowance commitment C_a.", + "vectors": [ + { + "inputs": { + "dvk": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9", + "s_a_s": "0x12345", + "op_i": "0xabcd" + }, + "output": "0x0c3e3560bd6c62d83e90b731e346ac30eb4b87bee1a3218b2bb71b0645e4f09c" + } + ] +} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json new file mode 100644 index 000000000..21903ec79 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json @@ -0,0 +1,15 @@ +{ + "primitive": "sponge_squeeze_3", + "design_doc_refs": ["Section 2.5", "Constraint W_a3", "Constraint T_a6", "Constraint S_a3", "Constraint O_a6"], + "description": "Poseidon2 sponge with three squeezes (one absorb of (d, s, sigma), permute, output (state[0], state[1], state[2])). Same absorb and same permutation as sponge_squeeze_2, so lanes 0 and 1 are bit-identical to that primitive's outputs on the same inputs -- verify that agreement before trusting an implementation. Canonical lane order: 0 = amount mask, 1 = balance / allowance / per-transfer randomness mask, 2 = the sender-auditor secret-escrow slot, which carries the new spendable blinding on the three checkpoint operations (Withdraw, Transfer sender side, SetSpender) and the delegation viewing key on spender transfers. Only the sender-auditor channel, d = AUDITOR_SENDER (11), is squeezed three-wide; the recipient-auditor channel, d = AUDITOR_RECIPIENT (12), stays at two lanes.", + "vectors": [ + { + "inputs": { "d": "0x0b", "s": "0x12345", "sigma": "0x01" }, + "output": [ + "0x2787d1e01bbca7828e13e9b2b3fa11cfcfe9c3d2b121b9e17543146822fe1feb", + "0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17b77", + "0x1c4e67103733b87be6672e0af91fc5416b6d582bf99d5a0f78ea6aaf5fab20b0" + ] + } + ] +} diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr index 8fec9c36d..d95737b01 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr @@ -1,7 +1,7 @@ use stellar_confidential_lib::{ H, assert_on_curve_non_identity, commit, derive_allow_r, derive_spend_r, domain, - dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_balance, encrypt_esc_dvk, scalar_mul, - sponge_squeeze_2, vk_from_sk, + dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_balance, encrypt_esc_dvk, + encrypt_esc_dvk_auditor, scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -59,6 +59,29 @@ mod tests; // + Poseidon2(delta_esc_dvk, s_esc, op_i) // Masked dvk_i for the // spender. +// S14 dvk_cipher_aud = dvk_i +// + Poseidon2(delta_esc_dvk_aud, s_{a,s}, op_i) +// Auditor-side escrow +// of dvk_i. Hands the +// owner's auditor the +// full opening of C_a. +// Folding an unopenable +// C_a into C_spend +// would desync the +// auditor's accumulator +// for the account +// permanently. +// A separate single- +// output Poseidon +// rather than a sponge +// lane, because lane 2 +// of this channel is +// taken by S_a6. Costs +// one Poseidon and zero +// scalar muls. +// Unconditional -- there +// is no delegation that +// can exist without it. // S13 r_e != 0 Rules out R_e = O, // S_{a,s} = O, and a // trivial escrow @@ -73,15 +96,20 @@ mod tests; // key; see S12(a)). // S_a2 s_{a,s} = ecdh(r_e, K_aud_s) Owner-auditor ECDH // shared scalar. -// S_a3 (m_v, m_b) = SpongeSqueeze_2(delta_aud_s, +// S_a3 (m_v, m_b, m_r) +// = SpongeSqueeze_3(delta_aud_s, // s_{a,s}, sigma) Owner-channel -// sponge: two masks. +// sponge: three masks. // S_a4 v_tilde_aud_s = v_a + m_v Owner-auditor // encrypted escrow // amount. // S_a5 b_tilde_aud_s = (v - v_a) + m_b Owner-auditor // encrypted balance // checkpoint. +// S_a6 r_tilde_aud_s = r' + m_r Owner-auditor escrow +// of the new spendable +// blinding, over S9's +// r'. // // Point-validation doctrine (Section 10.8) // ---------------------------------------- @@ -96,7 +124,7 @@ mod tests; // explicitly validates it on-curve AND non-identity before the auditor block // runs. // -// Public inputs (24 fields, in design-doc canonical order) +// Public inputs (26 fields, in design-doc canonical order) // -------------------------------------------------------- // Idx Param Symbol Source / Note // --- ----- ------ ----------------------------- @@ -140,6 +168,14 @@ mod tests; // 23 b_tilde_aud_s b_tilde_aud_s Prover-supplied owner-auditor // encrypted balance checkpoint; // emitted. +// 24 r_tilde_aud_s r_tilde_aud_s Prover-supplied owner-auditor +// escrow of the new spendable +// blinding; emitted. +// 25 dvk_cipher_aud dvk_cipher_aud Prover-supplied owner-auditor +// escrow of dvk_i; emitted. +// Distinct from +// escrowed_dvk_cipher, which is +// the spender-side escrow. // // Private witnesses // ----------------- @@ -180,6 +216,8 @@ fn main( r_e_y: pub Field, v_tilde_aud_s: pub Field, b_tilde_aud_s: pub Field, + r_tilde_aud_s: pub Field, + dvk_cipher_aud: pub Field, ) { // S13 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity (R_e, S_{a,s}, @@ -263,12 +301,20 @@ fn main( // S_a2 (owner-auditor shared-secret scalar) let s_a_s = ecdh(r_e, k_aud_s); - // S_a3 (owner-channel masks: amount, then balance) - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, sigma); + // S_a3 (owner-channel masks: amount, balance, then the secret-escrow slot) + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma); // S_a4 assert(v_a + m_s[0] == v_tilde_aud_s); // S_a5 assert(v_new + m_s[1] == b_tilde_aud_s); + + // S_a6 + assert(r_new + m_s[2] == r_tilde_aud_s); + + // S14 -- reuses s_a_s from S_a2, so no new scalar multiplication. Runs + // after the auditor block so the shared scalar is already in scope. + let dvk_cipher_aud_derived = encrypt_esc_dvk_auditor(dvk, s_a_s, op_i); + assert(dvk_cipher_aud_derived == dvk_cipher_aud); } diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr index 082a71041..b2616068b 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr @@ -1,7 +1,8 @@ use crate::main; use stellar_confidential_lib::{ commit, derive_allow_r, derive_spend_r, domain, dvk_from_vk_op, ecdh, encrypt_allowance, - encrypt_balance, encrypt_esc_dvk, H, scalar_mul, sponge_squeeze_2, vk_from_sk, + encrypt_balance, encrypt_esc_dvk, encrypt_esc_dvk_auditor, H, scalar_mul, + sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -83,6 +84,11 @@ global ESCROWED_DVK_CIPHER: Field = 0x1ef89e3bd93ec1afed649d150e9d0ff5df72002759e260cb66b06604a4d7ebe0; global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c4397; +// Lane-2 secret-escrow slot (S_a6) and the auditor-side dvk escrow (S14). +global R_TILDE_AUD_S: Field = + 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; +global DVK_CIPHER_AUD: Field = + 0x18dc4199771af4fe37c1993b95c8d3188fc0a58080185b4939d359555a6799e1; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c07c; @@ -109,9 +115,11 @@ fn print_fixtures() { let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); let r_e_pt = scalar_mul(R_E, H); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); let v_tilde_aud_s = V_A + m_s[0]; let b_tilde_aud_s = V_NEW + m_s[1]; + let r_tilde_aud_s = r_new + m_s[2]; + let dvk_cipher_aud = encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I); let yopx = y_op.x; let yopy = y_op.y; @@ -138,6 +146,8 @@ fn print_fixtures() { println(f"R_E_Y = {rey}"); println(f"V_TILDE_AUD_S = {v_tilde_aud_s}"); println(f"B_TILDE_AUD_S = {b_tilde_aud_s}"); + println(f"R_TILDE_AUD_S = {r_tilde_aud_s}"); + println(f"DVK_CIPHER_AUD = {dvk_cipher_aud}"); } #[test] @@ -178,7 +188,8 @@ fn set_spender_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 = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let r_new = derive_spend_r(vk, SIGMA); assert(k_aud_s.x == K_AUD_S_X); assert(k_aud_s.y == K_AUD_S_Y); @@ -187,6 +198,8 @@ fn set_spender_auditor_fixtures_match_lib() { assert(escrowed_dvk_cipher == ESCROWED_DVK_CIPHER); assert(V_A + m_s[0] == V_TILDE_AUD_S); assert(V_NEW + m_s[1] == B_TILDE_AUD_S); + assert(r_new + m_s[2] == R_TILDE_AUD_S); + assert(encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I) == DVK_CIPHER_AUD); } // `run_main` accepts all 24 public inputs as parameters -- nothing baked in. @@ -224,6 +237,8 @@ fn run_main( r_e_y_in: Field, v_tilde_aud_s_in: Field, b_tilde_aud_s_in: Field, + r_tilde_aud_s_in: Field, + dvk_cipher_aud_in: Field, ) { main( sk_in, @@ -255,6 +270,8 @@ fn run_main( r_e_y_in, v_tilde_aud_s_in, b_tilde_aud_s_in, + r_tilde_aud_s_in, + dvk_cipher_aud_in, ); } @@ -289,6 +306,8 @@ fn run_fixture() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -316,7 +335,7 @@ fn full_allowance_escrow() { let escrowed_dvk_cipher = encrypt_esc_dvk(dvk, s_esc, OP_I); let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -347,6 +366,8 @@ fn full_allowance_escrow() { R_E_Y, V + m_s[0], 0 + m_s[1], + r_new + m_s[2], + encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I), ); } @@ -369,7 +390,7 @@ fn rejects_under_funded_escrow() { let escrowed_dvk_cipher = encrypt_esc_dvk(dvk, s_esc, OP_I); let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -400,6 +421,8 @@ fn rejects_under_funded_escrow() { R_E_Y, v_a_too_large + m_s[0], (V - v_a_too_large) + m_s[1], + r_new + m_s[2], + encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I), ); } @@ -421,7 +444,7 @@ fn rejects_v_out_of_range() { let b_tilde = encrypt_balance(v_new_huge, vk, SIGMA); let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, v_huge, @@ -452,6 +475,8 @@ fn rejects_v_out_of_range() { R_E_Y, V_TILDE_AUD_S, v_new_huge + m_s[1], + r_new + m_s[2], + encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), ); } @@ -478,7 +503,7 @@ fn rejects_v_a_out_of_range() { let escrowed_dvk_cipher = encrypt_esc_dvk(dvk, s_esc, OP_I); let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -509,6 +534,8 @@ fn rejects_v_a_out_of_range() { R_E_Y, v_a_huge + m_s[0], v_new_huge + m_s[1], + r_new + m_s[2], + encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), ); } @@ -561,6 +588,8 @@ fn rejects_wrong_sk() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -578,7 +607,7 @@ fn rejects_wrong_balance_opening() { let b_tilde_bad = encrypt_balance(v_new_bad, vk, SIGMA); let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, v_bad, @@ -609,6 +638,8 @@ fn rejects_wrong_balance_opening() { R_E_Y, V_TILDE_AUD_S, v_new_bad + m_s[1], + r_new + m_s[2], + encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), ); } @@ -659,6 +690,8 @@ fn rejects_wrong_addr_f() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -700,6 +733,8 @@ fn rejects_wrong_op_i() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -736,6 +771,8 @@ fn rejects_tampered_c_a() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -773,6 +810,8 @@ fn rejects_tampered_a_tilde() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -809,6 +848,8 @@ fn rejects_tampered_c_spend_new() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -845,6 +886,8 @@ fn rejects_tampered_b_tilde() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -884,6 +927,8 @@ fn rejects_tampered_escrowed_dvk_r_x() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -921,6 +966,8 @@ fn rejects_tampered_escrowed_dvk_cipher() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -962,6 +1009,8 @@ fn rejects_wrong_y_op() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -983,7 +1032,8 @@ fn rejects_r_e_zero() { let s_esc_zero = ecdh(0, y_op); let escrowed_dvk_cipher_zero = encrypt_esc_dvk(dvk, s_esc_zero, OP_I); let s_a_s_zero = ecdh(0, k_aud_s); - let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA); + let m_s_zero = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA); + let r_new = derive_spend_r(vk, SIGMA); run_main( SK, V, @@ -1014,6 +1064,8 @@ fn rejects_r_e_zero() { 0, V_A + m_s_zero[0], V_NEW + m_s_zero[1], + r_new + m_s_zero[2], + encrypt_esc_dvk_auditor(dvk, s_a_s_zero, OP_I), ); } @@ -1035,7 +1087,8 @@ fn rejects_wrong_r_e() { let s_esc_bad = ecdh(r_e_bad, y_op); let escrowed_dvk_cipher_bad = encrypt_esc_dvk(dvk, s_esc_bad, OP_I); let s_a_s_bad = ecdh(r_e_bad, k_aud_s); - let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA); + let m_s_bad = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA); + let r_new = derive_spend_r(vk, SIGMA); run_main( SK, V, @@ -1066,6 +1119,8 @@ fn rejects_wrong_r_e() { R_E_Y, V_A + m_s_bad[0], V_NEW + m_s_bad[1], + r_new + m_s_bad[2], + encrypt_esc_dvk_auditor(dvk, s_a_s_bad, OP_I), ); } @@ -1103,6 +1158,8 @@ fn rejects_off_curve_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1140,6 +1197,8 @@ fn rejects_identity_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1177,6 +1236,8 @@ fn rejects_wrong_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1213,6 +1274,8 @@ fn rejects_tampered_v_tilde_aud_s() { R_E_Y, V_TILDE_AUD_S + 1, B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1249,5 +1312,7 @@ fn rejects_tampered_b_tilde_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S + 1, + R_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr index d419818a9..aad3acb5a 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr @@ -1,6 +1,6 @@ use stellar_confidential_lib::{ H, assert_on_curve_non_identity, commit, derive_allow_r, derive_transfer_blind, domain, ecdh, - encrypt_allowance, encrypt_amount, scalar_mul, sponge_squeeze_2, + encrypt_allowance, encrypt_amount, scalar_mul, sponge_squeeze_2, sponge_squeeze_3, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -86,16 +86,36 @@ mod tests; // OWNER, not the // spender (Section // 8.4). -// O_a6 (m_v_s, m_a_s) -// = SpongeSqueeze_2(delta_aud_s, +// O_a6 (m_v_s, m_a_s, m_r_s) +// = SpongeSqueeze_3(delta_aud_s, // s_{a,s}, sigma_a) Owner-channel -// sponge: two masks. +// sponge: three masks. // O_a7 v_tilde_aud_s = v_transfer + m_v_s Owner-auditor // encrypted amount. // O_a8 a_tilde_aud_s = (v_a - v_transfer) + m_a_s Owner-auditor // encrypted // post-transfer // allowance. +// O_a9 dvk_cipher_aud = dvk_i + m_r_s Owner-auditor +// re-escrow of the +// delegation viewing +// key. dvk_i is +// already a witness +// (O2), so this is +// one field addition. +// Buys one-operation +// convergence for a +// live delegation +// whose auditor key +// rotated after +// set_spender -- the +// same property every +// other auditor +// ciphertext has. It +// does nothing for a +// frozen account, +// which emits no +// ciphertext at all. // // Contract binding (Section 7.8 final paragraph) // --------------------------------------------- @@ -132,7 +152,7 @@ mod tests; // explicitly validates both on-curve AND non-identity before the // corresponding ECDH consumes them. // -// Public inputs (24 fields, in design-doc canonical order) +// Public inputs (25 fields, in design-doc canonical order) // -------------------------------------------------------- // Idx Param Symbol Source / Note // --- ----- ------ ------------------------------- @@ -177,6 +197,8 @@ mod tests; // 23 a_tilde_aud_s a_tilde_aud_s Prover-supplied owner-auditor // encrypted post-transfer // allowance; emitted. +// 24 dvk_cipher_aud dvk_cipher_aud Prover-supplied owner-auditor +// re-escrow of dvk_i; emitted. // // Private witnesses // ----------------- @@ -220,6 +242,7 @@ fn main( r_tilde_aud_r: pub Field, v_tilde_aud_s: pub Field, a_tilde_aud_s: pub Field, + dvk_cipher_aud: pub Field, ) { // O13 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity. @@ -308,12 +331,17 @@ fn main( // O_a5 (owner-auditor shared-secret scalar) let s_a_s = 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, sigma_a); + // O_a6 (owner-channel masks: amount, post-transfer allowance, then the + // secret-escrow slot) + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma_a); // O_a7 assert(v_transfer + m_s[0] == v_tilde_aud_s); // O_a8 assert(v_a_new + m_s[1] == a_tilde_aud_s); + + // O_a9 -- dvk_i is already a private witness (O2), so this is one field + // addition: zero scalar muls, no new domain tag. + assert(dvk_i + m_s[2] == dvk_cipher_aud); } diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr index 6337933ab..d2fc71d94 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -1,7 +1,7 @@ use crate::main; use stellar_confidential_lib::{ commit, derive_allow_r, derive_transfer_blind, domain, ecdh, encrypt_allowance, encrypt_amount, H, - scalar_mul, sponge_squeeze_2, + scalar_mul, sponge_squeeze_2, sponge_squeeze_3, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -70,6 +70,10 @@ global R_TILDE_AUD_R: Field = 0x16cb31d142cf3005f8bd95ab3f59c3bda0da0de7e1d2ac16b4d038548a02b364; global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c42cf; +// Lane-2 secret-escrow slot (O_a9): the delegation viewing key, re-escrowed +// to the owner's auditor on every spender transfer. +global DVK_CIPHER_AUD: Field = + 0x10a1e8766310b1369a48b22e12f01b8610041bf25bc1807d01812491f9702f57; global A_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; @@ -103,9 +107,10 @@ fn print_fixtures() { let r_tilde_aud_r = r_transfer + m_r[1]; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); let v_tilde_aud_s = V_TRANSFER + m_s[0]; let a_tilde_aud_s = V_A_NEW + m_s[1]; + let dvk_cipher_aud = DVK_I + m_s[2]; let yopx = y_op.x; let yopy = y_op.y; @@ -146,6 +151,7 @@ fn print_fixtures() { 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}"); + println(f"DVK_CIPHER_AUD = {dvk_cipher_aud}"); } #[test] @@ -188,7 +194,7 @@ fn spender_transfer_auditor_fixtures_match_lib() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); assert(k_aud_r.x == K_AUD_R_X); assert(k_aud_r.y == K_AUD_R_Y); @@ -200,6 +206,7 @@ fn spender_transfer_auditor_fixtures_match_lib() { assert(r_transfer + m_r[1] == R_TILDE_AUD_R); assert(V_TRANSFER + m_s[0] == V_TILDE_AUD_S); assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); + assert(DVK_I + m_s[2] == DVK_CIPHER_AUD); } // `run_main` accepts every public input as a parameter -- nothing baked in. @@ -238,6 +245,7 @@ fn run_main( r_tilde_aud_r_in: Field, v_tilde_aud_s_in: Field, a_tilde_aud_s_in: Field, + dvk_cipher_aud_in: Field, ) { main( sk_op_in, @@ -270,6 +278,7 @@ fn run_main( r_tilde_aud_r_in, v_tilde_aud_s_in, a_tilde_aud_s_in, + dvk_cipher_aud_in, ); } @@ -305,6 +314,7 @@ fn run_fixture() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -342,7 +352,7 @@ fn second_transfer_against_same_allowance() { let s_a_r = ecdh(r_e_2, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); let s_a_s = ecdh(r_e_2, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -375,6 +385,7 @@ fn second_transfer_against_same_allowance() { r_transfer + m_r[1], v_transfer_2 + m_s[0], v_a_new_2 + m_s[1], + DVK_I + m_s[2], ); } @@ -399,7 +410,7 @@ fn full_allowance_transfer() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -432,6 +443,7 @@ fn full_allowance_transfer() { r_transfer + m_r[1], V_A + m_s[0], 0 + m_s[1], + DVK_I + m_s[2], ); } @@ -469,6 +481,7 @@ fn rejects_wrong_sk_op() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -542,6 +555,7 @@ fn rejects_wrong_dvk_against_contract() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -557,7 +571,7 @@ fn rejects_wrong_allowance_opening() { 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 = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -590,6 +604,7 @@ fn rejects_wrong_allowance_opening() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_bad + m_s[1], + DVK_I + m_s[2], ); } @@ -615,7 +630,7 @@ fn rejects_v_a_new_out_of_range() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -648,6 +663,7 @@ fn rejects_v_a_new_out_of_range() { r_transfer + m_r[1], v_transfer_too_large + m_s[0], (V_A - v_transfer_too_large) + m_s[1], + DVK_I + m_s[2], ); } @@ -669,7 +685,7 @@ fn rejects_v_a_out_of_range() { 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 = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -702,6 +718,7 @@ fn rejects_v_a_out_of_range() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_huge + m_s[1], + DVK_I + m_s[2], ); } @@ -727,7 +744,7 @@ fn rejects_v_transfer_out_of_range() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); run_main( SK_OP, @@ -760,6 +777,7 @@ fn rejects_v_transfer_out_of_range() { r_transfer + m_r[1], v_transfer_huge + m_s[0], v_a_new_huge + m_s[1], + DVK_I + m_s[2], ); } @@ -800,6 +818,7 @@ fn rejects_wrong_recipient_pvk() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -847,6 +866,7 @@ fn rejects_poisoned_c_transfer() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -884,6 +904,7 @@ fn rejects_tampered_v_tilde() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -929,6 +950,7 @@ fn rejects_tampered_a_tilde_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -966,6 +988,7 @@ fn rejects_tampered_c_a_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1013,6 +1036,7 @@ fn rejects_sigma_a_new_equal_to_sigma_a() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1039,7 +1063,7 @@ fn rejects_r_e_zero() { let s_a_r_zero = ecdh(0, k_aud_r); let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_zero, SIGMA_A); let s_a_s_zero = ecdh(0, k_aud_s); - let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA_A); + let m_s_zero = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA_A); run_main( SK_OP, @@ -1072,6 +1096,7 @@ fn rejects_r_e_zero() { r_transfer_zero + m_r_zero[1], V_TRANSFER + m_s_zero[0], V_A_NEW + m_s_zero[1], + DVK_I + m_s_zero[2], ); } @@ -1095,7 +1120,7 @@ fn rejects_wrong_r_e() { let s_a_r_bad = ecdh(r_e_bad, k_aud_r); let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_bad, SIGMA_A); let s_a_s_bad = ecdh(r_e_bad, k_aud_s); - let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA_A); + let m_s_bad = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA_A); run_main( SK_OP, @@ -1128,6 +1153,7 @@ fn rejects_wrong_r_e() { r_transfer_bad + m_r_bad[1], V_TRANSFER + m_s_bad[0], V_A_NEW + m_s_bad[1], + DVK_I + m_s_bad[2], ); } @@ -1166,6 +1192,7 @@ fn rejects_off_curve_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1204,6 +1231,7 @@ fn rejects_identity_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1240,6 +1268,7 @@ fn rejects_off_curve_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1276,6 +1305,7 @@ fn rejects_identity_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1313,6 +1343,7 @@ fn rejects_tampered_v_tilde_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1350,6 +1381,7 @@ fn rejects_tampered_r_tilde_aud_r() { R_TILDE_AUD_R + 1, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1387,6 +1419,7 @@ fn rejects_tampered_v_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S + 1, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1424,6 +1457,7 @@ fn rejects_tampered_a_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S + 1, + DVK_CIPHER_AUD, ); } @@ -1462,6 +1496,7 @@ fn rejects_wrong_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } @@ -1500,5 +1535,6 @@ fn rejects_wrong_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + DVK_CIPHER_AUD, ); } diff --git a/packages/tokens/src/confidential/circuits/transfer/src/main.nr b/packages/tokens/src/confidential/circuits/transfer/src/main.nr index e7dbedb49..2574c68f9 100644 --- a/packages/tokens/src/confidential/circuits/transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/transfer/src/main.nr @@ -1,6 +1,6 @@ use stellar_confidential_lib::{ H, assert_on_curve_non_identity, commit, derive_spend_r, derive_transfer_blind, domain, ecdh, - encrypt_amount, encrypt_balance, scalar_mul, sponge_squeeze_2, vk_from_sk, + encrypt_amount, encrypt_balance, scalar_mul, sponge_squeeze_2, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -68,15 +68,23 @@ mod tests; // T_a5 s_{a,s} = ecdh(r_e, K_aud_s) Sender-auditor ECDH // shared scalar // (reuses r_e). -// T_a6 (m_v_s, m_b_s) -// = SpongeSqueeze_2(delta_aud_s, +// T_a6 (m_v_s, m_b_s, m_r_s) +// = SpongeSqueeze_3(delta_aud_s, // s_{a,s}, sigma) Sender-channel -// sponge: two masks. +// sponge: three masks. // T_a7 v_tilde_aud_s = v_transfer + m_v_s Sender-auditor // encrypted amount. // T_a8 b_tilde_aud_s = (v_A - v_transfer) + m_b_s Sender-auditor // encrypted balance // checkpoint. +// T_a9 r_tilde_aud_s = r_A' + m_r_s Sender-auditor +// escrow of the new +// spendable blinding, +// over T10's r_A'. +// Lets the auditor +// track the full +// opening of C_spend +// forward. // // Point-validation doctrine (Section 10.8) // ---------------------------------------- @@ -91,7 +99,7 @@ mod tests; // T_a1 / T_a5, so this file explicitly validates both on-curve AND // non-identity before the auditor block runs. // -// Public inputs (24 fields, in design-doc canonical order) +// Public inputs (25 fields, in design-doc canonical order) // -------------------------------------------------------- // Idx Param Symbol Source / Note // --- ----- ------ ----------------------------------- @@ -127,6 +135,9 @@ mod tests; // 23 b_tilde_aud_s b_tilde_aud_s Prover-supplied sender-auditor // encrypted balance checkpoint; // emitted. +// 24 r_tilde_aud_s r_tilde_aud_s Prover-supplied sender-auditor +// escrow of the new spendable +// blinding; emitted. // // Private witnesses // ----------------- @@ -167,6 +178,7 @@ fn main( r_tilde_aud_r: pub Field, v_tilde_aud_s: pub Field, b_tilde_aud_s: pub Field, + r_tilde_aud_s: pub Field, ) { // T13 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity. @@ -248,12 +260,16 @@ fn main( // T_a5 (sender-auditor shared-secret scalar) let s_a_s = ecdh(r_e, k_aud_s); - // T_a6 (sender-channel masks: amount, then balance) - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, sigma); + // T_a6 (sender-channel masks: amount, balance, then the secret-escrow + // slot) + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma); // T_a7 assert(v_transfer + m_s[0] == v_tilde_aud_s); // T_a8 assert(v_new + m_s[1] == b_tilde_aud_s); + + // T_a9 + assert(r_new + m_s[2] == r_tilde_aud_s); } diff --git a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr index 5ff281fbd..dd2254803 100644 --- a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr @@ -1,7 +1,7 @@ use crate::main; use stellar_confidential_lib::{ commit, derive_spend_r, derive_transfer_blind, domain, ecdh, encrypt_amount, encrypt_balance, H, - scalar_mul, sponge_squeeze_2, vk_from_sk, + scalar_mul, sponge_squeeze_2, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -71,6 +71,9 @@ global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c42cf; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; +// Lane-2 secret-escrow slot: r_tilde_aud_s = r_A' + m_r_s, over T10's r_A'. +global R_TILDE_AUD_S: Field = + 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; #[test] fn print_fixtures() { @@ -99,9 +102,10 @@ fn print_fixtures() { let r_tilde_aud_r = r_transfer + m_r[1]; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); let v_tilde_aud_s = V_TRANSFER + m_s[0]; let b_tilde_aud_s = V_NEW + m_s[1]; + let r_tilde_aud_s = r_new + m_s[2]; let pvkx = pvk_b.x; let pvky = pvk_b.y; @@ -133,6 +137,7 @@ fn print_fixtures() { println(f"R_TILDE_AUD_R = {r_tilde_aud_r}"); println(f"V_TILDE_AUD_S = {v_tilde_aud_s}"); println(f"B_TILDE_AUD_S = {b_tilde_aud_s}"); + println(f"R_TILDE_AUD_S = {r_tilde_aud_s}"); } #[test] @@ -174,7 +179,9 @@ fn transfer_auditor_fixtures_match_lib() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let vk = vk_from_sk(SK, ADDR_F); + let r_new = derive_spend_r(vk, SIGMA); assert(k_aud_r.x == K_AUD_R_X); assert(k_aud_r.y == K_AUD_R_Y); @@ -186,6 +193,7 @@ fn transfer_auditor_fixtures_match_lib() { assert(r_transfer + m_r[1] == R_TILDE_AUD_R); assert(V_TRANSFER + m_s[0] == V_TILDE_AUD_S); assert(V_NEW + m_s[1] == B_TILDE_AUD_S); + assert(r_new + m_s[2] == R_TILDE_AUD_S); } // `run_main` accepts all 24 public inputs as parameters -- nothing baked in. @@ -223,6 +231,7 @@ fn run_main( r_tilde_aud_r_in: Field, v_tilde_aud_s_in: Field, b_tilde_aud_s_in: Field, + r_tilde_aud_s_in: Field, ) { main( sk_in, @@ -254,6 +263,7 @@ fn run_main( r_tilde_aud_r_in, v_tilde_aud_s_in, b_tilde_aud_s_in, + r_tilde_aud_s_in, ); } @@ -288,6 +298,7 @@ fn run_fixture() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -316,7 +327,7 @@ fn full_balance_transfer() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -347,6 +358,7 @@ fn full_balance_transfer() { r_transfer + m_r[1], V + m_s[0], 0 + m_s[1], + r_new + m_s[2], ); } @@ -370,7 +382,7 @@ fn rejects_under_funded_transfer() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -401,6 +413,7 @@ fn rejects_under_funded_transfer() { r_transfer + m_r[1], v_transfer_too_large + m_s[0], (V - v_transfer_too_large) + m_s[1], + r_new + m_s[2], ); } @@ -422,7 +435,7 @@ fn rejects_v_out_of_range() { let c_new = commit(v_new_huge, r_new); let b_tilde = encrypt_balance(v_new_huge, vk, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, v_huge, @@ -453,6 +466,7 @@ fn rejects_v_out_of_range() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_new_huge + m_s[1], + r_new + m_s[2], ); } @@ -480,7 +494,7 @@ fn rejects_v_transfer_out_of_range() { let s_a_r = ecdh(R_E, k_aud_r); let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, V, @@ -511,6 +525,7 @@ fn rejects_v_transfer_out_of_range() { r_transfer + m_r[1], v_transfer_huge + m_s[0], v_new_huge + m_s[1], + r_new + m_s[2], ); } @@ -555,6 +570,7 @@ fn rejects_wrong_sk() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -571,7 +587,7 @@ fn rejects_wrong_balance_opening() { let c_new_bad = commit(v_new_bad, r_new); let b_tilde_bad = encrypt_balance(v_new_bad, vk, SIGMA); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); run_main( SK, v_bad, @@ -602,6 +618,7 @@ fn rejects_wrong_balance_opening() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_new_bad + m_s[1], + r_new + m_s[2], ); } @@ -648,6 +665,7 @@ fn rejects_wrong_addr_f() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -686,6 +704,7 @@ fn rejects_wrong_recipient_pvk() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -730,6 +749,7 @@ fn rejects_poisoned_c_transfer() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -766,6 +786,7 @@ fn rejects_tampered_v_tilde() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -802,6 +823,7 @@ fn rejects_tampered_b_tilde() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -838,6 +860,7 @@ fn rejects_tampered_c_spend_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -863,7 +886,8 @@ fn rejects_r_e_zero() { let s_a_r_zero = ecdh(0, k_aud_r); let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_zero, SIGMA); let s_a_s_zero = ecdh(0, k_aud_s); - let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA); + let m_s_zero = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA); + let r_new = derive_spend_r(vk_from_sk(SK, ADDR_F), SIGMA); run_main( SK, V, @@ -894,6 +918,7 @@ fn rejects_r_e_zero() { r_transfer_zero + m_r_zero[1], V_TRANSFER + m_s_zero[0], V_NEW + m_s_zero[1], + r_new + m_s_zero[2], ); } @@ -915,7 +940,8 @@ fn rejects_wrong_r_e() { let s_a_r_bad = ecdh(r_e_bad, k_aud_r); let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_bad, SIGMA); let s_a_s_bad = ecdh(r_e_bad, k_aud_s); - let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA); + let m_s_bad = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA); + let r_new = derive_spend_r(vk_from_sk(SK, ADDR_F), SIGMA); run_main( SK, V, @@ -946,6 +972,7 @@ fn rejects_wrong_r_e() { r_transfer_bad + m_r_bad[1], V_TRANSFER + m_s_bad[0], V_NEW + m_s_bad[1], + r_new + m_s_bad[2], ); } @@ -983,6 +1010,7 @@ fn rejects_off_curve_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1020,6 +1048,7 @@ fn rejects_identity_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1055,6 +1084,7 @@ fn rejects_off_curve_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1090,6 +1120,7 @@ fn rejects_identity_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1127,6 +1158,7 @@ fn rejects_wrong_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1164,6 +1196,7 @@ fn rejects_wrong_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1200,6 +1233,7 @@ fn rejects_tampered_v_tilde_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1236,6 +1270,7 @@ fn rejects_tampered_r_tilde_aud_r() { R_TILDE_AUD_R + 1, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1272,6 +1307,7 @@ fn rejects_tampered_v_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S + 1, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1308,5 +1344,6 @@ fn rejects_tampered_b_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S + 1, + R_TILDE_AUD_S, ); } diff --git a/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json b/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json index ad7189e2e..66adbe4ba 100644 --- a/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x0000000000000000000000000000001b0bcac9dce30ac04720972a66a06045ea","0x00000000000000000000000000000000001005336c4f381202589b3506ea8f40","0x000000000000000000000000000000de74ae5a9880b088bd173f1253ef36b0f1","0x000000000000000000000000000000000022c52e6340dce6f64c0656851171d7","0x0000000000000000000000000000001697d4f845b0f618397a0859f76b87db3c","0x00000000000000000000000000000000000adff912596c6d0e9127a7a9346265","0x0000000000000000000000000000007a9b82f177aaf60304dd06235035b4141c","0x00000000000000000000000000000000000f0a488a0b134d5d6602bfe3edb649","0x000000000000000000000000000000aa3d3ba88ee808b0f52fbeb71b6326471c","0x00000000000000000000000000000000002dac6cc6570585ab23727b26e0790e","0x00000000000000000000000000000044ff7cf6231dc58f2afa332937fd32f2b6","0x000000000000000000000000000000000000161b6466c09a362b884277791a8d","0x000000000000000000000000000000599dac4cde8b4e76a62a8c0f8106d69b71","0x000000000000000000000000000000000004030c61ba95aa7b622819252ec92a","0x000000000000000000000000000000f19d4ef66d26b5f1ecdcb5d78fda68f9f0","0x0000000000000000000000000000000000226f614c0287897e67e8a52f4b2392","0x0000000000000000000000000000006bc680d3a2fe67c82c091b942fcffd6aea","0x00000000000000000000000000000000001ebd8c4548c120f831a800e7847359","0x0000000000000000000000000000008ba19baf8afa354c14261b6a5665dc8e36","0x00000000000000000000000000000000002e357a2f9d6d16047650a3723501af","0x000000000000000000000000000000f103296c7c0ee57e40360bece45c447647","0x000000000000000000000000000000000004be12eeccc78897157fac581ed031","0x00000000000000000000000000000097033d659d46e14032695d33c195a5ed39","0x00000000000000000000000000000000001034e051dd92a38cf67180452968b5","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x000000000000000000000000000000aba4586d2fb72d76d0392998c8c56f2fc0","0x00000000000000000000000000000000001e8b35596e134832bfbae1a9ddf0eb","0x000000000000000000000000000000a28c3c027e9a472c1663729c83a9c9f61f","0x000000000000000000000000000000000003f5630f2c0e0c6ed17957ca93cc1c","0x000000000000000000000000000000a9b3c578d9cd2f1b1ec4cd74cbb47e588c","0x00000000000000000000000000000000002588344fb20402b460766f9a5e62d7","0x000000000000000000000000000000f6dac0c80d62ca550666d7d5651ca8bedf","0x00000000000000000000000000000000000e40b92ee16be807f7576e7bbf36fa","0x00000000000000000000000000000033fd31cd667d95e8815f8ce997fcf355f7","0x00000000000000000000000000000000001343b3298cd02f5e168eba6d326761","0x000000000000000000000000000000e70069899227e4a42f1a37c20db25ad0bd","0x000000000000000000000000000000000022161b052ea12746192f6ef2c11086","0x000000000000000000000000000000c6ab2981c28c889cc89b97ce23c63e50d2","0x0000000000000000000000000000000000181e22ec1ba42eb1dfac60647880bd","0x000000000000000000000000000000ad3dfcdd8bcc37294862278a1bf1dd376c","0x000000000000000000000000000000000020320840931d4a9f2c030d8aadd1b4","0x000000000000000000000000000000f2e71144323f7b8f1c4bf834b57f905a10","0x00000000000000000000000000000000001e362c6a6547a01dec544d44850762","0x0000000000000000000000000000001e6bd3938c4d6ebfc1a7baebccf8889c2f","0x00000000000000000000000000000000001378d134ae3b553e13c7e875fc8896","0x0000000000000000000000000000003c83eda777391195746d9756b6d1acf5b0","0x0000000000000000000000000000000000297221eab510af95e082f8063670be","0x000000000000000000000000000000d0b7d7279191ed16bbf861d8bca47ae618","0x00000000000000000000000000000000001937c9db207067e0818b6aff991f71","0x0000000000000000000000000000006d484eca98129ae65415906f90a2ff48dd","0x00000000000000000000000000000000000e44731657c7a04b3803a79b0f8486","0x000000000000000000000000000000d62fdd8b978d19f1c710c42200f654c21a","0x00000000000000000000000000000000000e763c5b7f4495c87247d3e13c4e13","0x000000000000000000000000000000c6e6c07d5a41fe47f8d18d134f7e808800","0x000000000000000000000000000000000016e81a27c47bf738979584678dae77","0x00000000000000000000000000000001ae5dfec41320fbc1e7b95db2838c680a","0x000000000000000000000000000000000024cde7dab07610ea93412f11453a32","0x0000000000000000000000000000001ddb85dc8eb1c6edc0045b51a0ee0f48dc","0x00000000000000000000000000000000002eee1fd4872d69bd436afb60791f62","0x000000000000000000000000000000b76f302a35cabe286feef1b2b87807a59e","0x00000000000000000000000000000000002cb81803dea497d27050884f9c57fc","0x000000000000000000000000000000a95e23fe3ad0ed90661fd062bdcbdca094","0x00000000000000000000000000000000000b714b2bd9892df5778ab317918a54","0x0000000000000000000000000000005a60aae5b608307a16b720f25c29de8e6e","0x00000000000000000000000000000000001a319eb1e6d0bccdd75582d6e9f377","0x0000000000000000000000000000000c3ad607a1c36e9f7b4fdb7cb1a7a1b2fd","0x00000000000000000000000000000000001b156a2d46be70bf538ffdbc013ce8","0x000000000000000000000000000000f26cb23c8a10506a2463163c391a289ebd","0x00000000000000000000000000000000000ca204c0a8832cd57c8b878af3df85","0x000000000000000000000000000000ff68879b815fb6861f7627c8ec2114e88d","0x00000000000000000000000000000000000c686cf57a1ed1e0c3b2182623d953","0x000000000000000000000000000000663f00e180b27021afb0215176b19b3d3b","0x00000000000000000000000000000000000bf7230dbfc8df3477fd73d53b98b4","0x00000000000000000000000000000085bd8af700fb001391fc21293e8b549a02","0x0000000000000000000000000000000000068fbe20a938fa7fa34f58cbb64241","0x0000000000000000000000000000009b8b4ef3bff9f8b8d73e4bb08877e38ae9","0x000000000000000000000000000000000010ebddcc02b3858b88870e1c13cd34","0x00000000000000000000000000000022916750324a78c8e6dc452965815fc85a","0x00000000000000000000000000000000000dc88aee00f84c70bea95184b92b5c","0x000000000000000000000000000000b478e858150b16f1b2121e096caa90001b","0x000000000000000000000000000000000010105d3a704f56969fbb1ca64a2754","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000ffec6ec615ca63e74c4d735795decbb02c","0x0000000000000000000000000000000000211ababdad6ea01bfd11d305f2860c","0x000000000000000000000000000000830e6b43237b7bff3b6cccd19f8fafeae8","0x0000000000000000000000000000000000184a72052579e9ff70b59493d34fb2"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000002a","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000001a","0x0000000000000000000000000000001ee8326ff6fa4d9eaa698e6140f07f937b","0x0000000000000000000000000000000000106a0a88936a807b905fd25fa6eae5","0x0000000000000000000000000000007c8a46d26563134c136509e706b746f1c4","0x00000000000000000000000000000000000590438230856dc4e29b2b487f82a6","0x0000000000000000000000000000008fda4fa9a21711c3439d8c7bdc8706a35a","0x000000000000000000000000000000000026ee93cae09f66482049b8ab9f18ee","0x000000000000000000000000000000fa6519a25edb2d71cde10a7979794e21ff","0x00000000000000000000000000000000000783403486a30c30781b1336b06de0","0x0000000000000000000000000000002c88f0e429c3aa1b48f7f93509eb30bc9a","0x00000000000000000000000000000000000d417179f4c315c350cc1107b9cfc9","0x000000000000000000000000000000b0b25c47633b74d1a2bf1cd00ffce7b12c","0x000000000000000000000000000000000007ce4d696b72e2d8452d52a7caf9de","0x0000000000000000000000000000002d12248226512f020e334317dae6eaf0c8","0x000000000000000000000000000000000018cccf723238de695bcde25904b27d","0x000000000000000000000000000000f88387a18ddc16904307058f7d7a883e4a","0x00000000000000000000000000000000001d06b3270da56a3607007ee7ad0f1e","0x000000000000000000000000000000ee1a79ad9afc1d02b840e27c5283e74499","0x000000000000000000000000000000000028d1b1924fe7cf03f605e701eccd20","0x000000000000000000000000000000a2e8413b26a238c1274c1c4bc1bf3fa366","0x000000000000000000000000000000000028554b198c6912b267e8a6bf0987da","0x000000000000000000000000000000725ddea9830a058064e593ae1150357563","0x00000000000000000000000000000000000be21052e0ca4bbc01af225ab441ef","0x00000000000000000000000000000039b1667579c4ac66601d7962273b2bd832","0x00000000000000000000000000000000001e82560f75396646bb960ee9ec317b","0x000000000000000000000000000000933174c97d07b3e2e562d13bd83ed809d0","0x00000000000000000000000000000000002c183f1ad7406f0e39afd5ae8a4d66","0x000000000000000000000000000000cf09eb98da45a5b7fd4fc995638326c2a7","0x00000000000000000000000000000000000465c7d5dfc1c170aa4885b2933dfa","0x0000000000000000000000000000004488817604cfb82b60e35ade6c2bfbbde4","0x000000000000000000000000000000000025df503deb0f9d455fd7fdbc35501d","0x0000000000000000000000000000005b0af382eaa78b050823e07021a2596730","0x000000000000000000000000000000000013296f3f21d5be36f6c3de4f7e12bb","0x00000000000000000000000000000013e4981e445ea861b4d191c94874786ced","0x000000000000000000000000000000000009f2a00d23e559fc77b790f178d644","0x0000000000000000000000000000005a04ff9ac166a13efe0bbb42af5a9370a2","0x00000000000000000000000000000000001d488faa8537a3184ed1838cfa8da9","0x0000000000000000000000000000006ebe4f355ba6a3fc1d7e57f657359fcf7e","0x00000000000000000000000000000000001970bb0ec1ae1d702597e0504befc0","0x000000000000000000000000000000ff7fa0375289b0c62312d5c20c6e9b9d82","0x0000000000000000000000000000000000242ff04c2fb35a5b4940794a5e0c9b","0x000000000000000000000000000000e24c5739a33f3016deeb3150ca6cd333ed","0x0000000000000000000000000000000000168f59e1c5afdc0b6f4351e098969d","0x0000000000000000000000000000008fabee483e7aa0b20e40295d2fee6e79da","0x0000000000000000000000000000000000121880c763fa3fd1bdfc39945bf18a","0x000000000000000000000000000000df647f1842d3f41f64ad17f3fef8cad92a","0x00000000000000000000000000000000000baa779152f64c30acd67fb6a96532","0x00000000000000000000000000000040611399572dd13dfc9e5711b25a957117","0x00000000000000000000000000000000000233a10ee44b41e3123d589039619e","0x000000000000000000000000000000ed142ab059b436c2440f0567e5985e038e","0x0000000000000000000000000000000000107423e6071c6594e7960804836a87","0x0000000000000000000000000000008c0b459dfd4818f2ce5d4622d460ac4b3b","0x000000000000000000000000000000000027f96010efb256b54377ea2cf2203b","0x00000000000000000000000000000026c87845540abf9fb61ca692bb9b09cbdf","0x000000000000000000000000000000000025253d1360866f06ffaf41fbbe02cd","0x00000000000000000000000000000007173b04261b3c7cd9d727708f105a9885","0x00000000000000000000000000000000001d7182ce722bde4dfaff0d58d75980","0x0000000000000000000000000000001b1f57f97ae7e7875b0692f61d07eaa2e9","0x000000000000000000000000000000000022e1317bfd6ab50566ca1997c94fe9","0x000000000000000000000000000000332b66feaefc3aaa35d31970310ecbb369","0x000000000000000000000000000000000023a7e369eab70d6081c8c3f5f3bbe9","0x000000000000000000000000000000a248122c42f86368af6a0c74a9a25d5f9c","0x000000000000000000000000000000000000ee8f7378796672556adecf8f6c63","0x000000000000000000000000000000e8c233c18d70dfd6675ae5dd7df5e29271","0x00000000000000000000000000000000002c56c41a07a3006c2830ac414004e7","0x000000000000000000000000000000c675e97962a8230f86a1093d714e9f0f05","0x00000000000000000000000000000000001b125d3316c14b62c532f6b8b2e4fe","0x00000000000000000000000000000089bd1899f66cf8f75b58a2b60a9de48cf7","0x00000000000000000000000000000000002706f62382cc7e17c7e531832ff966","0x000000000000000000000000000000d26a37225805d4e0170474e9e02188fb21","0x00000000000000000000000000000000001e50e6e7ad7be4d8dacbf29e940af4","0x000000000000000000000000000000efba449cb7d241c0dd520375104721726d","0x00000000000000000000000000000000001b095bb696a673bd0c5a116e27b404","0x0000000000000000000000000000006dba6a66fc696e4d1e69e3c6a93317ec77","0x000000000000000000000000000000000019ec4850bfd37b9ff6c6c37ee18ea0","0x000000000000000000000000000000e2f05188e6cf778a0e83cbd802df2137dc","0x00000000000000000000000000000000000648fd3d1938953473fbf1992a1a0d","0x0000000000000000000000000000004b8a4408e4550a88712539ac23123da4d9","0x00000000000000000000000000000000002d42bdc43b945b84bb790ce832b4fa","0x0000000000000000000000000000001c3b96a17adf9ede03ef8631fb3dd882e9","0x000000000000000000000000000000000002042c33e5a67a0466ab51daa1cd58","0x000000000000000000000000000000c718f214b89a0282b726a88d928a48a5e1","0x00000000000000000000000000000000001adfcfe9e07545d3f43ca4491dd2ef","0x000000000000000000000000000000b01dbb08a6e78d278fb9777e55f5062e2c","0x0000000000000000000000000000000000122a27907479081d47ce05e12a9bc2","0x0000000000000000000000000000008273c63c240753dd46d3ebe5905c877ceb","0x000000000000000000000000000000000029bf117651e308871fd01200cd3d97","0x000000000000000000000000000000e7c89b48a1f264687e29c5bb6a3b29b0ea","0x000000000000000000000000000000000018c2c4ecff68ed36990b085ae6eeb0","0x0000000000000000000000000000005206f07655d92b74b8f70b49b3c0d20b6d","0x000000000000000000000000000000000014e1b95e20c0363366a7d450ddb471","0x000000000000000000000000000000c3188cd7a04d6c8a8803f89c399e9bc958","0x00000000000000000000000000000000001480c491cfda58b06ac1de20233d25","0x00000000000000000000000000000017515b2bd236e2f9c584a7ce552dc8fcc2","0x000000000000000000000000000000000010cc2efa8f18c2b3612e01a8f94503","0x0000000000000000000000000000001f0a3f81411b1e06edf45c3394c8c296e2","0x0000000000000000000000000000000000093763194f12aae29dcb9f7b74552a","0x00000000000000000000000000000022eadbf390242527d6646603cb2ba8fb18","0x0000000000000000000000000000000000040bb072ad71de805dc3d35901d8fd","0x0000000000000000000000000000009969e938d09ae8a3e0b2e88252474433f1","0x000000000000000000000000000000000001a0c3834a281427a0bd40fbfad2c0","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f3f3adcb44841356ca07ede3ffdaa4e56f","0x00000000000000000000000000000000001ffaeed8a0fd4e93e92f31906d07db","0x000000000000000000000000000000947443af422077ce83dc603affb9d07832","0x0000000000000000000000000000000000247c8888f27a2569a7c50eafb17f6b"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json index 4f047cf19..8c94c4041 100644 --- a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x000000000000000000000000000000a173185a6c66bbbf8844688be7ac5b05ba","0x0000000000000000000000000000000000178d5026a2292c295bacc6839d146c","0x000000000000000000000000000000251fbdee5ec057add298a568ebfccdaa3c","0x00000000000000000000000000000000000fcea813296807774dac67504290f0","0x0000000000000000000000000000006c7743cf4bc8503bd574b9c7b042a1b68d","0x000000000000000000000000000000000027dee1df21902824e3fb2a07d21db4","0x0000000000000000000000000000003b8d04d07881ea4472156051067e9d33d3","0x00000000000000000000000000000000002efb7c99cbaf72320bdac23789b559","0x00000000000000000000000000000019f8887663e824b08e215a41790a2ec7a6","0x00000000000000000000000000000000001cc6ea2f5fc3b0120e75b4cbaf2861","0x000000000000000000000000000000165b607efeda4ffc649af0b694b6a32c26","0x000000000000000000000000000000000012ade532f1db76e5584869d990a2fc","0x0000000000000000000000000000009467b47a84166e9e2d04ab3aae32ab9c15","0x0000000000000000000000000000000000098a67f56e850a274e2fd7d9bb44de","0x000000000000000000000000000000f9647c52015aa4311f503490baa5e873ff","0x0000000000000000000000000000000000278197a2e23df63ac913b9b617427e","0x0000000000000000000000000000001d6c6dd79dbc9867c840f99a32d23e51d1","0x00000000000000000000000000000000001da7bb1e031b6a663f1bceceaf7bd3","0x000000000000000000000000000000c13e098ec0c7ef662dd1983657e77bdea7","0x000000000000000000000000000000000000ec8e935a8bd17321ee190d54b6f9","0x000000000000000000000000000000f1f89ce288dfec299c4ea104b2dd5b88c1","0x00000000000000000000000000000000002c543d8f601164e5d5facba816c37f","0x000000000000000000000000000000d4cfd490b180059e6b6ed708de819f4aa1","0x00000000000000000000000000000000000f12ea1c37d54e5283e3f4f4d6e79a","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000002f2a2f928122d8521c561b495e7e1857f5","0x00000000000000000000000000000000001ef0e3e9173c235ec2b1c0b91281c4","0x00000000000000000000000000000065181a2e3847dd0d30a22c9b2eb0422b4a","0x0000000000000000000000000000000000172776113a217eb5cf32ccaf0c95c8","0x0000000000000000000000000000001d411cd7d552ebaef417157ea93cc846df","0x00000000000000000000000000000000000420bb60da34cbbfa5bc9a410e5765","0x000000000000000000000000000000a397dbb24a08eff9d676f0749459e71c88","0x0000000000000000000000000000000000082a13f1f83d1a894640ccb07f9c3d","0x00000000000000000000000000000058c4e00e4c5e324009ead3ecf212577819","0x00000000000000000000000000000000000f3381ec4cc7c75ca4ee3e1d594234","0x0000000000000000000000000000008f4aeb1bd095fbcfd120926369f2264d72","0x00000000000000000000000000000000001869edafbb233d9fc8c2376ef49866","0x00000000000000000000000000000051807132e87254a64a7d37e681a280a72b","0x00000000000000000000000000000000000ca61bee2d3897a477e8b3e5370b9f","0x000000000000000000000000000000a051f66e31acd00649bfcc30cc17a57559","0x00000000000000000000000000000000000682ad983b1e1483f2e6fcf4af40a0","0x000000000000000000000000000000d766209beee813fbfbf6cb84ef8ddfa216","0x0000000000000000000000000000000000246ec1ff20b9106a42885b97157afd","0x0000000000000000000000000000003d61f572e4a04f3eb184ddfcf57e4cd1f6","0x00000000000000000000000000000000000155a8d254c90b62bd0da6bed49121","0x0000000000000000000000000000004e3a7e8ff0058f7cef545a45aca6604ead","0x0000000000000000000000000000000000181ba86db74a39162abf8850c70d80","0x000000000000000000000000000000ccfc6208c78202a6a5db64bb0dea3d05a0","0x00000000000000000000000000000000003029cc67f5222913abce8b7f6bfc33","0x000000000000000000000000000000f47a548e30eb6664e7da4fee0fe8fd5cc8","0x00000000000000000000000000000000001110a66e7db8c7d275aead5e44a2e5","0x000000000000000000000000000000e0a2f05efe775db5fa6e9356129adfd308","0x00000000000000000000000000000000002f655e46cd96550b177cab30f78557","0x000000000000000000000000000000eb7004471c4ca9e986bc42b14fa712b9bc","0x000000000000000000000000000000000012ebd5509439bc031e4fa3658c603d","0x000000000000000000000000000000fcde1a8d022d92236c7e08695be0353fc4","0x00000000000000000000000000000000002dad713f031c9987fa3f12d4541941","0x000000000000000000000000000000afc918b76e32490ec37f5b095b3f14b2bc","0x000000000000000000000000000000000023f616db2c19d173c8f0756bdab2a4","0x000000000000000000000000000000976b02396f55456aae29523e685dd9f935","0x00000000000000000000000000000000001627baa0461616fec69ea4934e656f","0x000000000000000000000000000000584805b0c65b1d33ef7763a7b0ffcc2736","0x000000000000000000000000000000000025dbe6974ca7111098ca63c21db3b7","0x00000000000000000000000000000009a61c7809ebdf49649f6d13b0f0f2fa33","0x000000000000000000000000000000000021d6a57b5260ea6c8e3740022e76a0","0x000000000000000000000000000000337d27788cbe9b4e1a895a73b9e8dcea15","0x0000000000000000000000000000000000017a55101bd2c03b1336b37df158c7","0x00000000000000000000000000000010aa17f60dfbdc5bb37fd16e798958b040","0x00000000000000000000000000000000000e862f3cfd0004a863fdc3ef7355c0","0x000000000000000000000000000000e4e18e522f28a916bbabab2b7a70f9fbc6","0x0000000000000000000000000000000000015f5961f9651dc930ce8206f964b4","0x000000000000000000000000000000a4908a5d585331c8cc1cbfa0b5dc2d1e1a","0x0000000000000000000000000000000000061168b4c58069364aafecc59ccb98","0x0000000000000000000000000000001b63cd11e8e1b438d1793ac621f0c1c26c","0x00000000000000000000000000000000000f487b28c699f3932e7c94abfdee20","0x00000000000000000000000000000010aeff6fa851db928f235dd4fbc17cfcf2","0x000000000000000000000000000000000012cf1027e8f15d9dd21b68032a9641","0x000000000000000000000000000000aa14cb32534ccfb68f4c4560f301d4c3cf","0x00000000000000000000000000000000000cee8cb1cf07d59b0be8e95d33a844","0x000000000000000000000000000000356c4714a52f5c1e03cccf3dea2d266989","0x00000000000000000000000000000000000355a55d816040412d356ec7d77a12","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000005a1ab0e0419a29211c7a50a0054e9ef2a7","0x00000000000000000000000000000000001998ccc89e4b3f4434109ca924791a","0x000000000000000000000000000000a1f76296454097f832ec1dc41c05248d8c","0x00000000000000000000000000000000002557d1b101e196a9be90c395e7ff18"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000b1e2f541998a2755e5213d3e971e91df06","0x00000000000000000000000000000000001c85aa14f04fe5cd6ba340f1e36f30","0x000000000000000000000000000000dd17a4a404747a820741028f49190e3643","0x00000000000000000000000000000000001c56196f530a939d9d97a3cc03ab0b","0x000000000000000000000000000000e138bb15582e87af9d51fbc7ed6e0e7695","0x00000000000000000000000000000000001e7f60c2349b166c96ad0d379b4ca3","0x000000000000000000000000000000b98deafa29f053f7ceb11d0d32de87dc7c","0x0000000000000000000000000000000000002ce030b942f99071c3b7b51a20c3","0x000000000000000000000000000000f88fb777e1dc9c467fbc86e05b6b8a8e8a","0x00000000000000000000000000000000000b3dcec21f9a3468ac8cff9f0dc6b2","0x0000000000000000000000000000004456c9e04f1ae5534b41cb9d4d0f05556a","0x00000000000000000000000000000000001e8b9a96be11a99546efa51b0adeed","0x0000000000000000000000000000008d0020e155991b2fdf7d87f21051e09bf1","0x00000000000000000000000000000000002faca69121bb28c07ff1f61acf5bc6","0x0000000000000000000000000000002bf8105c273f37166bf859778efe38beae","0x000000000000000000000000000000000003d52e114558e4968f6751b3244396","0x000000000000000000000000000000c8a6c7a4920d3415fab8e852d34e731f3d","0x0000000000000000000000000000000000169f38558aa5d5fbc8ccfc308ece82","0x0000000000000000000000000000007ab272b3b30d2aa5a04ab9ff8b03761300","0x0000000000000000000000000000000000048cbf034592a047b3f743cc06a9b3","0x00000000000000000000000000000092f9818005d01ae6aa95051e81a4d8fd7c","0x000000000000000000000000000000000019f5135e35bb7fd0bb0db2984503d7","0x0000000000000000000000000000001a8a976ae62ab0d4ebdf0f026a25320094","0x0000000000000000000000000000000000237782b32e0dd4f3385e0e58707578","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x00000000000000000000000000000083f8426178a4adb8759e565672d48d57b2","0x00000000000000000000000000000000000d2162b8663e51626b63b613f38d10","0x0000000000000000000000000000005abf0765b4ec2d9008d3a0b0e4507d896d","0x00000000000000000000000000000000001c9eb82857d4ed7141bbdd48792bf6","0x00000000000000000000000000000053644e331267af096b7fa639d567af46d5","0x0000000000000000000000000000000000169b62f18a62c1dd259c0e4c1e6ed7","0x000000000000000000000000000000e0c1b58069ea333eef6ab3adfdebfbff1b","0x0000000000000000000000000000000000194f5f1fb476a26932b74e9b3b0076","0x0000000000000000000000000000007f16b72a7cd37b7fad520beb587385af0c","0x00000000000000000000000000000000000b7c2b90171c94d593ce8c018f9d9f","0x0000000000000000000000000000006b97f4bcd4ffbb3f43a44c9b54da291227","0x000000000000000000000000000000000011e40fb9bb5d8e48416a30e2fbec52","0x00000000000000000000000000000045fe00b3b7a774a6f24792e2232d7cff7e","0x00000000000000000000000000000000001931e0d13d1f24ae08944ddf7477e9","0x000000000000000000000000000000a03c16e18ed20dc8b0359b35f488a561b5","0x00000000000000000000000000000000001e367c248e85a81becee8094786fa5","0x000000000000000000000000000000a9e97de75737843e2a615ddfcf184170b3","0x000000000000000000000000000000000012af86eb66bebd2a4d37c160d27a23","0x0000000000000000000000000000000be148149805efb06f7487733e2ca3f78d","0x000000000000000000000000000000000001623d48ab894fd1c64ab90c49fc39","0x000000000000000000000000000000c0e17eccf6222f6f5e80382b856c9069ca","0x0000000000000000000000000000000000049669495d53f7e25db94054b7bf66","0x000000000000000000000000000000063bf54ae8896ffcfcd5f483bce7ffbd2c","0x000000000000000000000000000000000011e1d2550ae5e4aa5f37048b93c3ac","0x00000000000000000000000000000022a5b91d96420560bfdf12ae5165751d8e","0x000000000000000000000000000000000023e6d77db8a0dec18f31aca6ade9c3","0x000000000000000000000000000000ce8ad7503f564d906acc22f7b0cb9dd19f","0x000000000000000000000000000000000017d33b927e0c32c36e85da3c2c0f31","0x0000000000000000000000000000008805aeb41843b56d14004556766019c8ee","0x00000000000000000000000000000000001c3ccfccedd90945ebed03bdf8055f","0x000000000000000000000000000000e1d30462a2f0338a3c2a60c597c8066728","0x000000000000000000000000000000000015539075e03a859b281b75c49ba7d0","0x000000000000000000000000000000229e8d446d2172bca1bde608f9b7af6940","0x00000000000000000000000000000000000f8f294047a4fe4f38bb3d32986e65","0x0000000000000000000000000000003547cd41239103ff65ed2aa52e662d1fa7","0x000000000000000000000000000000000029d698f9e85044e11bc2fa37409c12","0x0000000000000000000000000000003449790c59fc7a4349e1e55768a7066fd4","0x0000000000000000000000000000000000075204d5ba12a18730f9cca06c9d14","0x000000000000000000000000000000d74e3abd35dea7d5c3864b37381dacb1dd","0x0000000000000000000000000000000000105a75d044776abb9e403442460c86","0x000000000000000000000000000000aae1559412cb8f880d77c7267b8e5317fc","0x000000000000000000000000000000000029296657d4604dca5b5cfa3646301e","0x0000000000000000000000000000005d4b8efc3486782d15288b1bbb15565916","0x00000000000000000000000000000000000d04883311bd540e8eedc4f2c8d15a","0x000000000000000000000000000000957eb2ae0714fab81cb7bc641afd950611","0x0000000000000000000000000000000000219ba3ad43be1e3cbd9fce0ce50769","0x000000000000000000000000000000c0268a81e172ba6755637a92aa132948f6","0x00000000000000000000000000000000000cba22f3994d2bfac4ed66eccc1e0b","0x000000000000000000000000000000df174b538c33c37f6aae1f5376293eb040","0x00000000000000000000000000000000002e8e391362e10fcc3e73988410b62e","0x00000000000000000000000000000043520aea0acff78687103480b09c01a8f6","0x00000000000000000000000000000000002a69cc75b104ac7855fd7f75f184a3","0x000000000000000000000000000000267c9488653d674ef240ba580f33176340","0x000000000000000000000000000000000019e6a90b94d863a6d4c4e39e80e6c0","0x0000000000000000000000000000009c7b961d4157b3b26b9156a3932361c86e","0x0000000000000000000000000000000000275212059032c11bb72f6a50581c87","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000d678f1a0bcc54fa74011eefbef799f4a0d","0x000000000000000000000000000000000030439c58cae8bce4e184e2b8bda865","0x000000000000000000000000000000c8f80f131a32e400e35d3dad34964738b9","0x00000000000000000000000000000000000fcd9a67a0311379f8a005c79daec1"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/vks/transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/transfer.vk.json index 932c7350a..79ac1df9e 100644 --- a/packages/tokens/src/confidential/circuits/vks/transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x00000000000000000000000000000004413d24f9ef99a13491acc3aa55395d62","0x00000000000000000000000000000000000b9cbec0ba4ade4c074a49116d168f","0x0000000000000000000000000000001c647836b8994b8842561aabc00fcc67c2","0x000000000000000000000000000000000027ec474a1c645c0b33a9001aa66079","0x00000000000000000000000000000008fc94084b14bb94fcec2e8eb2aa6d6338","0x000000000000000000000000000000000000dc8867233b315171148598a1cd3f","0x000000000000000000000000000000b279cd48ac75427503ed3352c9c9a5334a","0x000000000000000000000000000000000012c9697469839cf1f3dd53c4d1309c","0x000000000000000000000000000000f9f9d31e4e345760912032b27368b64c3c","0x000000000000000000000000000000000001d2b60c1be711bbc4fe20283aec42","0x0000000000000000000000000000008bad4e8849f784170c2c0efee3bcd96f13","0x000000000000000000000000000000000017d032a52b15c7764e65077b005953","0x0000000000000000000000000000001df0203a9392db9f72c54363e298885359","0x0000000000000000000000000000000000102827f901378f6b56b63238ca4f61","0x000000000000000000000000000000f9a38ce984a2739067d3f5530069acd5b2","0x0000000000000000000000000000000000299fee9e9ac6a881667f79562984f6","0x000000000000000000000000000000b1df6fbc084945ad009a91a6406832e706","0x000000000000000000000000000000000023d8ced027973fe412a0a8a8ae562d","0x000000000000000000000000000000d1c1cd6d0c1f8aca7ba576a278ff3a2851","0x0000000000000000000000000000000000097e71727da99a2a4f8f5674525595","0x000000000000000000000000000000cb99e5e8a724031a8844c9e77e87de8d54","0x0000000000000000000000000000000000031c3791e5e36df9e8641da31e8556","0x000000000000000000000000000000cbb525c67113f08c10c149e587ad41a4b1","0x00000000000000000000000000000000000723274327f1c28920cf374bf42b2b","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x00000000000000000000000000000090be9e0f4ad68be758dc91464100d964f4","0x000000000000000000000000000000000009e8536a046d743505b08104ea028f","0x000000000000000000000000000000cb4fd5227fe827fec053677babcb2ebda7","0x00000000000000000000000000000000001f0fbf6631cee0ffcfb86e249aadf5","0x000000000000000000000000000000d3efb3a64db75d1b3d967e32bbf21cd6c9","0x000000000000000000000000000000000025c52c5e5f67d9aa920c536ea6a599","0x0000000000000000000000000000007028a22cfc51631a8537ede3e2d07d3ca1","0x000000000000000000000000000000000015b337967fc201c9744d5f6be5196d","0x00000000000000000000000000000027466a58010816fba3923c274d63f62f81","0x0000000000000000000000000000000000136e6df8fbaa4970d742e35f737054","0x0000000000000000000000000000008ec1ab7bcb285a743a72bc725e617463f0","0x00000000000000000000000000000000002c6cf860d994aed7991185d6b43238","0x000000000000000000000000000000c5eeb9738e1051a201c65dda1fb7cd1e76","0x00000000000000000000000000000000001cc137bf0f6d056c73b3e65254afac","0x000000000000000000000000000000cc30c6c27e8bf2fdb653c1d4beb23bc87e","0x00000000000000000000000000000000001372ba0a7e82cab73226f2366b26fc","0x000000000000000000000000000000b6335b2a4b071d351828c4337e7a9e8c4a","0x00000000000000000000000000000000001dc9e72c2fdb64cc3c0d25057a8d24","0x0000000000000000000000000000009a6e568feac9da1616acf6de081c1968e6","0x00000000000000000000000000000000002ca76fc9d813030d3a0fbf2c17cd72","0x000000000000000000000000000000ef40ad028444c52560905e3578c8d0ae55","0x0000000000000000000000000000000000263ade3e29bdc96854cab29124430d","0x000000000000000000000000000000a5e98ec9faa0203b948564a5931a6230f7","0x000000000000000000000000000000000020062a3319ec0a4f1c4ac674110721","0x000000000000000000000000000000c8b7dfed18f3e5898b56dfdc1865110c7b","0x00000000000000000000000000000000001527146e79e49f249a186cfe7c15eb","0x00000000000000000000000000000085ec7fa4c3be15fa392abb598d434f32ea","0x00000000000000000000000000000000001dcef54518d6b45a568d745d4465db","0x000000000000000000000000000000e82f8e8258b24217bde2dd08bd2d623acf","0x00000000000000000000000000000000001719c66f3e93abadebb5593ac9b041","0x00000000000000000000000000000017bebabc00e318a6daa37132677f86fd21","0x00000000000000000000000000000000001175d817775004346c6555f836556d","0x0000000000000000000000000000008269ab0a36bae1c6c7ef80f675500a1412","0x000000000000000000000000000000000004a143d6fa4b117034bc918cf2e543","0x00000000000000000000000000000076b98a6f23d948831c387bcc10a10a349e","0x00000000000000000000000000000000001940ab63da158a6a4dbf77a2456545","0x0000000000000000000000000000005cdf03320c85129a300ab51777cb3a73a7","0x00000000000000000000000000000000000661cdf5d6eeb598d5e33aebf877a0","0x000000000000000000000000000000e8a716b26c32928c1babd9c1b0353ad8e6","0x00000000000000000000000000000000000b51d04c0213555e9bf3f673e3e6f8","0x000000000000000000000000000000d92aa4f9cbf2a9e94daa3f73029201942c","0x000000000000000000000000000000000002b5f46fe7494b6922ea1de624156f","0x0000000000000000000000000000004fa4be1b794645a5ec1098cbc217657ad5","0x00000000000000000000000000000000000b0ab13f772163323cd412372d194c","0x0000000000000000000000000000009a3429a482019f705358fd897415e23ab0","0x000000000000000000000000000000000026f4957cf960f28e1e257190f1145c","0x000000000000000000000000000000abf5352ecb12764ee23881fc7cc946fbc7","0x000000000000000000000000000000000014d5b06fa1468445a54c04bc2f4def","0x00000000000000000000000000000044cfa260ec1024d9dffea7cfee9abd80f9","0x00000000000000000000000000000000001a288b6b1e1365604897e638698e44","0x000000000000000000000000000000abd3436c788a8c92a1eb840022a99ed2b0","0x000000000000000000000000000000000027dd023c90a794bdbb178dc963eaea","0x000000000000000000000000000000187303d4a4b721cccdcafdeb1a3a5c8aa9","0x000000000000000000000000000000000029a62d8a44d50e98d8d8afd79a2a85","0x0000000000000000000000000000002c05db33d64eff927f702bfec559a0c867","0x000000000000000000000000000000000020867e1ebec347b4648103cf4fff67","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000001de872e949b7932ce49e5508e210397dbb","0x00000000000000000000000000000000001113f4c90761cf8a67d0b888c54886","0x000000000000000000000000000000b5b4d07530ac0dc44948da70e53de01ed7","0x00000000000000000000000000000000001b70a28af537d468ef471dab8d4914"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000f5d772a8b2b7bde6a9ecce3933c8ba7506","0x000000000000000000000000000000000005f8d9bec870eabb5428428cec9dbc","0x00000000000000000000000000000092ebb216fc3afa183f8593b7510c46ca76","0x000000000000000000000000000000000007e3e3a47295dfe7d76f593e657e16","0x000000000000000000000000000000640c5d727b698896a855ee57acba0e0499","0x00000000000000000000000000000000001d8a1707073e9fb82a1078ce2975a5","0x000000000000000000000000000000cd9e145bb7dafdcb53af565e81bd724533","0x00000000000000000000000000000000002b3f18887dceacffc34eda46dc10df","0x000000000000000000000000000000562d5f29dc5281a43c645517c310ceb8ff","0x000000000000000000000000000000000025ba00f39e165de83b2c3bda9cb123","0x000000000000000000000000000000502ca1dcf2b6b28d7b4b9dd70f33db9b0c","0x00000000000000000000000000000000001878dff2dbb12392d55453324082ce","0x00000000000000000000000000000003722d0ce1932cb3c4a20ac17a68598d3b","0x000000000000000000000000000000000004353836de7a6e7166870265703c3e","0x000000000000000000000000000000ee08c3b6eb99fa5127802af028a08d199e","0x0000000000000000000000000000000000277df57716804cb66fbe60bbe6d590","0x000000000000000000000000000000a8083855303defc3959dceda342048be9d","0x0000000000000000000000000000000000042bd5e14aaf19b6243d483665cbd3","0x000000000000000000000000000000581575c27408860b85da4bf0ca3903334c","0x00000000000000000000000000000000001c21a68e72092d02e857bef160acce","0x000000000000000000000000000000561e3ea66cad00b2056f7554c84e4172e8","0x0000000000000000000000000000000000060a5a2cba3421ed00e31c508b149e","0x0000000000000000000000000000002995f46a078062cd92b4fac0477462f097","0x00000000000000000000000000000000000dc2d83172aaae19e5ef68e7707656","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x000000000000000000000000000000f500b7356c6ab562b9605690eea7b2bd8c","0x000000000000000000000000000000000009058533e5724c9a430ad8a3c98308","0x00000000000000000000000000000048c14b4e47bdf367947798d4dd1b3d27ab","0x000000000000000000000000000000000017878e4241e0c6d3949939c3ab498f","0x000000000000000000000000000000283a030e81778adc0f01680d7c4d3ed5ef","0x000000000000000000000000000000000015d65e0b38f10a425851e973b7e563","0x0000000000000000000000000000005204bf41c8022c119f289b8b7e2559012f","0x00000000000000000000000000000000000f45588264380a850b800e4fc2b277","0x000000000000000000000000000000285143090900edd247a5f97a3e0c936111","0x0000000000000000000000000000000000190b77599be7c3341e633c2207e969","0x000000000000000000000000000000fc195d824e5cb74a4f61070a1fb42c5863","0x00000000000000000000000000000000002a6cb0e5fb6a925ff072ab7440d21e","0x000000000000000000000000000000bff6259607afe2170934377bbd2c3366df","0x000000000000000000000000000000000029d0cb4dcaba401c3f7138dedd8e39","0x000000000000000000000000000000884b33df3e47f9f324df0318436ef65804","0x00000000000000000000000000000000001695ee1d81b1e4d037d42c67498058","0x000000000000000000000000000000cf881c4b8bb60b7f7b05f0e1b18323c545","0x00000000000000000000000000000000000fe9480b17503def7d999ffbe215fa","0x0000000000000000000000000000009b23b561b39075617e24eb348fcf3dea9a","0x000000000000000000000000000000000004d6f14944850a5721c2a99e25b72d","0x0000000000000000000000000000008ad328fe3d5ed1f17440303774a9ffd8dc","0x00000000000000000000000000000000000566f306cd4810e118c58c2af03ca1","0x000000000000000000000000000000e6d58d11e40fb164ae0f24ddb5cb8d2a24","0x000000000000000000000000000000000023270455c2aee0ed8bdd2eae73e4ac","0x00000000000000000000000000000027850ceb032e59656e83337140db0045c4","0x000000000000000000000000000000000009ef4debccbff4c8f4a214b5094cf1","0x0000000000000000000000000000006c008a091acee0c880ba8634d16a846222","0x00000000000000000000000000000000001d5b664647d97ecf2ed8cd42085e27","0x0000000000000000000000000000008c3ecbc123ece174301b1d674ab52261d6","0x00000000000000000000000000000000000d0bd94f74c8e8fd745e178c536511","0x000000000000000000000000000000865e886a1e7ddddc7e597a410b46db647b","0x0000000000000000000000000000000000141339f6cec9d3e576c3ccfe25f472","0x0000000000000000000000000000004eb9e73a8257da2ba22652f29eb038d1b3","0x00000000000000000000000000000000001bcead29ab67995ed99f425fd53b89","0x00000000000000000000000000000019e4f87389f1395ddbefd6b4ff88e823e4","0x0000000000000000000000000000000000015126dcbdfc878382a8d06fde6b8b","0x0000000000000000000000000000006fe8bd2b551f88ea84e4fa8a31a5c88473","0x00000000000000000000000000000000001a4b3e2a2e6ff2b5beadb8041aacaa","0x00000000000000000000000000000001044ec0f270f3c3bc2d8b929fd112b364","0x0000000000000000000000000000000000005bd1120beaa52b62d9d7431d5b2e","0x0000000000000000000000000000005ac02d8ebd8dfadb3f2f6080147450646b","0x00000000000000000000000000000000001a31184f8288965c6c91b6aa215bba","0x000000000000000000000000000000457b0e2bd6402709ef3e9d77f3e445f02e","0x000000000000000000000000000000000011b4f505ab7a15527647419e777c21","0x000000000000000000000000000000577c1eb10946b588313356f4fdd107bfd4","0x000000000000000000000000000000000029dae68696ce656493d3089b673d71","0x0000000000000000000000000000008702bcea38196f4a06d6c6743f83b25e71","0x00000000000000000000000000000000001797c635854e2c02447ff20e101ee3","0x0000000000000000000000000000009ae73d7eddaa8f5b7752166997d7663c43","0x00000000000000000000000000000000002f2f513aa81cea87bdb7ad33211f2f","0x00000000000000000000000000000073bf7f66ca3c357165d811434f0074eadd","0x00000000000000000000000000000000000a0caa960e29beb402c44963504a54","0x000000000000000000000000000000a0864b1140f9bff8333eae8a225e2178e3","0x00000000000000000000000000000000002607530a0f7804828ff3bc81effea1","0x000000000000000000000000000000f165beb0b8dcc2844bfe53fbd72496c7e2","0x00000000000000000000000000000000001b6a7890f4808fd8fb19f6c23127b8","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000004a85a0c64e04f69a36a97deb3958c6bbf0","0x0000000000000000000000000000000000199c8bc40ae10ed7f07310b7975cda","0x000000000000000000000000000000ff0c31a2001f2ff66db7afbadc36e96392","0x0000000000000000000000000000000000080e3aa2e0896e4f1a235744ee0d7c"] \ 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 84456fda7..aad7919ba 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","0x000000000000000000000000000000e597d7a43e1658fccdb57ddf48c37e5473","0x00000000000000000000000000000000001193bcf3c4251926ee33d674f84987","0x0000000000000000000000000000000baeee3ec66b08d113bd9b5f2677180418","0x0000000000000000000000000000000000169966bb708e8d372103425d107ebd","0x000000000000000000000000000000af4e0624fbc3fb17c1fc5387dc7d647685","0x0000000000000000000000000000000000173735e4349fe3ccb518a9a54dcb14","0x000000000000000000000000000000bf9dde92a2c32830bab41da0e30ad6bccf","0x0000000000000000000000000000000000086ef65b435baeef94ebc5e94611cb","0x0000000000000000000000000000005d54bbe76b91a1c6e1c341fe11218b7cdf","0x00000000000000000000000000000000001a31367e051e5b12ab9653b83d7fb5","0x000000000000000000000000000000153cc60f1784443c98cc6150b112629fb7","0x000000000000000000000000000000000018d4e69f77fafbee4385aa43522009","0x000000000000000000000000000000cf9f96c32e8dc2db412963d3cd9871e1b4","0x000000000000000000000000000000000001c58ba0e8ccd62b538ba0e7e38790","0x000000000000000000000000000000ccc339f1ae6f9d0e5b1383cda494c1a780","0x000000000000000000000000000000000020fc1c18361ef24f296fcfd6eda535","0x000000000000000000000000000000e2233ee6fb9eb58b252db928e3e89311d7","0x0000000000000000000000000000000000170098446da68cebadd10f03159739","0x00000000000000000000000000000050278a139eb755328f4ab36d3d766ac3c1","0x00000000000000000000000000000000002d40aba8e2a0b3d415a1658ce4a829","0x000000000000000000000000000000cddf8ca7068e0e861dfd8a2d58610a50e7","0x0000000000000000000000000000000000154bf9bb67fd24d2005a9a01d39ca4","0x000000000000000000000000000000fe48c66b4105d57a0dceea39003ce4b8a5","0x00000000000000000000000000000000002bfa809f8f7de18409ad040d1e1390","0x000000000000000000000000000000176df2b9f9816b99b87dcd27314f248492","0x00000000000000000000000000000000000396fae9cfc50f85ebfe6bf44327d7","0x0000000000000000000000000000000107a790c778e6a2ffbae75e5764ee8cc2","0x00000000000000000000000000000000000e0f2756936ef0c0b2175663320713","0x0000000000000000000000000000000750098d0e116f1e93f1288acc950929e4","0x000000000000000000000000000000000029eb04b0dd469b00a82249ab293ebb","0x000000000000000000000000000000ae4184b486d7e33213b5be4c1fd17e373e","0x000000000000000000000000000000000010a740ffa5e0dfeab5dbf9617a8e81","0x00000000000000000000000000000024829bafb32570dc9d762cbffbd515611b","0x000000000000000000000000000000000016ff65fed205274a44f4622f1d5139","0x0000000000000000000000000000009652c7e9c052b125d82c422401fede028a","0x00000000000000000000000000000000001af922f957a23b18cd7b8f59f868cf","0x000000000000000000000000000000905c0c34f8112b8db53aa91e142245a61f","0x00000000000000000000000000000000000c84c6bdac23e9bcbd0ea28ce4847d","0x0000000000000000000000000000000061d74669c77c502b5fe6779f65892eaf","0x00000000000000000000000000000000001693564a3fac5e69d46818e1aa0fe1","0x000000000000000000000000000000e7e7ba36f6691bfc01870d47fd49f532be","0x00000000000000000000000000000000001473f3835ff08ca24236e1339ec3ca","0x0000000000000000000000000000004b068fde313c5b4d5dc8794a669d455257","0x000000000000000000000000000000000009742e7eab970cca765392ccc507b1","0x000000000000000000000000000000ff4675f67809d637b96d8c16337feb9f42","0x00000000000000000000000000000000001990e3e48557be6488ae35f0a51a95","0x00000000000000000000000000000085fcb8baeae055d6aa0bc02f8972f996b3","0x00000000000000000000000000000000001a909e20c4bf8940de1010fdeba8c0","0x00000000000000000000000000000026493d51a261f9d81c876414c0257f16e5","0x00000000000000000000000000000000001c7f30ca9ccde5cccca36703e63e9c","0x000000000000000000000000000000187c6d9276766c4114d4ad373138ac5334","0x000000000000000000000000000000000017cfa853d6882c4db823390713961b","0x0000000000000000000000000000007ca7da7ca56e8d165cd92f693671290235","0x00000000000000000000000000000000002cc308df428e14c4fa51df0a2e4b04","0x000000000000000000000000000000dd06766d59592ee4cdfb39bd81c3e8a81b","0x000000000000000000000000000000000005e13e18e0c2f6e0ddd3e5916cc944","0x0000000000000000000000000000008e0dbdb19560c20ab64e43a8279a8b4256","0x0000000000000000000000000000000000058ab6892e78443ba96b837ade42fc","0x000000000000000000000000000000c7009ef82d611b4dc16fab927433897de5","0x000000000000000000000000000000000018ca6385544e57541b0775b28895c9","0x00000000000000000000000000000029a6c1dfd2f703b9bd2f83b7cf56b5b7ba","0x000000000000000000000000000000000017d7459cbe727aa3c4b26169d0f795","0x0000000000000000000000000000007760e92b68eace4738858a05bdf9ab0c5b","0x00000000000000000000000000000000000304ef1f846e4abd3928cfc3316f12","0x0000000000000000000000000000005a4803b4eea8ed1648680131f6013fb36c","0x0000000000000000000000000000000000233aadd2b997264caa8b7687a5b147","0x00000000000000000000000000000094ff2c242ea80bd05eb5957312d32c2c17","0x000000000000000000000000000000000027032a31ba7b4286afc91524c420f7","0x0000000000000000000000000000007106b178573e9b851ffa1e16134d407a1a","0x000000000000000000000000000000000015c9a22d78f6548deb9a54fa84dc13","0x00000000000000000000000000000022708f1f0aef815b7cf99a66698cee52f9","0x00000000000000000000000000000000000387fe2425c59e75d884f32991fd91","0x00000000000000000000000000000074ce2422329832f9618c74c72ef4740385","0x0000000000000000000000000000000000080fc591f68b66fefe0d6e3b4a7c2c","0x000000000000000000000000000000a1d75292cc2bc1f1eff3cd81f64f6f91a0","0x000000000000000000000000000000000029d52acd302678d419d9e0b1f16807","0x000000000000000000000000000000bf0f9a13faa9252ff93bf1b24c745c3a50","0x000000000000000000000000000000000000c431a1843df444d3008dc20d1fc7","0x0000000000000000000000000000009103b5d371a24e95242f720bd87d5cb6ba","0x000000000000000000000000000000000013b28f89f55e9d0c5a901245936ef9","0x000000000000000000000000000000b11178bfc6c1b9d0581942640655dce0a6","0x00000000000000000000000000000000000845a08e2071080b55c5ccbf7a7fa3","0x000000000000000000000000000000e4a16685a95b9a022eaacfc7916e3422f3","0x000000000000000000000000000000000010dd7a24c1648e2b655803281f4efd","0x000000000000000000000000000000984d80910016772cb83ed3881f90ebcc8c","0x00000000000000000000000000000000001b6540dd28b967d06897d2fe4f4732","0x000000000000000000000000000000b7445646b5160c380130b66e34450aa736","0x00000000000000000000000000000000000ef8897e2c65d0ec75dc09f85edeac","0x00000000000000000000000000000045688ef774d85c8bd41f9665bad2d794c8","0x00000000000000000000000000000000002ac07a74c19ad88489ed6f1e93dc9e","0x000000000000000000000000000000a5a32649850aae3c839626ec285717f399","0x000000000000000000000000000000000010cdf0710877e231d375a2ef4dc3d3","0x000000000000000000000000000000160d4e95b954b96077723f1800f420dc77","0x0000000000000000000000000000000000280ae74a2dae2f5e68706031174b76","0x000000000000000000000000000000440bb5fc5471f6a37dbe12fbef6a37c009","0x0000000000000000000000000000000000106864b3460bfded9d5dc448fe29e8","0x000000000000000000000000000000d29de5d4acc99c6c72fa17e0a80bf88401","0x000000000000000000000000000000000007e83dfa443e984d53e0323523f742","0x0000000000000000000000000000001cfe095fa8e5d91145e1222746c27083ad","0x000000000000000000000000000000000006a7d963d96d78bc035faf0a744f8e","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x00000000000000000000000000000048818b917ab2937e3b04b8a63242c8d685","0x00000000000000000000000000000000000f8f935e6d133cf64a5f2a95b7320c","0x000000000000000000000000000000ea7294c82d95f81a10e62710512933b773","0x000000000000000000000000000000000018b83b936d6bbdf96f12867151464e"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000020","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000010","0x0000000000000000000000000000004c442649afc96694fbd11364e67ad38665","0x000000000000000000000000000000000028053d1ff061a2df1452b4d996643e","0x0000000000000000000000000000001582b8d4502c01e5ffdd936e93c193fa97","0x00000000000000000000000000000000001ea7640ca206713afd369069bbd871","0x000000000000000000000000000000ab706e244ff19b91c383680960675e9940","0x000000000000000000000000000000000018f77e0f17928f1209d27f893bbb0e","0x0000000000000000000000000000006062946ceb70f901200642ab8816baff79","0x000000000000000000000000000000000011e6980aab93a7a998040a00d7c70f","0x000000000000000000000000000000fd0957636b6cd60c293e7d603f68a5b6e2","0x00000000000000000000000000000000001f5dbb5ce8342a7e22730401b6aa5f","0x000000000000000000000000000000173181fdcb5c2df435472b2fdb1270f8e9","0x00000000000000000000000000000000000f350011e6f7b73f33ee49baf5eb7e","0x000000000000000000000000000000383619f6dd719ad90bc863da0ab8d72adf","0x000000000000000000000000000000000020c1bc2be87cbd4a7fa36dfdd857a0","0x0000000000000000000000000000007dcb345c387a00d6aa197af9ee697d344e","0x00000000000000000000000000000000001c73fa78aeb84b556ee48f42b00291","0x000000000000000000000000000000c95c5cf30bb32e7e999104b2e787426f7c","0x00000000000000000000000000000000000f1e98707c5742c2fb2046fd405c2a","0x000000000000000000000000000000c9a2d2aff1a043878fe8846d393a094608","0x00000000000000000000000000000000002664a800fe61ea766996ed02064911","0x000000000000000000000000000000952680e1c239306ef5e6af3d4102725f6a","0x000000000000000000000000000000000011e345325c59c658bf5a607968b298","0x0000000000000000000000000000009a6d0c416152a4d75d15f5157aa6db133f","0x000000000000000000000000000000000007e4d4865aafe796f204a900e4afce","0x00000000000000000000000000000092e27aaad5daa09c02bd3d4a06261c6955","0x0000000000000000000000000000000000279d0d683708150813227323363a98","0x000000000000000000000000000000b093a03ff149ad26be672954978aa36c7f","0x000000000000000000000000000000000008ca4e0cdddab1cf17d82fac1ab338","0x0000000000000000000000000000006b138628d74be50bbf305f178f475acc62","0x0000000000000000000000000000000000045e4248bbac34003bfbba4b4db8ae","0x000000000000000000000000000000ea7ccd3292923da76a46553c0fa3e620e1","0x00000000000000000000000000000000001ccd9c4471951bf3ff804c9a303f85","0x00000000000000000000000000000032ffd82879a53a599955e658616d6ee2f2","0x00000000000000000000000000000000002c569129a60c84497390ee7174f9ee","0x00000000000000000000000000000068e4044dcf7fadf62665814a69fca364fa","0x00000000000000000000000000000000000dcfa704637eedbb8e45464a2ef845","0x000000000000000000000000000000b6983b29c27f6a8b5bb026a1e76845d121","0x0000000000000000000000000000000000037758ec0c8d66e25607a554491aab","0x000000000000000000000000000000c85f0f87a4f1fadbe7e0bcb8c75fac8ca8","0x0000000000000000000000000000000000085458e28b36e782ac13a261894f8f","0x000000000000000000000000000000cdfcb6dd05b85366435fdd3db7865ace3a","0x000000000000000000000000000000000021a9cad52c4e49041088163537ebf9","0x00000000000000000000000000000012d65cc124eb2723b65fd696cf9d80a780","0x0000000000000000000000000000000000020845f24f752bbecddbd6bde2e07e","0x000000000000000000000000000000fe833f1ef6649c54762b39d7978680a236","0x000000000000000000000000000000000012c3745781a0664266edd96dea2d82","0x000000000000000000000000000000b3c6ab497730a0d91cb60f47bb7dea8328","0x000000000000000000000000000000000027f5cd7d54e2a7e58c6e06977feb8a","0x0000000000000000000000000000001b8e81967fefbcf14ef9aa07d09b795c10","0x0000000000000000000000000000000000290fcc53bc94e99bc7b6d0c5b6ac68","0x0000000000000000000000000000009f436cb5ec371892fe986aa33b3f528bf9","0x00000000000000000000000000000000001fe0ad95c51ab6cdb54ad28dd874cc","0x000000000000000000000000000000d6c4eb2310e53daf54409c0ad88ae0675f","0x000000000000000000000000000000000009e14b24cce77616b3cc7f9a704529","0x0000000000000000000000000000007669d25c4e452701f6246301fd1f4b6247","0x00000000000000000000000000000000001e98087048b27558482e0ab925ca88","0x0000000000000000000000000000000b8e00279cd27f8e79ddd6f115786a2cb2","0x00000000000000000000000000000000000f11502980c524525557a41ae89ef2","0x0000000000000000000000000000005399536b916f427b858903a1e15cb6493b","0x00000000000000000000000000000000000e3a6547370bac47f42469c4365a22","0x0000000000000000000000000000006817ff88a828dc759d612032b2c4d7d1a5","0x0000000000000000000000000000000000151d74521b7b510ab012a788a44504","0x000000000000000000000000000000022be5646a84a6a3b907e4fca07f227357","0x00000000000000000000000000000000002922887b4d45a45a9f50e4bdd2c9ad","0x0000000000000000000000000000006ca6d0060aee07323c58d157cd592e5aa9","0x00000000000000000000000000000000002768d343b4719ee740d4716eb78762","0x000000000000000000000000000000e6e8d8ca0e237a70530528cc9c2e1f9700","0x00000000000000000000000000000000002d4697f9e03380f07cea7635865fee","0x0000000000000000000000000000000cf58ad6e38f7872eb217bd112b48fd56d","0x0000000000000000000000000000000000253f9f0d5b07a7608b2659b7912fda","0x000000000000000000000000000000e4143d1db51214335ea56a34e70ec20dbe","0x0000000000000000000000000000000000142446512f94cb2392d7c5ad87d080","0x00000000000000000000000000000076f4dcd726dda842133baf7a628ee02f28","0x000000000000000000000000000000000000984210bd7caa2e5387ac673933bc","0x00000000000000000000000000000040436cbcf89d866f03731dcc2d43a0dac6","0x000000000000000000000000000000000010dba246da2fcc8c533e6882c47976","0x000000000000000000000000000000d5ea8a0956143bd21c871bc815a1227b42","0x00000000000000000000000000000000001ec8042c2134addedbc3014cad8e19","0x0000000000000000000000000000004480f4586763d9f91600be77eeff6c387c","0x0000000000000000000000000000000000300c18297df2312b4433b3b3cd24f5","0x000000000000000000000000000000db4acb40bf25a938aae223baba211ead95","0x000000000000000000000000000000000018e953437f1138e83b2ad414da67b0","0x00000000000000000000000000000096e99865d497c4b89ff90e3df7eca2159f","0x000000000000000000000000000000000007694ca87ce7b62f35c42cd904744e","0x000000000000000000000000000000c6d3193628a55a914a1ad75168e7155748","0x000000000000000000000000000000000000f71154026816bf941623343d89d2","0x000000000000000000000000000000691a6116e567ef5f1a576558df94ebbde5","0x000000000000000000000000000000000029d3301c46141dce1cee3552479bf8","0x000000000000000000000000000000ca7ba36583fdf1ae1435c9c977dfcabb24","0x000000000000000000000000000000000018baad696f8f908e5bb1e6683a84bf","0x000000000000000000000000000000bb2016356836c7e3aca49f161906cb86b1","0x000000000000000000000000000000000008d2fb9e407703816dac893b653985","0x00000000000000000000000000000041e30592e5b686ae7c91c0c16bcafce094","0x000000000000000000000000000000000026ef15da093439d27b304bfb2644ff","0x00000000000000000000000000000005e2e27132a1ee599dc1f267dbb6491c6b","0x00000000000000000000000000000000002085801a9ce94286f972b123775fca","0x0000000000000000000000000000006614deb9657f4d2631270f067acfe3b1b4","0x000000000000000000000000000000000012c12492034628a48a6fca591203cf","0x0000000000000000000000000000009674be0b43c0206da4a14d09f00de3a32f","0x000000000000000000000000000000000016b9ededc6771a87c48af38dd80e79","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000003716fcb657ff7deb84775a442cdf9b967b","0x00000000000000000000000000000000001778125c0cf94a5433a70b1020b3f7","0x000000000000000000000000000000f1ed4cadcc714972d4558c36c6c943d87b","0x000000000000000000000000000000000023fb4e8a8043c534a75ed4ded83d7a"] \ 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 a2c479d00..5af565b88 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, 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, domain, ecdh, encrypt_balance, + scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -41,20 +41,28 @@ mod tests; // auditor ECDH. // W_a2 s_{a,s} = ecdh(r_e, K_{aud,s}) Sender-auditor ECDH // shared scalar (2.4). -// W_a3 m_b = SpongeSqueeze_2(delta_aud_s, +// W_a3 (m_b, m_r) = SpongeSqueeze_3(delta_aud_s, // s_{a,s}, -// sigma)[1] Sender-channel sponge, -// second squeeze (the -// balance slot; the -// first-squeeze amount -// slot is unused -- the +// sigma)[1], [2] Sender-channel sponge, +// lanes 1 and 2. Lane 0 +// (the amount slot) is +// unused -- the // withdrawal amount is -// public). +// public -- and skipping +// it keeps the checkpoint +// pad distinct from every +// amount pad. // 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. +// W_a5 r_tilde_aud_s = r' + m_r Sender-auditor escrow +// of the new spendable +// blinding, over W5's r' +// (emitted). This is what +// lets the auditor track +// the full Pedersen +// opening of C_spend +// forward. // // Point-validation doctrine (Section 10.8) // ---------------------------------------- @@ -65,7 +73,7 @@ mod tests; // 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 (15 fields, in design-doc canonical order) +// Public inputs (16 fields, in design-doc canonical order) // -------------------------------------------------------- // Idx Param Symbol Source / Note // --- ----- ------ ------------------------------------- @@ -89,6 +97,8 @@ mod tests; // 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. +// 15 r_tilde_aud_s r_tilde_aud_s Prover-supplied sender-auditor escrow +// of the new spendable blinding; emitted. // // Private witnesses // ----------------- @@ -118,6 +128,7 @@ fn main( r_e_x: pub Field, r_e_y: pub Field, b_tilde_aud_s: pub Field, + r_tilde_aud_s: pub Field, ) { // W8 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity. @@ -168,7 +179,13 @@ fn main( // W_a2 (shared-secret scalar) let s_a_s = 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, sigma); - assert(b_tilde_aud_s_derived == b_tilde_aud_s); + // W_a3 (sender-channel masks: lane 0 unused, then balance, then the + // secret-escrow slot) + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma); + + // W_a4 + assert(v_new + m_s[1] == b_tilde_aud_s); + + // W_a5 + assert(r_new + m_s[2] == r_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 1683537e6..c182f5881 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -1,7 +1,7 @@ use crate::main; use stellar_confidential_lib::{ - commit, derive_spend_r, ecdh, encrypt_auditor_sender_balance, encrypt_balance, H, scalar_mul, - vk_from_sk, + commit, derive_spend_r, domain, ecdh, encrypt_auditor_sender_balance, encrypt_balance, H, + scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -58,6 +58,9 @@ global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c07c; +// Lane-2 secret-escrow slot: r_tilde_aud_s = r' + m_r, over W5's r'. +global R_TILDE_AUD_S: Field = + 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; #[test] fn print_fixtures() { @@ -73,6 +76,7 @@ fn print_fixtures() { let r_e_pt = scalar_mul(R_E, H); let s_a_s = ecdh(R_E, k_aud_s); let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s, SIGMA); + let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; let cx = c_new.x; let cy = c_new.y; let kx = k_aud_s.x; @@ -87,6 +91,7 @@ fn print_fixtures() { println(f"R_E_X = {rx}"); println(f"R_E_Y = {ry}"); println(f"B_TILDE_AUD_S = {b_tilde_aud_s}"); + println(f"R_TILDE_AUD_S = {r_tilde_aud_s}"); } #[test] @@ -113,11 +118,15 @@ fn withdraw_auditor_fixtures_match_lib() { let r_e_pt = scalar_mul(R_E, H); let s_a_s = ecdh(R_E, k_aud_s); let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s, SIGMA); + let vk = vk_from_sk(SK, ADDR_F); + let r_new = derive_spend_r(vk, SIGMA); + let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; 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); + assert(r_tilde_aud_s == R_TILDE_AUD_S); } #[test] @@ -142,6 +151,7 @@ fn matches_fixture() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -157,6 +167,7 @@ fn full_withdrawal() { let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); let b_tilde_aud_s = encrypt_auditor_sender_balance(0, s_a_s, SIGMA); + let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; main( SK, V, @@ -177,6 +188,7 @@ fn full_withdrawal() { R_E_X, R_E_Y, b_tilde_aud_s, + r_tilde_aud_s, ); } @@ -191,6 +203,8 @@ fn rejects_under_funded_withdrawal() { let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); let b_tilde_aud_s_invalid = encrypt_auditor_sender_balance(V - a_too_large, s_a_s, SIGMA); + let r_tilde_aud_s_invalid = + r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; main( SK, V, @@ -211,6 +225,7 @@ fn rejects_under_funded_withdrawal() { R_E_X, R_E_Y, b_tilde_aud_s_invalid, + r_tilde_aud_s_invalid, ); } @@ -243,6 +258,7 @@ fn rejects_v_out_of_range() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -270,6 +286,7 @@ fn rejects_a_out_of_range() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -295,6 +312,7 @@ fn rejects_wrong_sk() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -320,6 +338,7 @@ fn rejects_wrong_balance_opening() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -345,6 +364,7 @@ fn rejects_wrong_addr_f() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -370,6 +390,7 @@ fn rejects_tampered_b_tilde() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -395,6 +416,7 @@ fn rejects_tampered_c_spend_new() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -422,6 +444,7 @@ fn rejects_r_e_zero() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -449,6 +472,7 @@ fn rejects_wrong_r_e() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -476,6 +500,7 @@ fn rejects_off_curve_k_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -503,6 +528,7 @@ fn rejects_identity_k_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -532,6 +558,7 @@ fn rejects_tampered_b_tilde_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S + 1, + R_TILDE_AUD_S, ); } @@ -560,5 +587,6 @@ fn rejects_wrong_k_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } diff --git a/packages/tokens/src/confidential/compliance/test.rs b/packages/tokens/src/confidential/compliance/test.rs index 2056b245b..c9bb4bcd4 100644 --- a/packages/tokens/src/confidential/compliance/test.rs +++ b/packages/tokens/src/confidential/compliance/test.rs @@ -191,6 +191,7 @@ fn withdraw_payload(e: &Env) -> WithdrawPayload { r_e_point: pt(e), sigma: fr(e), b_tilde_aud_s: fr(e), + r_tilde_aud_s: fr(e), } } @@ -206,6 +207,7 @@ fn transfer_payload(e: &Env) -> TransferPayload { r_tilde_aud_r: fr(e), v_tilde_aud_s: fr(e), b_tilde_aud_s: fr(e), + r_tilde_aud_s: fr(e), } } @@ -221,6 +223,7 @@ fn spender_transfer_payload(e: &Env) -> SpenderTransferPayload { r_tilde_aud_r: fr(e), v_tilde_aud_s: fr(e), a_tilde_aud_s: fr(e), + dvk_cipher_aud: fr(e), } } @@ -236,6 +239,8 @@ fn set_spender_payload(e: &Env) -> SetSpenderPayload { sigma_a: fr(e), v_tilde_aud_s: fr(e), b_tilde_aud_s: fr(e), + r_tilde_aud_s: fr(e), + dvk_cipher_aud: fr(e), } } diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index 2bbbd5dcb..4b206529b 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -77,7 +77,7 @@ $$\text{base}(\mathbb{G}) = \mathbb{F}\_r^{\text{BN254}}, \qquad \text{scalar}(\ 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$$. Every secret scalar in the core protocol that is drawn rather than derived ($$\sigma$$, $$\sigma\_a$$) is produced by the **rejection sampling** procedure, which yields a uniform draw from $$\mathbb{F}\_r$$; the extension layers draw two further scalars the same way, $$r\_{\text{disc}}$$ ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4) and the auditor-sampled $$r\_e^{\text{new}}$$ ([COMPLIANCE.md](./COMPLIANCE.md) §5.3). +**Scalar sampling.** Grumpkin scalars live in $$\mathbb{F}\_q$$, which is slightly larger than $$\mathbb{F}\_r$$. Every secret scalar in the core protocol that is drawn rather than derived ($$\sigma$$, $$\sigma\_a$$) is produced by the **rejection sampling** procedure, which yields a uniform draw from $$\mathbb{F}\_r$$; the extension layers draw one further scalar the same way, $$r\_{\text{disc}}$$ ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4). 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})$$. @@ -143,15 +143,15 @@ The domain tag is always the first element absorbed, so $$\text{Poseidon2}(\delt - Symmetric encryption: $$\tilde{v} = v + \text{Poseidon2}(\delta\_{\text{transfer\\\_amount}}, s, \sigma)$$ - Domain separation: each invocation includes a leading constant $$\delta$$ to prevent cross-context collisions -**Two-mask mode for auditor channels.** The per-transfer auditor ciphertexts (Section 8) need two masks from one absorb. Since $$(\delta\_{\text{channel}}, s, \sigma)$$ is exactly one rate-3 block, the two masks are taken as **two lanes of a single permutation output**, not as two sequential squeezes: +**Multi-lane mode for auditor channels.** The per-transfer auditor ciphertexts (Section 8) need several masks from one absorb. Since $$(\delta\_{\text{channel}}, s, \sigma)$$ is exactly one rate-3 block, the masks are taken as **lanes of a single permutation output**, not as sequential squeezes: -$$\text{SpongeSqueeze}\_2(\delta\_{\text{channel}}, s, \sigma) = \bigl(\text{state}[0], \\, \text{state}[1]\bigr), \qquad \text{state} = \text{permute}\bigl([\delta\_{\text{channel}}, \\, s, \\, \sigma, \\, 3 \cdot 2^{64}]\bigr)$$ +$$\text{SpongeSqueeze}\_n(\delta\_{\text{channel}}, s, \sigma) = \bigl(\text{state}[0], \\, \ldots, \\, \text{state}[n-1]\bigr), \qquad \text{state} = \text{permute}\bigl([\delta\_{\text{channel}}, \\, s, \\, \sigma, \\, 3 \cdot 2^{64}]\bigr)$$ -where $$s$$ is the ECDH shared scalar of Section 2.4. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$. No other arity is instantiated; $$n = 2$$ everywhere this notation appears. +where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is the number of rate lanes read; the capacity lane $$\text{state}[3]$$ is never squeezed. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, squeezed three-wide, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$, squeezed two-wide. No other arity is instantiated. Because the absorb is one block, $$\text{SpongeSqueeze}\_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}\_2(\delta, s, \sigma)$$: widening a channel adds a lane without changing the value of any existing one. -Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Single-ciphertext channels (the Withdraw balance checkpoint, W\_a3) take **lane 1** and leave the amount lane unused, so a checkpoint pad can never coincide with an amount pad. +Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **secret-escrow slot**: it carries the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6) and the delegation viewing key $$dvk\_i$$ on spender transfers (O\_a9). Carrying two kinds of plaintext under one lane is not pad reuse — the pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. -**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_2(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one of the two modes, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two-mask tags; every other tag in Section 13 is used only with the single-output form above. +**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ is the sole three-lane tag and $$\delta\_{\text{aud\\\_r}}$$ the sole two-lane tag; every other tag in Section 13 is used only with the single-output form above. The sponge masks are deterministic in $$(s, \sigma)$$, where $$s$$ is the ECDH shared scalar of Section 2.4, so reusing the pair across two operations reuses every pad slot they share, and a slot whose plaintext is known in one operation (e.g. a transfer amount known to its recipient) decrypts the other operation's ciphertext in that slot. The canonical slot assignment above limits the blast radius of such reuse to same-slot pairs, but does not eliminate it; provers and wallets MUST use a fresh $$(r\_e, \sigma)$$ for every proof. Because $$r\_e$$ is derived from the originator's viewing key and the salt rather than drawn independently (§5.3), a fresh salt is the only thing that makes the pair fresh: the salt carries the entire requirement, and Section 9.6's retry rule is what discharges it. @@ -290,7 +290,7 @@ The spending public key is stored on-chain at registration. Knowledge of $$sk$$ $$vk = \text{Poseidon}(\delta\_{\text{vk}}, sk, \text{addr\\\_f})$$ -A scalar in $$\mathbb{F}\_r$$, unique per $$(sk, \text{addr\\\_f})$$ pair. Enables balance decryption without spending authority, and — through the ephemeral-scalar derivation of §5.3 — reconstruction of the Pedersen openings of transfers the account originated; §9.4 states the full capability of a compromised $$vk$$. Cannot recover $$sk$$ (Poseidon preimage resistance). Because $$\text{addr\\\_f}$$ is bound into the derivation, proofs that constrain $$vk$$ (R2, W2, T2, S2, V2) are inherently bound to the contract, eliminating the need for explicit per-circuit context binding. +A scalar in $$\mathbb{F}\_r$$, unique per $$(sk, \text{addr\\\_f})$$ pair. Enables balance decryption without spending authority, and — through the ephemeral-scalar derivation of §5.3 — reconstruction of the Pedersen openings of transfers the account originated; §9.4 states the full capability of a compromised $$vk$$. Cannot recover $$sk$$ (Poseidon preimage resistance). Because $$\text{addr\\\_f}$$ is bound into the derivation, proofs that constrain $$vk$$ (R2, W2, T2, S2) are inherently bound to the contract, eliminating the need for explicit per-circuit context binding. ### 4.3 Public Viewing Key @@ -351,7 +351,7 @@ $$W\_{\text{receive}} = (v\_r, r\_r) \quad \text{such that} \quad C\_{\text{rece **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: +**Update rules.** Each balance-modifying event updates the accumulators as follows: | Event | Accumulator update | |:---|:---| @@ -600,10 +600,11 @@ The owner withdraws a public amount $$a$$ (typed `i128`) from their spendable ba | 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} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (sender-auditor ECDH shared scalar, §2.4) | -| W\_a3 | $$(\cdot, m\_b) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel sponge; $$m\_b$$ is the second squeeze — the balance slot, matching T\_a6/S\_a3/V\_a3. The first-squeeze amount slot is unused: the withdrawal amount is public, and skipping the slot keeps the checkpoint pad distinct from every amount pad even under $$(r\_e, \sigma)$$ reuse, Section 2.5) | +| W\_a3 | $$(\cdot, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel sponge; $$m\_b$$ is lane 1 — the balance slot, matching T\_a6/S\_a3 — and $$m\_r$$ is lane 2, the secret-escrow slot. Lane 0, the amount slot, is unused: the withdrawal amount is public, and skipping the slot keeps the checkpoint pad distinct from every amount pad even under $$(r\_e, \sigma)$$ reuse, Section 2.5) | | W\_a4 | $$\tilde{b}\_{\text{aud,s}} = (v - a) + m\_b$$ (sender-auditor encrypted balance checkpoint) | +| W\_a5 | $$\tilde{r}\_{\text{aud,s}} = r' + m\_r$$ (sender-auditor escrow of the new spendable blinding, over W5's $$r'$$; with W\_a4 this hands the sender's auditor the opening of $$C\_{\text{spend}}'$$, Section 8.1) | -**Public inputs (15 fields):** +**Public inputs (16 fields):** | Input | Notes | |:---|:---| @@ -612,13 +613,13 @@ The owner withdraws a public amount $$a$$ (typed `i128`) from their spendable ba | $$\text{addr\\\_f}$$ | 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_commitment`, the rest emitted in event | +| $$C\_{\text{spend}}'$$, $$\sigma$$, $$\tilde{b}$$, $$R\_e$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | Prover-supplied, in this order; $$C\_{\text{spend}}'$$ written to `from.spendable_commitment`, 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_commitment` $$= C\_{\text{spend}}'$$, and calls `token.transfer(self, to, a)`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{b}\_{\text{aud,s}})$$. +**Post-verification:** The contract verifies the proof, sets `from`.`spendable_commitment` $$= C\_{\text{spend}}'$$, and calls `token.transfer(self, to, a)`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$. ### 7.6 Confidential Transfer @@ -648,11 +649,12 @@ The sender (account $$A$$, spending key $$sk\_A$$) transfers a hidden amount $$v | T\_a3 | $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}$$ (recipient-auditor encrypted transfer amount) | | T\_a4 | $$\tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + 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} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (sender-auditor ECDH shared scalar, reuses ephemeral scalar) | -| T\_a6 | $$(m\_{v,s}, m\_{b,s}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel masks) | +| T\_a6 | $$(m\_{v,s}, m\_{b,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel masks) | | T\_a7 | $$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}$$ (sender-auditor encrypted transfer amount) | | T\_a8 | $$\tilde{b}\_{\text{aud,s}} = (v\_A - v\_{\text{transfer}}) + m\_{b,s}$$ (sender-auditor encrypted balance checkpoint) | +| T\_a9 | $$\tilde{r}\_{\text{aud,s}} = r\_A' + m\_{r,s}$$ (sender-auditor escrow of the new spendable blinding, over T10's $$r\_A'$$; with T\_a8 this hands the sender's auditor the opening of $$C\_{\text{spend}}'$$, Section 8.1) | -**Public inputs (24 fields, counting each Grumpkin point as two $$\mathbb{F}\_r$$ coordinates):** +**Public inputs (25 fields, counting each Grumpkin point as two $$\mathbb{F}\_r$$ coordinates):** | Input | Notes | |:---|:---| @@ -662,14 +664,14 @@ The sender (account $$A$$, spending key $$sk\_A$$) transfers a hidden amount $$v | $$\text{addr\\\_f}$$ | 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{transfer}}$$, $$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_commitment`, $$C\_{\text{transfer}}$$ added to recipient's `receiving_commitment`, the rest emitted in event | +| $$C\_{\text{spend}}'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | Prover-supplied, in this order; $$C\_{\text{spend}}'$$ written to sender's `spendable_commitment`, $$C\_{\text{transfer}}$$ added to recipient's `receiving_commitment`, the rest emitted in event | **Private witnesses:** $$sk\_A$$, $$vk\_A$$, $$v\_A$$, $$r\_A$$, $$v\_{\text{transfer}}$$, $$r\_e$$. **Post-verification:** The contract verifies the proof, then: - Sets $$A$$`.spendable_commitment` $$= C\_{\text{spend}}'$$ - Adds to recipient: $$B$$`.receiving_commitment` $$\mathrel{+}= C\_{\text{transfer}}$$ -- 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}})$$ +- 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}}, \tilde{r}\_{\text{aud,s}})$$ **Recipient processing.** Upon observing the event, the recipient computes $$s = \text{ECDH}(vk, R\_e)$$, derives amount and blinding. The decryption flow is independent of whether the sender was the owner or a spender. @@ -694,13 +696,15 @@ The owner locks funds from their spendable balance into a per-spender escrow. Th | 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) | +| S14 | $$\text{dvk\\\_cipher\\\_aud} = \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i) + dvk\_i$$ (auditor-side escrow of the delegation viewing key over the S\_a2 shared scalar, Section 7.11; one Poseidon, no new scalar multiplication) | | S\_a1 | $$R\_e = r\_e \cdot H$$ (ephemeral key for auditor ECDH) | | S\_a2 | $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (owner-auditor ECDH shared scalar, §2.4) | -| S\_a3 | $$(m\_v, m\_b) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (owner-auditor channel masks) | +| S\_a3 | $$(m\_v, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \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) | +| S\_a6 | $$\tilde{r}\_{\text{aud,s}} = r' + m\_r$$ (owner-auditor escrow of the new spendable blinding, over S9's $$r'$$; with S\_a5 this hands the owner's auditor the opening of $$C\_{\text{spend}}'$$, Section 8.1) | -**Public inputs (24 fields):** +**Public inputs (26 fields):** | Input | Notes | |:---|:---| @@ -710,11 +714,11 @@ The owner locks funds from their spendable balance into a per-spender escrow. Th | $$\text{op}\_i$$ | $$\text{address\\\_to\\\_field}$$(`spender` argument), computed per-call by the contract (§2.7) | | $$\text{addr\\\_f}$$ | 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_commitment`, the delegation fields written to storage, the rest emitted in event | +| $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | Prover-supplied, in this order; $$C\_{\text{spend}}'$$ written to owner's `spendable_commitment`, 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_commitment` $$= C\_{\text{spend}}'$$ and stores the `SpenderDelegation`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}})$$. +**Post-verification:** The contract verifies the proof, sets `spendable_commitment` $$= C\_{\text{spend}}'$$ and stores the `SpenderDelegation`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}}, \text{dvk\\\_cipher\\\_aud})$$. ### 7.8 Spender Transfer @@ -742,11 +746,12 @@ The spender transfers from the owner's escrowed allowance to a recipient. | O\_a3 | $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}$$ (recipient-auditor encrypted transfer amount) | | O\_a4 | $$\tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + 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} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (owner-auditor ECDH shared scalar, reuses ephemeral scalar) | -| O\_a6 | $$(m\_{v,s}, m\_{a,s}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ (owner-auditor channel masks) | +| O\_a6 | $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ (owner-auditor channel masks) | | O\_a7 | $$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}$$ (owner-auditor encrypted transfer amount) | | O\_a8 | $$\tilde{a}\_{\text{aud,s}} = (v\_a - v\_{\text{transfer}}) + m\_{a,s}$$ (owner-auditor encrypted post-transfer allowance) | +| O\_a9 | $$\text{dvk\\\_cipher\\\_aud} = dvk\_i + m\_{r,s}$$ (owner-auditor re-escrow of the delegation viewing key, Section 7.11; $$dvk\_i$$ is already a witness by O2, so this is one field addition) | -**Public inputs (24 fields):** +**Public inputs (25 fields):** | Input | Notes | |:---|:---| @@ -755,11 +760,11 @@ The spender transfers from the owner's escrowed allowance to a recipient. | $$\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 spender's. The visibility model points balance- and allowance-checkpoint ciphertexts at the funds' owner. | -| $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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{transfer}}$$ added to recipient's `receiving_commitment`, the rest emitted in event | +| $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | Prover-supplied, in this order; allowance fields written to delegation storage, $$C\_{\text{transfer}}$$ added to recipient's `receiving_commitment`, 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{transfer}}$$, $$r\_e$$. -**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores `new_allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}})$$. +**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores `new_allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}}, \text{dvk\\\_cipher\\\_aud})$$. **Ephemeral scalar.** The spender derives $$r\_e = \text{Poseidon}(\delta\_{\text{eph}}, vk\_{\text{op}}, \sigma\_a)$$ (§5.3) from its *own* viewing key rather than the owner's, so that the spender can later disclose it ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7). The circuit does not constrain the derivation; it does not constrain $$vk\_{\text{op}}$$ at all, per *Contract binding* below. One consequence follows for the owner: since the owner does not hold $$vk\_{\text{op}}$$, the owner cannot recompute $$r\_e$$ for a spender transfer and cannot disclose it without the spender's cooperation ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7, *Coverage asymmetry*). diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index afaaeb1a1..49940fe9a 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -15,7 +15,7 @@ for §8-§13. ### 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. +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$$; the recipient channel squeezes two masks, the sender channel three (Section 2.5 *Lane assignment*). **Recipient's auditor** ($$K\_{\text{aud,r}}$$, from the recipient's `auditor_id`) receives the transfer amount and the per-transfer Pedersen randomness: @@ -230,7 +230,7 @@ The dominant cost in Noir circuits is elliptic curve scalar multiplication. With | `SetSpender` | 7 | $$Y$$ (S1), $$C\_{\text{spend}}$$ opening (S3), $$C\_a$$ (S7), $$C\_{\text{spend}}'$$ (S10), $$R\_e$$ (S\_a1), $$dvk\_i$$ escrow ECDH (S12, §7.11), owner-auditor ECDH (S\_a2) | | `RevokeSpender` | 6 | $$Y$$ (V1), $$C\_a$$ opening (V4), $$C\_{\text{spend}}$$ opening (V5), $$C\_{\text{spend}}'$$ (V7), $$R\_e$$ (V\_a1), owner-auditor ECDH (V\_a2) | -`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 94, `RevokeSpender` 123, `SetSpender` 131, `Transfer` 133, `SpenderTransfer` 135. +`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The auditor-side escrow of $$dvk\_i$$ (S14) reuses the S\_a2 shared scalar and adds a Poseidon evaluation, not a call. The lane-2 escrows (W\_a5, T\_a9, S\_a6, O\_a9) read a third lane of a permutation each circuit already computes and cost one field addition apiece. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 95, `RevokeSpender` 123, `Transfer` 134, `SetSpender` 135, `SpenderTransfer` 136. 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). @@ -260,7 +260,8 @@ global H: EmbeddedCurvePoint = EmbeddedCurvePoint { /// 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) +/// blindings, and (for the spend-side input opening of C_spend in W3/T3/S3 and +/// of both balance commitments in CB1/CB2) /// 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*. @@ -512,9 +513,10 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the | $$\delta\_{\text{eph}}$$ | 14 | Deterministic ephemeral-scalar derivation (§5.3) | | $$\delta\_{\text{disc\\\_bind}}$$ | 15 | Disclosure ciphertext to the disclosure recipient, aggregate variant ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §10) | | $$\delta\_{\text{disc}}$$ | 16 | Disclosure ciphertext to the disclosure recipient ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4) | +| $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Delegation key escrow to the owner's auditor (S14, §7.11, §8.5) | -This table assigns all sixteen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract. All sixteen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. +This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16; it takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ across two channels so that every row of §5.3's *Why reusing $$r\_e$$ is safe* keeps a distinct tag. -**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the two-mask form of §2.5 shares its first lane with the single-output form on the same inputs. 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. +**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag squeezed three-wide, and it is squeezed three-wide everywhere it appears. 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. **Cross-protocol collision.** Future protocols that share Grumpkin / BN254 / Poseidon2 with this protocol -- 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/v1:X"}))$$, but this is a deployment-time choice that must be applied uniformly and disclosed in the deployment's circuit-binding documentation. diff --git a/packages/tokens/src/confidential/docs/INDEXER.md b/packages/tokens/src/confidential/docs/INDEXER.md index ab0d7222d..ac17d05ef 100644 --- a/packages/tokens/src/confidential/docs/INDEXER.md +++ b/packages/tokens/src/confidential/docs/INDEXER.md @@ -114,7 +114,7 @@ The indexer is trusted for **availability and completeness only** — never for - **Confidentiality.** Everything the indexer holds is public chain data: commitments, masked ciphertexts, and ECDH ephemerals. A curious indexer learns nothing beyond what any chain observer sees (DESIGN_cont §9). - **Integrity fails closed.** Recovery ends with the wallet checking its reconstructed openings against the **on-chain** commitments (`C_spend =? v·G + r·H`, DESIGN §5.2 step 7). A tampered or incomplete history cannot produce a wrong balance that verifies; it produces a detectable mismatch. -- **Withholding is the residual risk.** A malicious or broken indexer can deny recovery (a liveness failure, not a soundness one). Two structural mitigations: for the recent window the RPC is an independent source of the same events (the hybrid split of §1), so archive withholding bites only the pre-window history; and for that older history wallets SHOULD support multiple independent archive endpoints, with deployments running or contracting at least two. +- **Withholding is the residual risk.** A malicious or broken indexer can deny recovery (a liveness failure, not a soundness one). Two structural mitigations: for the recent window the RPC is an independent source of the same events (the hybrid split of §1), so archive withholding bites only the pre-window history; and for that older history wallets SHOULD support multiple independent archive endpoints, with deployments running or contracting at least two. The auditor's forward-tracked openings (DESIGN_cont §8.1) are a second recovery source in principle, but no specification defines an auditor-to-wallet channel, so they do not relax this recommendation. ## 8. Conformance and Versioning diff --git a/packages/tokens/src/confidential/docs/OVERVIEW.md b/packages/tokens/src/confidential/docs/OVERVIEW.md index 1bbe76f89..d9840bb24 100644 --- a/packages/tokens/src/confidential/docs/OVERVIEW.md +++ b/packages/tokens/src/confidential/docs/OVERVIEW.md @@ -91,8 +91,8 @@ Merge is the gate between received funds and spendable funds. | Step | Who | What happens | |:-----|:----|:-------------| | 1 | Account holder | Specifies the withdrawal amount in the wallet. This amount will be publicly visible on-chain once the transaction executes. | -| 2 | Wallet | Generates a zero-knowledge proof demonstrating balance sufficiency, correct construction of the new spendable balance commitment with deterministic randomness, and a sender-auditor encrypted balance checkpoint produced via ephemeral ECDH with the sender's auditor key. | -| 3 | Contract | Verifies the proof, replaces the spendable balance commitment, and transfers the corresponding amount of regular tokens from the contract back to the account holder. Emits an event carrying the ephemeral public key, the salt, the encrypted balance scalar (for owner recovery), and the sender-auditor balance ciphertext. | +| 2 | Wallet | Generates a zero-knowledge proof demonstrating balance sufficiency, correct construction of the new spendable balance commitment with deterministic randomness, and a sender-auditor encrypted balance checkpoint — the new balance and its blinding factor — produced via ephemeral ECDH with the sender's auditor key. | +| 3 | Contract | Verifies the proof, replaces the spendable balance commitment, and transfers the corresponding amount of regular tokens from the contract back to the account holder. Emits an event carrying the ephemeral public key, the salt, the encrypted balance scalar (for owner recovery), and the sender-auditor ciphertexts. | --- diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index 33cc1d81e..b0b5aa90c 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -93,15 +93,15 @@ A value is a **canonical** $$\mathbb{F}_r$$ representative iff it is a 32-byte b ### 4.3 Poseidon2 sponge -The sponge construction, its width and rate, the IV placement, the padding rule, and the two-lane form of $$\text{SpongeSqueeze}_2$$ are specified normatively in DESIGN.md §2.5. What follows is what that construction additionally requires of a client. +The sponge construction, its width and rate, the IV placement, the padding rule, and the two- and three-lane forms $$\text{SpongeSqueeze}_2$$ and $$\text{SpongeSqueeze}_3$$ are specified normatively in DESIGN.md §2.5. What follows is what that construction additionally requires of a client. -**Two self-checks are available before any proof is generated.** The absorbed length in $$\text{SpongeSqueeze}_2$$ is always 3, so its IV is fixed at $$3 \cdot 2^{64}$$; and its first lane is identical to $$\text{poseidon\\\_with\\\_domain}(\delta, [s, \sigma])$$ on the same inputs. An implementation that reproduces both has the block layout and the IV lane right. +**Three self-checks are available before any proof is generated.** The absorbed length in both squeeze forms is always 3, so the IV is fixed at $$3 \cdot 2^{64}$$; the first lane is identical to $$\text{poseidon\\\_with\\\_domain}(\delta, [s, \sigma])$$ on the same inputs; and $$\text{SpongeSqueeze}_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}_2(\delta, s, \sigma)$$, since the absorb fits one rate-3 block and both forms read the same permutation. An implementation that reproduces all three has the block layout and the IV lane right. The third is pinned by `circuits/lib/testdata/sponge_squeeze_3.json`. **The domain-tagged funnel.** Every Poseidon2 invocation in the protocol routes through one entry point that places the domain tag as the **first absorbed element**: $$\text{poseidon\\\_with\\\_domain}(\delta, [x_1, \ldots, x_n]) = \text{sponge}([\delta, x_1, \ldots, x_n])$$ -Squeeze-slot assignment is canonical and MUST be followed: lane 0 is always an amount mask, lane 1 is always a balance, allowance, or per-transfer-randomness mask. Single-ciphertext channels — the `Withdraw` balance checkpoint (DESIGN.md W_a3/W_a4) — take lane **1** and leave lane 0 unused, so a checkpoint pad can never coincide with an amount pad. +Squeeze-slot assignment is canonical and MUST be followed: lane 0 is always an amount mask, lane 1 is always a balance, allowance, or per-transfer-randomness mask, and lane 2 is always the sender-auditor secret-escrow slot — the new spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`, the delegation viewing key $$dvk_i$$ on `SpenderTransfer` (DESIGN.md §2.5). Only the sender-auditor channel ($$\delta_{\text{aud\\\_s}}$$) is squeezed three-wide; the recipient channel ($$\delta_{\text{aud\\\_r}}$$) stays at two lanes. `Withdraw`, whose amount is public, takes lanes **1** and **2** and leaves lane 0 unused (DESIGN.md W_a3–W_a5), so a checkpoint pad can never coincide with an amount pad. ### 4.4 Generators and commitments @@ -157,10 +157,11 @@ Secret scalars — $$\sigma$$, $$\sigma_a$$ — MUST be produced by the rejectio | $$\delta_{\text{eph}}$$ | 14 | No — derived off-circuit (DESIGN.md §5.3) | | $$\delta_{\text{disc\\\_bind}}$$ | 15 | No — off-chain disclosure only | | $$\delta_{\text{disc}}$$ | 16 | No — off-chain disclosure only | +| $$\delta_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Yes | -DESIGN_cont.md §13 assigns all sixteen values and is their only source; the right-hand column is this document's addition. $$\delta_{\text{disc\\\_bind}}$$ and $$\delta_{\text{disc}}$$ belong to the off-chain disclosure layer (SELECTIVE_DISCLOSURE.md §2.2). Tag 1 is absorbed by the contract rather than by a circuit — the contract derives $$\text{addr\\\_f}$$ and $$\text{op}_i$$ on-chain and the circuits receive them as opaque public inputs (DESIGN.md §2.7 *Usage sites*) — so it is part of the on-chain wire contract all the same. None of 14–16 is absorbed either in a circuit or on-chain, so none is part of the on-chain wire contract, but all three are part of the cross-client contract because two wallets serving the same account must agree on them (§6.3). +DESIGN_cont.md §13 assigns all seventeen values and is their only source; the right-hand column is this document's addition. $$\delta_{\text{disc\\\_bind}}$$ and $$\delta_{\text{disc}}$$ belong to the off-chain disclosure layer (SELECTIVE_DISCLOSURE.md §2.2). Tag 1 is absorbed by the contract rather than by a circuit — the contract derives $$\text{addr\\\_f}$$ and $$\text{op}_i$$ on-chain and the circuits receive them as opaque public inputs (DESIGN.md §2.7 *Usage sites*) — so it is part of the on-chain wire contract all the same. None of 14–16 is absorbed either in a circuit or on-chain, so none is part of the on-chain wire contract, but all three are part of the cross-client contract because two wallets serving the same account must agree on them (§6.3). -All sixteen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the two-mask tags; the remaining fourteen, including 1 and 14–16, are single-output tags. +All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tag 11 is the sole three-mask tag and tag 12 the sole two-mask tag; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows $$dvk_i$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_dvk_auditor.json`. ### 4.9 Address compression @@ -513,7 +514,7 @@ RPC and archive compose: the RPC serves the recent tail, the archive everything ## 13. Security Requirements -**Secret handling.** The root, $$sk$$, $$vk$$, $$dvk_i$$, every derived $$r_e$$, and every cached opening are secrets. Implementations MUST keep them within the trust boundary (§2.1), SHOULD zeroize buffers holding them once no longer needed, and MUST NOT transmit them to any remote service except under §8.3's explicit opt-in. +**Secret handling.** The root, $$sk$$, $$vk$$, $$dvk_i$$, every derived $$r_e$$, every cached opening, and — on the auditor side — $$k$$ and the accumulated openings of §11 are secrets. Implementations MUST keep them within the trust boundary (§2.1), SHOULD zeroize buffers holding them once no longer needed, and MUST NOT transmit them to any remote service except under §8.3's explicit opt-in. $$vk$$ MUST NOT be presented as a safely-shareable read-only credential. It exposes every historical balance checkpoint, every incoming amount, every delegation allowance, and — through the ephemeral-scalar derivation of DESIGN.md §5.3 — the opening of every transfer the account originated. Its only guarantee is that it cannot authorize spending. A party that needs outbound visibility is served with D-sender proofs, which are bound to that party and to a nonce (SELECTIVE_DISCLOSURE.md §13.2), never by handing over the key. diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 4ff44587d..05fb9b80b 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -74,7 +74,7 @@ This document reuses the notation, key hierarchy, and commitment scheme from DES - $$sk\_A$$, $$vk\_A$$, $$\text{PVK}\_A$$: an account's spending key, viewing key, and public viewing key (DESIGN.md §4). - $$\text{addr\\\_f}$$: the contract's compressed address Field $$\text{address\\\_to\\\_field}(\text{contract})$$, bound into $$vk$$ derivation (DESIGN.md §2.7, §4.2). Stored once at construction in the contract's instance storage (DESIGN.md §3.5). - $$K\_{\text{aud,s}}$$, $$K\_{\text{aud,r}}$$, $$aud\_{sk}$$: the sender-side and recipient-side auditor Grumpkin public keys, and an auditor's secret key (DESIGN_cont.md §8.1, §8.3). Each account selects an `auditor_id` at registration; the same `auditor_id` may resolve to either role depending on the transfer's direction. -- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. +- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. - $$H$$: the Grumpkin Pedersen generator used uniformly for key derivation and ECDH (DESIGN.md §2.3, §2.4). ### 2.1 Disclosure Recipient @@ -97,7 +97,7 @@ The disclosure layer inherits the protocol's threat model (DESIGN.md §3.2) and **Holder is the prover for D-recipient and D-sender variants.** The holder is trusted only to produce *correct* proofs about events they choose to disclose. The holder is *not* trusted to be complete: they may withhold events. Recipients that require completeness must obtain it from the auditor (DESIGN_cont.md §8) or from out-of-band evidence. -**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. +**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen openings of each scoped account's $$C\_{\text{spend}}$$ and of every $$C\_a$$ (DESIGN_cont.md §8.1, §8.2, §8.5); the D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. **Disclosure recipient is honest-but-curious.** The recipient correctly verifies proofs and decrypts ciphertexts addressed to their key. The recipient may attempt to replay or rebroadcast proofs; nonce binding prevents reuse against other parties. @@ -173,8 +173,8 @@ The bundle does **not** include the event's payload, the disclosing account's ad Given a bundle for $$(P\_R, \nu)$$ that this verifier previously issued, the recipient MUST perform every step below in order. Each step's failure is a hard reject; the recipient MUST NOT learn $$v\_{\text{transfer}}$$ from a bundle that fails any step. 1. **Resolve the event.** Look up $$\text{ref}\_E$$ via the indexer or via direct RPC of the transaction. The lookup MUST return exactly one event whose contract address equals the deployed confidential-token contract. Extract the event's payload fields verbatim: - - For `Transfer`: `from`, `to`, $$R\_e$$, $$\sigma$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). - - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). + - For `Transfer`: `from`, `to`, $$R\_e$$, $$\sigma$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). + - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN_cont.md §11.2). Any other event type, or a `circuit_id` whose constraints reference a field the event does not carry, is rejected here. @@ -229,7 +229,7 @@ The account holder is the recipient of an on-chain confidential transfer (either | # | Constraint | |:--|:---| -| D1 | $$vk\_A = \text{Poseidon}(\delta\_{\text{vk}}, sk\_A, \text{addr\\\_f})$$ (viewing key correctly derived, binds proof to contract; mirrors DESIGN.md R2/T2/W2/S2/V2) | +| D1 | $$vk\_A = \text{Poseidon}(\delta\_{\text{vk}}, sk\_A, \text{addr\\\_f})$$ (viewing key correctly derived, binds proof to contract; mirrors DESIGN.md R2/T2/W2/S2) | | D2 | $$\text{PVK}\_A = vk\_A \cdot H$$ (binds proof to on-chain account) | | D3 | $$s = \text{ECDH}(vk\_A, R\_e)$$ (recipient-side ECDH shared scalar, DESIGN.md §2.4) | | D4 | $$v\_{\text{transfer}} = \tilde{v} - \text{Poseidon}(\delta\_{\text{transfer\\\_amount}}, s, \sigma\_E)$$ (correct decryption of event amount; matches DESIGN.md T9 for `Transfer` and O9 for `SpenderTransfer`) | @@ -307,9 +307,9 @@ A D-sender proof for a `SpenderTransfer` proves that the spender (not the owner) The auditor proves to a third party that an on-chain event corresponds to a transfer of amount $$v\_{\text{transfer}}$$ for one of the accounts under the auditor's scope. Used when the holder is uncooperative or when the disclosure recipient requires a guarantee that the auditor (not just the holder) has attested. -**Which auditor.** Every transfer carries ciphertexts under *two* auditor keys (DESIGN_cont.md §8.1): the recipient-side key $$K\_{\text{aud,r}}$$ (channel $$\delta\_{\text{aud\\\_r}}$$, two squeezes yielding masks for $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$) and the sender-side key $$K\_{\text{aud,s}}$$ (channel $$\delta\_{\text{aud\\\_s}}$$, two squeezes yielding masks for $$v\_{\text{transfer}}$$ and the sender's post-transfer balance). Whichever auditor is disclosing reuses the same shared-secret derivation they perform to read events natively; the circuit additionally encrypts the result to the disclosure recipient. +**Which auditor.** Every transfer carries ciphertexts under *two* auditor keys (DESIGN_cont.md §8.1): the recipient-side key $$K\_{\text{aud,r}}$$ (channel $$\delta\_{\text{aud\\\_r}}$$, two lanes yielding masks for $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$) and the sender-side key $$K\_{\text{aud,s}}$$ (channel $$\delta\_{\text{aud\\\_s}}$$, three lanes yielding masks for $$v\_{\text{transfer}}$$, the sender's post-transfer balance, and the secret-escrow slot of DESIGN.md §2.5). Whichever auditor is disclosing reuses the same shared-secret derivation they perform to read events natively; the circuit additionally encrypts the result to the disclosure recipient. -The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{\delta\_{\text{aud\\\_r}}, \delta\_{\text{aud\\\_s}}\\}$$ and the corresponding event ciphertext as $$\tilde{v}\_{\text{aud}} \in \\{\tilde{v}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}\\}$$. In each case the amount mask is the *first* squeeze of the channel's two-squeeze sponge; the second squeeze ($$m\_{r,r}$$ or $$m\_{b,s}$$) is computed and discarded for an amount disclosure, or used in place of the first for the balance/randomness variants noted below. +The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{\delta\_{\text{aud\\\_r}}, \delta\_{\text{aud\\\_s}}\\}$$ and the corresponding event ciphertext as $$\tilde{v}\_{\text{aud}} \in \\{\tilde{v}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}\\}$$. In each case the amount mask is lane 0 of the channel's sponge; the remaining lanes ($$m\_{r,r}$$ on the recipient channel; $$m\_{b,s}$$ and $$m\_{r,s}$$ on the sender channel) are computed and discarded for an amount disclosure, or used in place of lane 0 for the balance/randomness variants noted below. A3 is written with $$\text{SpongeSqueeze}\_2$$ because its first two lanes coincide with those of $$\text{SpongeSqueeze}\_3$$ on either channel (DESIGN.md §2.5); only a variant reading lane 2 squeezes three-wide. **Public inputs** @@ -337,7 +337,7 @@ D-auditor does not bind to an account record; the auditor key already binds the **Verifier flow.** Follow §5.3 with `circuit_id = D-auditor` (or the chosen balance / randomness variant). Step 2 is skipped — no $$\text{PVK}\_A$$ lookup is needed. Step 3 resolves $$K\_{\text{aud}}$$ at the event's ledger: $$K\_{\text{aud,r}}$$ from the `auditor_id` on the event's `to` account when disclosing the recipient-side channel, or $$K\_{\text{aud,s}}$$ from the `auditor_id` on the `from` account when disclosing the sender-side channel. `from` is the funds' owner in both `Transfer` and `SpenderTransfer`, since the sender-auditor channel always tracks the owner (DESIGN.md §7.8). -**Balance / randomness variants.** The second squeeze of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. These variants are not separately tabulated. +**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's post-transfer $$C\_{\text{spend}}$$ rather than its value alone; A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. --- @@ -432,7 +432,7 @@ The confidential-token contract requires no new state-modifying entry points to |:---|:---|:---| | `confidential_balance(account) -> ConfidentialAccount` | Verifier extracts $$\text{PVK}\_A$$ (and $$\text{PVK}\_B$$ for D-sender, $$C\_{\text{spend}}$$ for D-balance) from the returned `ConfidentialAccount` tuple | Already exposed (DESIGN_cont.md §11.3); the struct carries every field this layer reads, so no narrower accessor is required | | Auditor contract's key lookup for `auditor_id` | Verifier looks up $$K\_{\text{aud,r}}$$ or $$K\_{\text{aud,s}}$$ | Already exposed (DESIGN_cont.md §8.3). The auditor contract MAY maintain a sequence of versioned keys per `auditor_id` with activation ledgers; the verifier MUST select the version whose activation ledger is the largest value not exceeding the disclosed event's ledger (DESIGN_cont.md §8.3, *Auditor's off-chain obligation*). | -| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$ and $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$. | +| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ / $$\text{dvk\\\_cipher\\\_aud}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$. | | Instance storage: $$\text{addr\\\_f}$$ | D-recipient, D-sender, and D-balance bind $$vk$$ derivation to the contract via $$\text{addr\\\_f}$$ | Computed once at construction (DESIGN.md §3.5); the verifier reproduces it from the contract address using the encoding in DESIGN.md §2.7 | These are the only on-chain dependencies. Disclosure proofs are otherwise self-contained off-chain artifacts. diff --git a/packages/tokens/src/confidential/mod.rs b/packages/tokens/src/confidential/mod.rs index 8af6269dc..1d7689747 100644 --- a/packages/tokens/src/confidential/mod.rs +++ b/packages/tokens/src/confidential/mod.rs @@ -663,6 +663,7 @@ pub struct Withdraw { pub sigma: BytesN<32>, pub b_tilde: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Emits a `Withdraw` event. @@ -676,6 +677,7 @@ pub fn emit_withdraw( sigma: &BytesN<32>, b_tilde: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, + r_tilde_aud_s: &BytesN<32>, ) { Withdraw { from: from.clone(), @@ -685,6 +687,7 @@ pub fn emit_withdraw( sigma: sigma.clone(), b_tilde: b_tilde.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), + r_tilde_aud_s: r_tilde_aud_s.clone(), } .publish(e); } @@ -705,6 +708,7 @@ pub struct Transfer { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Emits a `Transfer` event. @@ -721,6 +725,7 @@ pub fn emit_transfer( r_tilde_aud_r: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, + r_tilde_aud_s: &BytesN<32>, ) { Transfer { from: from.clone(), @@ -733,6 +738,7 @@ pub fn emit_transfer( r_tilde_aud_r: r_tilde_aud_r.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), + r_tilde_aud_s: r_tilde_aud_s.clone(), } .publish(e); } @@ -754,6 +760,7 @@ pub struct SpenderTransfer { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, + pub dvk_cipher_aud: BytesN<32>, } /// Emits an `SpenderTransfer` event. @@ -770,6 +777,7 @@ pub fn emit_spender_transfer( r_tilde_aud_r: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, a_tilde_aud_s: &BytesN<32>, + dvk_cipher_aud: &BytesN<32>, ) { SpenderTransfer { spender: spender.clone(), @@ -782,6 +790,7 @@ pub fn emit_spender_transfer( r_tilde_aud_r: r_tilde_aud_r.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), a_tilde_aud_s: a_tilde_aud_s.clone(), + dvk_cipher_aud: dvk_cipher_aud.clone(), } .publish(e); } @@ -800,6 +809,8 @@ pub struct SetSpender { pub b_tilde: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, + pub dvk_cipher_aud: BytesN<32>, } /// Emits a `SetSpender` event. @@ -814,6 +825,8 @@ pub fn emit_set_spender( b_tilde: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, + r_tilde_aud_s: &BytesN<32>, + dvk_cipher_aud: &BytesN<32>, ) { SetSpender { account: account.clone(), @@ -824,6 +837,8 @@ pub fn emit_set_spender( b_tilde: b_tilde.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), + r_tilde_aud_s: r_tilde_aud_s.clone(), + dvk_cipher_aud: dvk_cipher_aud.clone(), } .publish(e); } diff --git a/packages/tokens/src/confidential/storage.rs b/packages/tokens/src/confidential/storage.rs index 42cd61ae4..f08c8cda8 100644 --- a/packages/tokens/src/confidential/storage.rs +++ b/packages/tokens/src/confidential/storage.rs @@ -115,6 +115,9 @@ pub struct WithdrawPayload { pub r_e_point: Point, pub sigma: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + /// Sender-auditor secret-escrow slot (sponge lane 2): the blinding of the + /// new spendable commitment, `r' + m_r_s` (constraint W_a5). + pub r_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -141,6 +144,9 @@ pub struct TransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + /// Sender-auditor secret-escrow slot (sponge lane 2): the blinding of the + /// sender's new spendable commitment, `r_A' + m_r_s` (constraint T_a9). + pub r_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -167,6 +173,9 @@ pub struct SpenderTransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, + /// Owner-auditor secret-escrow slot (sponge lane 2): the delegation + /// viewing key re-escrowed as `dvk_i + m_r_s` (constraint O_a9). + pub dvk_cipher_aud: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -192,6 +201,13 @@ pub struct SetSpenderPayload { pub sigma_a: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + /// Owner-auditor secret-escrow slot (sponge lane 2): the blinding of the + /// new spendable commitment, `r' + m_r_s` (constraint S_a6). + pub r_tilde_aud_s: BytesN<32>, + /// Owner-auditor escrow of the delegation viewing key, + /// `dvk_i + Poseidon2(delta_esc_dvk_aud, s_a_s, op_i)` (constraint S14). + /// Distinct from `escrowed_dvk`, which is the *spender*-side escrow. + pub dvk_cipher_aud: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -574,7 +590,7 @@ pub fn merge(e: &Env, account: &Address) { /// /// * topics - `["withdraw", from: Address, to: Address]` /// * data - `[amount: i128, r_e_point: BytesN<64>, sigma: BytesN<32>, b_tilde: -/// BytesN<32>, b_tilde_aud_s: BytesN<32>]` +/// BytesN<32>, b_tilde_aud_s: BytesN<32>, r_tilde_aud_s: BytesN<32>]` /// /// # Notes /// @@ -610,7 +626,7 @@ pub fn withdraw( // PI order (DESIGN §7.5): // C_spend, Y, addr_f, K_aud_s, a, - // C_spend', sigma, b_tilde, R_e, b_tilde_aud_s + // C_spend', sigma, b_tilde, R_e, b_tilde_aud_s, r_tilde_aud_s let mut pi = Bytes::new(e); append_point(&mut pi, &account.spendable_commitment); append_point(&mut pi, &account.spending_public_key); @@ -622,6 +638,7 @@ pub fn withdraw( append_field(&mut pi, &payload.b_tilde); append_point(&mut pi, &payload.r_e_point); append_field(&mut pi, &payload.b_tilde_aud_s); + append_field(&mut pi, &payload.r_tilde_aud_s); verify(e, CircuitType::Withdraw, &pi, proof); @@ -639,6 +656,7 @@ pub fn withdraw( &payload.sigma, &payload.b_tilde, &payload.b_tilde_aud_s, + &payload.r_tilde_aud_s, ); } @@ -667,7 +685,7 @@ pub fn withdraw( /// /// * topics - `["transfer", from: Address, to: Address]` /// * data - `[r_e_point, v_tilde, sigma, b_tilde, v_tilde_aud_r, r_tilde_aud_r, -/// v_tilde_aud_s, b_tilde_aud_s]` +/// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s]` /// /// # Security Warning /// @@ -690,7 +708,8 @@ pub fn confidential_transfer( // PI order (DESIGN §7.6): // C_spend_A, Y_A, PVK_B, addr_f, K_aud_r, K_aud_s, // C_spend', C_transfer, R_e, v_tilde, b_tilde, sigma, - // v_tilde_aud_r, r_tilde_aud_r, v_tilde_aud_s, b_tilde_aud_s + // v_tilde_aud_r, r_tilde_aud_r, v_tilde_aud_s, b_tilde_aud_s, + // r_tilde_aud_s let mut pi = Bytes::new(e); append_point(&mut pi, &sender.spendable_commitment); append_point(&mut pi, &sender.spending_public_key); @@ -708,6 +727,7 @@ pub fn confidential_transfer( append_field(&mut pi, &payload.r_tilde_aud_r); append_field(&mut pi, &payload.v_tilde_aud_s); append_field(&mut pi, &payload.b_tilde_aud_s); + append_field(&mut pi, &payload.r_tilde_aud_s); verify(e, CircuitType::Transfer, &pi, proof); @@ -726,6 +746,7 @@ pub fn confidential_transfer( &payload.r_tilde_aud_r, &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, + &payload.r_tilde_aud_s, ); } @@ -761,7 +782,7 @@ pub fn confidential_transfer( /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` /// * data - `[r_e_point, v_tilde, sigma_a, v_tilde_aud_r, r_tilde_aud_r, -/// v_tilde_aud_s, a_tilde_aud_s]` +/// v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` /// /// # Security Warning /// @@ -795,7 +816,8 @@ pub fn confidential_transfer_from( // PI order (DESIGN §7.8): // C_a, sigma_a, Y_op, PVK_recipient, K_aud_r, K_aud_s, // C_a', C_transfer, R_e, v_tilde, a_tilde', sigma_a', - // v_tilde_aud_r, r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s + // v_tilde_aud_r, r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, + // dvk_cipher_aud let mut pi = Bytes::new(e); append_point(&mut pi, &delegation.allowance_commitment); append_field(&mut pi, &delegation.allowance_salt); @@ -813,6 +835,7 @@ pub fn confidential_transfer_from( append_field(&mut pi, &payload.r_tilde_aud_r); append_field(&mut pi, &payload.v_tilde_aud_s); append_field(&mut pi, &payload.a_tilde_aud_s); + append_field(&mut pi, &payload.dvk_cipher_aud); verify(e, CircuitType::SpenderTransfer, &pi, proof); @@ -838,6 +861,7 @@ pub fn confidential_transfer_from( &payload.r_tilde_aud_r, &payload.v_tilde_aud_s, &payload.a_tilde_aud_s, + &payload.dvk_cipher_aud, ); } @@ -873,7 +897,7 @@ pub fn confidential_transfer_from( /// /// * topics - `["set_spender", account: Address, spender: Address]` /// * data - `[live_until_ledger: u32, r_e_point, sigma, b_tilde, v_tilde_aud_s, -/// b_tilde_aud_s]` +/// b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` /// /// # Security Warning /// @@ -897,7 +921,8 @@ pub fn set_spender( // PI order (DESIGN §7.7): // C_spend, Y, Y_op, spender_id (op_i), addr_f, K_aud_s, // C_spend', C_a, escrowed_dvk, b_tilde, a_tilde, - // sigma, sigma_a, R_e, v_tilde_aud_s, b_tilde_aud_s + // sigma, sigma_a, R_e, v_tilde_aud_s, b_tilde_aud_s, + // r_tilde_aud_s, dvk_cipher_aud let mut pi = Bytes::new(e); append_point(&mut pi, &owner.spendable_commitment); append_point(&mut pi, &owner.spending_public_key); @@ -915,6 +940,8 @@ pub fn set_spender( append_point(&mut pi, &payload.r_e_point); append_field(&mut pi, &payload.v_tilde_aud_s); append_field(&mut pi, &payload.b_tilde_aud_s); + append_field(&mut pi, &payload.r_tilde_aud_s); + append_field(&mut pi, &payload.dvk_cipher_aud); verify(e, CircuitType::SetSpender, &pi, proof); @@ -942,6 +969,8 @@ pub fn set_spender( &payload.b_tilde, &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, + &payload.r_tilde_aud_s, + &payload.dvk_cipher_aud, ); } diff --git a/packages/tokens/src/confidential/test.rs b/packages/tokens/src/confidential/test.rs index e0923bce6..d6415bb72 100644 --- a/packages/tokens/src/confidential/test.rs +++ b/packages/tokens/src/confidential/test.rs @@ -219,6 +219,7 @@ fn withdraw_data(e: &Env) -> Bytes { r_e_point: fixture_point(e), sigma: fixture_field(e, 0xbb), b_tilde_aud_s: fixture_field(e, 0xcc), + r_tilde_aud_s: fixture_field(e, 0xdd), }, proof: Bytes::new(e), } @@ -238,6 +239,7 @@ fn transfer_data(e: &Env) -> Bytes { r_tilde_aud_r: fixture_field(e, 0x15), v_tilde_aud_s: fixture_field(e, 0x16), b_tilde_aud_s: fixture_field(e, 0x17), + r_tilde_aud_s: fixture_field(e, 0x18), }, proof: Bytes::new(e), } @@ -257,6 +259,8 @@ fn set_spender_data(e: &Env) -> Bytes { sigma_a: fixture_field(e, 0x24), v_tilde_aud_s: fixture_field(e, 0x25), b_tilde_aud_s: fixture_field(e, 0x26), + r_tilde_aud_s: fixture_field(e, 0x27), + dvk_cipher_aud: fixture_field(e, 0x28), }, proof: Bytes::new(e), } @@ -276,6 +280,7 @@ fn spender_transfer_data(e: &Env) -> Bytes { r_tilde_aud_r: fixture_field(e, 0x35), v_tilde_aud_s: fixture_field(e, 0x36), a_tilde_aud_s: fixture_field(e, 0x37), + dvk_cipher_aud: fixture_field(e, 0x38), }, proof: Bytes::new(e), } @@ -384,6 +389,7 @@ fn withdraw_non_canonical_scalar_panics() { r_e_point: fixture_point(&h.e), sigma: BytesN::from_array(&h.e, &[0xff; 32]), b_tilde_aud_s: fixture_field(&h.e, 0x12), + r_tilde_aud_s: fixture_field(&h.e, 0x13), }, proof: Bytes::new(&h.e), } From 13d6c21582eb37df48ae02edb4bff8c578129804 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:02:00 +0200 Subject: [PATCH 02/12] cite instead of copy in CLAUDE.md --- packages/tokens/src/confidential/CLAUDE.md | 8 ++++---- packages/tokens/src/confidential/circuits/CLAUDE.md | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/tokens/src/confidential/CLAUDE.md b/packages/tokens/src/confidential/CLAUDE.md index adf058640..6c84e551b 100644 --- a/packages/tokens/src/confidential/CLAUDE.md +++ b/packages/tokens/src/confidential/CLAUDE.md @@ -59,13 +59,13 @@ Five things exist in more than one file. Changing the normative copy means grepp | Content | Normative source | Copies live in | |:---|:---|:---| -| The 17 domain-separation tags | `DESIGN_cont.md` §13 | `SDK.md` §4.8, referenced by `SELECTIVE_DISCLOSURE.md` | -| Sponge lane assignment (lane 0 = amount mask, lane 1 = balance/allowance/randomness, lane 2 = sender-auditor secret-escrow slot) | `DESIGN.md` §2.5 | `SDK.md` §4.3 and §11 | +| Domain-separation tag assignments | `DESIGN_cont.md` §13 | `SDK.md` §4.8, referenced by `SELECTIVE_DISCLOSURE.md` | +| Sponge lane assignment | `DESIGN.md` §2.5 | `SDK.md` §4.3 and §11 | | Per-circuit scalar-multiplication counts | `DESIGN_cont.md` §10.3 | `OVERVIEW.md` | -| Checkpoint event set (`Withdraw`, `Transfer` sender side, `SetSpender`, `RevokeSpender`) | `DESIGN.md` §5.2 | `INDEXER.md`, `SDK.md` | +| Checkpoint event set | `DESIGN.md` §5.2 | `INDEXER.md`, `SDK.md` | | Replay-window anchor `T₀` | `DESIGN.md` §5.2 | `INDEXER.md`, `OVERVIEW.md` | -The tags are a cross-language wire contract. `DESIGN_cont.md` §13 assigns all seventeen and no other document may; `circuits/lib/src/lib.nr` implements 1–13 and 17, because 14 is derived off-circuit and 15–16 belong to the off-chain disclosure layer. That gap is intentional. Changing any assigned value is a new deployment, not an upgrade. +The tags are a cross-language wire contract. `DESIGN_cont.md` §13 is their only authoritative source: it assigns every value, and it states which subset `circuits/lib/src/lib.nr` implements and why the remainder are absent. Changing any assigned value is a new deployment, not an upgrade. ### Editing rules diff --git a/packages/tokens/src/confidential/circuits/CLAUDE.md b/packages/tokens/src/confidential/circuits/CLAUDE.md index 94c1855ee..69b209a41 100644 --- a/packages/tokens/src/confidential/circuits/CLAUDE.md +++ b/packages/tokens/src/confidential/circuits/CLAUDE.md @@ -34,11 +34,11 @@ Directory `transfer/` is package `circuit_transfer`; `gadgets/commit/` is `gadge ### Never hash raw -`poseidon_with_domain` is the only Poseidon entry point in `lib/src/lib.nr`; calling the underlying hash directly is a violation of the library contract. The domain tag is always the first absorbed element. The numeric tag values are the cross-language contract with the SDK — see `../CLAUDE.md` and `docs/DESIGN_cont.md` §13, which is their only authoritative source. +`poseidon_with_domain` is the only Poseidon entry point in `lib/src/lib.nr`; calling the underlying hash directly is a violation of the library contract. The domain tag is always the first absorbed element. The numeric tag values are the cross-language contract with the SDK — see `../CLAUDE.md` and `../docs/DESIGN_cont.md` §13, which is their only authoritative source. -Sponge parameters: width 4, rate 3, capacity 1, `iv = len · 2^64`. Empty input still applies the squeeze permutation, matching the on-chain sponge. The squeeze order is fixed — **lane 0 is always an amount mask, lane 1 always a balance/allowance/randomness mask, lane 2 always the sender-auditor secret-escrow slot** — and `encrypt_auditor_sender_balance` deliberately takes lane 1 so a balance checkpoint can never share a pad with an amount ciphertext under `(r_e, σ)` reuse. `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, and `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)` — the absorb fits one rate-3 block, so both read the same permutation, and a divergence would silently change every existing mask. +Sponge parameters, the canonical lane assignment, and the mode-exclusivity rule that follows from a single-block absorb are normative in `../docs/DESIGN.md` §2.5; the Noir sponge must match it exactly. The obligations that section places on this code: `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)`, and `encrypt_auditor_sender_balance` must keep taking lane 1. A divergence in any of the three silently changes every existing mask. -Only `AUDITOR_SENDER` (11) is squeezed three-wide; `AUDITOR_RECIPIENT` (12) stays at two lanes. Lane 2 carries a *different plaintext per operation* — the new spendable blinding on checkpoints, `dvk_i` on spender transfers — which is not pad reuse, because the pad is keyed by a per-operation-fresh `(s_{a,s}, σ)`. +`AUDITOR_SENDER` is the only tag squeezed three-wide and `AUDITOR_RECIPIENT` the only one squeezed two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. ECDH must absorb both `S.x` and `S.y`; x-only extraction collapses `P` and `-P`. From 477daa60d88c8d21d2a7e0ec94aa08ed6b7d4e86 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 10:21:58 +0200 Subject: [PATCH 03/12] docs(confidential): make the sender-auditor channel three-lane throughout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DESIGN_cont §8.1/§8.2/§8.4/§8.5 still read the sender channel two-wide and still claimed no auditor can open C_spend; SDK §11 and OVERVIEW's visibility table omitted lane 2 entirely. Scope the spend-side opening to the checkpoint events that escrow it, restore V2 in the vk contract-binding lists, and move the lane-2 semantics out of DESIGN §2.5 into §8.1. --- .../tokens/src/confidential/docs/DESIGN.md | 6 +-- .../src/confidential/docs/DESIGN_cont.md | 48 ++++++++++++------- .../tokens/src/confidential/docs/OVERVIEW.md | 6 +++ packages/tokens/src/confidential/docs/SDK.md | 18 +++---- .../confidential/docs/SELECTIVE_DISCLOSURE.md | 6 +-- 5 files changed, 52 insertions(+), 32 deletions(-) diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index 4b206529b..bf76cfe4c 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -149,9 +149,9 @@ $$\text{SpongeSqueeze}\_n(\delta\_{\text{channel}}, s, \sigma) = \bigl(\text{sta where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is the number of rate lanes read; the capacity lane $$\text{state}[3]$$ is never squeezed. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, squeezed three-wide, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$, squeezed two-wide. No other arity is instantiated. Because the absorb is one block, $$\text{SpongeSqueeze}\_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}\_2(\delta, s, \sigma)$$: widening a channel adds a lane without changing the value of any existing one. -Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **secret-escrow slot**: it carries the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6) and the delegation viewing key $$dvk\_i$$ on spender transfers (O\_a9). Carrying two kinds of plaintext under one lane is not pad reuse — the pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. +**Lane assignment.** Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **secret-escrow slot**; which plaintext it carries per operation, and why one lane over two kinds of plaintext is not pad reuse, is specified in Section 8.1 *The lane-2 slot*. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. -**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ is the sole three-lane tag and $$\delta\_{\text{aud\\\_r}}$$ the sole two-lane tag; every other tag in Section 13 is used only with the single-output form above. +**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two multi-lane tags; every other tag in Section 13 is used only with the single-output form above. Reading one multi-lane tag at two different widths is not a mode violation, by the prefix property above: the sender-auditor tag is squeezed three-wide on every operation that escrows lane 2 and two-wide on `RevokeSpender` (V\_a3), while the recipient-auditor tag is always two-wide. The sponge masks are deterministic in $$(s, \sigma)$$, where $$s$$ is the ECDH shared scalar of Section 2.4, so reusing the pair across two operations reuses every pad slot they share, and a slot whose plaintext is known in one operation (e.g. a transfer amount known to its recipient) decrypts the other operation's ciphertext in that slot. The canonical slot assignment above limits the blast radius of such reuse to same-slot pairs, but does not eliminate it; provers and wallets MUST use a fresh $$(r\_e, \sigma)$$ for every proof. Because $$r\_e$$ is derived from the originator's viewing key and the salt rather than drawn independently (§5.3), a fresh salt is the only thing that makes the pair fresh: the salt carries the entire requirement, and Section 9.6's retry rule is what discharges it. @@ -290,7 +290,7 @@ The spending public key is stored on-chain at registration. Knowledge of $$sk$$ $$vk = \text{Poseidon}(\delta\_{\text{vk}}, sk, \text{addr\\\_f})$$ -A scalar in $$\mathbb{F}\_r$$, unique per $$(sk, \text{addr\\\_f})$$ pair. Enables balance decryption without spending authority, and — through the ephemeral-scalar derivation of §5.3 — reconstruction of the Pedersen openings of transfers the account originated; §9.4 states the full capability of a compromised $$vk$$. Cannot recover $$sk$$ (Poseidon preimage resistance). Because $$\text{addr\\\_f}$$ is bound into the derivation, proofs that constrain $$vk$$ (R2, W2, T2, S2) are inherently bound to the contract, eliminating the need for explicit per-circuit context binding. +A scalar in $$\mathbb{F}\_r$$, unique per $$(sk, \text{addr\\\_f})$$ pair. Enables balance decryption without spending authority, and — through the ephemeral-scalar derivation of §5.3 — reconstruction of the Pedersen openings of transfers the account originated; §9.4 states the full capability of a compromised $$vk$$. Cannot recover $$sk$$ (Poseidon preimage resistance). Because $$\text{addr\\\_f}$$ is bound into the derivation, proofs that constrain $$vk$$ (R2, W2, T2, S2, V2) are inherently bound to the contract, eliminating the need for explicit per-circuit context binding. ### 4.3 Public Viewing Key diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 49940fe9a..a1685ce89 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -23,22 +23,24 @@ $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}}) \qquad \text{(DESIGN §2.4)}$$ $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma)$$ $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}, \qquad \tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + 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: +**Sender's auditor** ($$K\_{\text{aud,s}}$$, from the sender's `auditor_id`) receives the transfer amount, the sender's post-transfer balance, and -- in the lane-2 secret-escrow slot (DESIGN §2.5 *Lane assignment*) -- the sender's post-transfer spendable blinding $$r\_A'$$: $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}}) \qquad \text{(DESIGN §2.4)}$$ -$$(m\_{v,s}, m\_{b,s}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ -$$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}, \qquad \tilde{b}\_{\text{aud,s}} = (v\_A - v\_{\text{transfer}}) + m\_{b,s}$$ +$$(m\_{v,s}, m\_{b,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ +$$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}, \qquad \tilde{b}\_{\text{aud,s}} = (v\_A - v\_{\text{transfer}}) + m\_{b,s}, \qquad \tilde{r}\_{\text{aud,s}} = r\_A' + m\_{r,s}$$ -The transfer circuit (constraints T\_a1--T\_a8) enforces correct computation. At operation time, the contract fetches both auditor keys from the auditor contract using the *stored* `auditor_id` field of each account; neither the sender nor the recipient can substitute a different key for the operation being proven. This guarantee is scoped to operation time: *which* auditor an account is bound to is chosen by the account owner at registration (DESIGN §7.2), subject only to existence in the auditor registry unless the deployment gates the selection in its `Hooks::on_register` implementation ([COMPLIANCE.md](./COMPLIANCE.md) §4.3). +The transfer circuit (constraints T\_a1--T\_a9) enforces correct computation. At operation time, the contract fetches both auditor keys from the auditor contract using the *stored* `auditor_id` field of each account; neither the sender nor the recipient can substitute a different key for the operation being proven. This guarantee is scoped to operation time: *which* auditor an account is bound to is chosen by the account owner at registration (DESIGN §7.2), subject only to existence in the auditor registry unless the deployment gates the selection in its `Hooks::on_register` implementation ([COMPLIANCE.md](./COMPLIANCE.md) §4.3). 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} = \text{Poseidon}(\delta\_{\text{ecdh}}, S\_{a,s}.x, S\_{a,s}.y)$$ -$$(m\_{v,s}, m\_{b,s}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ -$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_{\text{new}} = \tilde{b}\_{\text{aud,s}} - m\_{b,s}$$ +$$(m\_{v,s}, m\_{b,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ +$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_{\text{new}} = \tilde{b}\_{\text{aud,s}} - m\_{b,s}, \qquad r\_{\text{new}} = \tilde{r}\_{\text{aud,s}} - m\_{r,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{transfer}}, r\_{\text{transfer}})$$. +**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6) and the delegation viewing key $$dvk\_i$$ on spender transfers (O\_a9, Section 8.4). Carrying two kinds of plaintext under one lane is not pad reuse: the pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. + **Recipient-auditor opening capability.** Because the recipient-auditor recovers $$r\_{\text{transfer}}$$ for every inbound transfer, and because deposits add to `receiving_commitment` 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{transfer},i} + \sum\_j a\_j, \qquad r\_r = \sum\_i r\_{\text{transfer},i}$$ @@ -48,10 +50,18 @@ where $$i$$ ranges over inbound transfers and spender-transfers since the last m The capability is bounded in three ways: - **Forward-only.** Only events emitted while the auditor key was active are decryptable. -- **Receiving-side only.** The opening covers `receiving_commitment`. It does not extend to $$C\_{\text{spend}}$$, 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 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. +- **Receiving-side only.** The reconstruction above covers `receiving_commitment`. It does not extend to $$C\_{\text{spend}}$$: the recipient-auditor cannot derive the spend-side blinding $$r\_s = \text{Poseidon}(\delta\_{\text{spend\\\_r}}, vk\_A, \sigma)$$, which depends on $$vk\_A$$. It 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. The spend-side opening reaches the *sender*-auditor by a different route -- the lane-2 escrow, bounded separately below -- not by this reconstruction. - **Reset by merge.** 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 reconstruction above restarts from the next inbound flow. -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, while the sender-auditor remains the seize-enabling party for the spendable-balance side via $$\tilde{b}\_{\text{aud,s}}$$. +**Sender-auditor opening capability.** The lane-2 escrow hands the sender-auditor the *blinding* of the account's post-operation spendable balance directly, without $$vk\_A$$: together with the value in $$\tilde{b}\_{\text{aud,s}}$$ it is a full Pedersen opening of $$C\_{\text{spend}}'$$. It is available at exactly the three checkpoint operations that escrow lane 2 -- withdrawal (W\_a5), outgoing transfer (T\_a9), and `set_spender` (S\_a6) -- and is likewise bounded: + +- **Forward-only**, on the same grounds as the recipient side. +- **Event-scoped, not standing.** `revoke_spender` escrows no blinding (V\_a3 stays two-lane, DESIGN §7.9), so a revoke leaves the auditor with the post-reclaim *value* alone and no opening. +- **Lapsed by an inbound transfer.** An escrowed blinding survives a merge only when every inflow it folds in carried $$r = 0$$ -- that is, deposits only (Section 7.3), whose amounts are public, so the auditor can still open the post-merge $$C\_{\text{spend}}$$. A single inbound confidential transfer or spender-transfer folds in an $$r\_{\text{transfer}}$$ the sender-auditor does not hold, and the opening lapses until the next checkpoint. + +Neither auditor therefore holds a standing opening of $$C\_{\text{spend}}$$; each holds one only as of a named event. + +These bounded openings are what enable 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, while the sender-auditor is the seize-enabling party for the spendable-balance side via $$\tilde{b}\_{\text{aud,s}}$$ and $$\tilde{r}\_{\text{aud,s}}$$. ### 8.2 Auditor Visibility Properties @@ -59,12 +69,12 @@ This bounded opening is what enables the clawback flow specified in [COMPLIANCE. **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{transfer}})$$ 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 spender**: 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 spender**: 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). +- **Outgoing transfer**: auditor decrypts post-transfer balance $$(v\_A - v\_{\text{transfer}})$$ from $$\tilde{b}\_{\text{aud,s}}$$ and the matching blinding $$r\_A'$$ from $$\tilde{r}\_{\text{aud,s}}$$ (constraints T\_a5--T\_a9). +- **Withdrawal**: auditor decrypts post-withdrawal balance $$(v - a)$$ from $$\tilde{b}\_{\text{aud,s}}$$ and the matching blinding $$r'$$ from $$\tilde{r}\_{\text{aud,s}}$$ (constraints W\_a1--W\_a5). The withdrawal amount $$a$$ is also visible as a public input. +- **Set spender**: auditor decrypts escrowed amount $$v\_a$$ from $$\tilde{v}\_{\text{aud,s}}$$, post-escrow balance $$(v - v\_a)$$ from $$\tilde{b}\_{\text{aud,s}}$$, and the matching blinding $$r'$$ from $$\tilde{r}\_{\text{aud,s}}$$ (constraints S\_a1--S\_a6). +- **Revoke spender**: 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). This is the one checkpoint operation that escrows no blinding: V\_a3 stays two-lane. -The recipient's auditor does not see the sender's balance in any of these operations. +The three operations that also escrow the post-operation spendable blinding confer the opening capability bounded in Section 8.1. The recipient's auditor does not see the sender's balance in any of these operations. **Per-transfer Pedersen randomness (recipient-auditor, not sender-auditor).** Beyond the transfer amount, the recipient's auditor also decrypts the per-transfer Pedersen blinding $$r\_{\text{transfer}}$$ from $$\tilde{r}\_{\text{aud,r}}$$ on every confidential transfer and spender-transfer; Section 8.1 states the opening capability this confers and its bounds (forward-only, receiving-side only, reset by merge). The sender's auditor does not see $$r\_{\text{transfer}}$$. The *originating account's own* $$vk$$ holder does, by recomputing $$r\_e$$ and hence $$s$$ (DESIGN.md §5.3); that path lies outside the auditor model and its consequences are stated in §9.4. @@ -84,15 +94,15 @@ When building public inputs for any operation that produces auditor ciphertexts ### 8.4 Spender Transfer Auditing -Each spender 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: +Each spender transfer produces auditor ciphertexts under two keys (constraints O\_a1--O\_a9), 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{transfer}} = \tilde{v}\_{\text{aud,r}} - m\_{v,r}, \qquad r\_{\text{transfer}} = \tilde{r}\_{\text{aud,r}} - m\_{r,r}$$ -The owner's auditor decrypts the transfer amount and post-transfer allowance: +The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the lane-2 secret-escrow slot -- the delegation viewing key $$dvk\_i$$ (O\_a9): -$$(m\_{v,s}, m\_{a,s}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ -$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}$$ +$$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ +$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}, \qquad dvk\_i = \text{dvk\\\_cipher\\\_aud} - m\_{r,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 spender-transfer inbound flows: $$r\_{\text{transfer}}$$ from spender-transfers contributes to $$r\_r$$ in $$C\_{\text{receive}}$$ identically to owner-transfer inbound flows. @@ -100,6 +110,8 @@ where $$s\_{a,r}$$, $$s\_{a,s}$$, and $$\sigma\_a$$ are recovered from the event The auditor tracks each allowance's current value through the per-event ciphertexts produced at every state-changing operation: `set_spender` 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_spender` reveals the reclaimed amount (Section 8.2). +**Allowance opening.** The owner's auditor also receives $$dvk\_i$$ itself: under $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ at `set_spender` (S14, DESIGN §7.11) and in lane 2 at every spender transfer (O\_a9, §8.4). Since the allowance blinding is $$r\_a = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a)$$ and $$\sigma\_a$$ is published in the event, the auditor reconstructs the full Pedersen opening of $$C\_a$$ at each of those events -- unlike the spendable side, with no merge to lapse it, since a delegation's only state transitions are the events themselves. + **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. --- @@ -517,6 +529,6 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16; it takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ across two channels so that every row of §5.3's *Why reusing $$r\_e$$ is safe* keeps a distinct tag. -**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag squeezed three-wide, and it is squeezed three-wide everywhere it appears. 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. +**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag ever squeezed three-wide, and the widths it is read at agree on their shared lanes (§2.5 *Mode exclusivity*). 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. **Cross-protocol collision.** Future protocols that share Grumpkin / BN254 / Poseidon2 with this protocol -- 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/v1:X"}))$$, but this is a deployment-time choice that must be applied uniformly and disclosed in the deployment's circuit-binding documentation. diff --git a/packages/tokens/src/confidential/docs/OVERVIEW.md b/packages/tokens/src/confidential/docs/OVERVIEW.md index d9840bb24..8c65079f2 100644 --- a/packages/tokens/src/confidential/docs/OVERVIEW.md +++ b/packages/tokens/src/confidential/docs/OVERVIEW.md @@ -129,8 +129,13 @@ The system supports **real-time auditing** via a dual-auditor model. Each accoun | Sender's post-transfer balance | No | Yes | | Withdrawal amount | n/a | Yes (publicly visible) | | Post-withdrawal balance | n/a | Yes | +| Post-operation spendable blinding factor | No | Yes, at withdrawal, outgoing transfer, and spender setup (enables opening the sender's spendable balance as of that event; not at spender revocation, and lapses at the next merge that folds in an inbound transfer) | | Spender escrow / reclaim amount | n/a | Yes (owner's auditor) | | Post-transfer spender allowance | No (for spender transfers) | Yes (owner's auditor) | +| Post-escrow / post-reclaim balance | n/a | Yes (owner's auditor), at spender setup and revocation respectively | +| Delegation viewing key `dvk` | No | Yes (owner's auditor), at spender setup and on every spender transfer (enables opening the allowance commitment) | + +The table covers every auditor ciphertext the protocol produces; `DESIGN_cont.md` §8.1-§8.5 is the normative account, including the bounds on each opening capability. Each auditor decrypts its ciphertexts by running the channel sponge (recipient-auditor channel for recipients, sender-auditor channel for senders/owners) with its private key, the ephemeral public key, and the per-operation salt published in the operation's event. @@ -140,6 +145,7 @@ Each auditor decrypts its ciphertexts by running the channel sponge (recipient-a - **Dual-auditor ciphertexts.** The ciphertexts each operation produces are enforced by its zero-knowledge proof, so they cannot be omitted or malformed, and no extra action is needed from users. - **Per-account scope.** Auditing one account reveals nothing about any other account. - **Recipient-side opening capability.** The recipient's auditor holds the per-transfer Pedersen blinding $r_{\text{transfer}}$, hence the full Pedersen opening of the recipient's receiving balance between merges, which is what enables the seizure/clawback flow specified in `COMPLIANCE.md` §5; the capability and its bounds are specified in `DESIGN_cont.md` §8.1. +- **Sender-side opening capability.** The sender's auditor holds the opening of the account's spendable balance as of each withdrawal, outgoing transfer, and spender setup, because those operations also encrypt the post-operation blinding factor to it. The capability is event-scoped, not standing, and its bounds are specified in `DESIGN_cont.md` §8.1. - **Seamless auditor rotation.** When an auditor key is rotated, the new key immediately receives ciphertexts on subsequent operations. For the sender's auditor, the balance checkpoint at the next owner-initiated proof operation (transfer, withdrawal, set spender, or revoke spender) provides the current balance with no event replay or bootstrapping. - **Spender visibility.** The owner's auditor sees spender transfer amounts and post-transfer allowances via the same dual-auditor mechanism, and additionally sees escrowed and reclaimed amounts at `set_spender` and `revoke_spender`. - **Viewing vs. spending separation.** A viewing key cannot move or spend funds. Spending requires the separate spending key, which is never shared. diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index b0b5aa90c..2044df81c 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -161,7 +161,7 @@ Secret scalars — $$\sigma$$, $$\sigma_a$$ — MUST be produced by the rejectio DESIGN_cont.md §13 assigns all seventeen values and is their only source; the right-hand column is this document's addition. $$\delta_{\text{disc\\\_bind}}$$ and $$\delta_{\text{disc}}$$ belong to the off-chain disclosure layer (SELECTIVE_DISCLOSURE.md §2.2). Tag 1 is absorbed by the contract rather than by a circuit — the contract derives $$\text{addr\\\_f}$$ and $$\text{op}_i$$ on-chain and the circuits receive them as opaque public inputs (DESIGN.md §2.7 *Usage sites*) — so it is part of the on-chain wire contract all the same. None of 14–16 is absorbed either in a circuit or on-chain, so none is part of the on-chain wire contract, but all three are part of the cross-client contract because two wallets serving the same account must agree on them (§6.3). -All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tag 11 is the sole three-mask tag and tag 12 the sole two-mask tag; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows $$dvk_i$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_dvk_auditor.json`. +All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the multi-lane tags — 11 read three-wide wherever lane 2 is escrowed and two-wide on `RevokeSpender`, 12 always two-wide; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows $$dvk_i$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_dvk_auditor.json`. ### 4.9 Address compression @@ -461,22 +461,24 @@ A spender MUST NOT be able to reach the owner's spendable balance through any in ## 11. Auditor Client -An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. For each channel it computes the shared scalar against the event's ephemeral point, derives the two lane masks (§4.3), and subtracts. +An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. The two channels differ in what they yield (DESIGN_cont.md §8.1): -| Channel | Lane 0 | Lane 1 | -|:--|:--|:--| -| Sender / owner ($$\delta_{\text{aud\\\_s}}$$) | Transfer amount, or the escrowed amount for `SetSpender` and the reclaimed amount for `RevokeSpender` | Sender's post-operation balance, or post-operation allowance for a spender transfer | -| Recipient ($$\delta_{\text{aud\\\_r}}$$) | Transfer amount | Per-transfer Pedersen randomness $$r_{\text{transfer}}$$ | +| Channel | Lane 0 | Lane 1 | Lane 2 | +|:--|:--|:--|:--| +| Sender / owner ($$\delta_{\text{aud\\\_s}}$$) | Transfer amount, or the escrowed amount for `SetSpender` and the reclaimed amount for `RevokeSpender` | Sender's post-operation balance, or post-operation allowance for a spender transfer | Post-operation spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`; $$dvk_i$$ on `SpenderTransfer`; nothing on `RevokeSpender`, which stays two-lane | +| Recipient ($$\delta_{\text{aud\\\_r}}$$) | Transfer amount | Per-transfer Pedersen randomness $$r_{\text{transfer}}$$ | — (channel is two-lane) | `Withdraw`, `SetSpender`, and `RevokeSpender` carry a sender-channel balance checkpoint whose pad is lane **1**. Only `Withdraw` leaves lane 0 unused, its amount being public (DESIGN.md W_a3, §4.3); `SetSpender` and `RevokeSpender` read lane 0 as well, for the escrowed and reclaimed amounts respectively (DESIGN.md S_a4, V_a4). +An implementation MUST squeeze the sender / owner channel three-wide and MUST NOT widen the recipient channel. Reading lane 2 on `RevokeSpender` yields a pad over no ciphertext (V_a3 is two-lane, DESIGN.md §7.9); an implementation MUST treat a `RevokeSpender` event as carrying no escrowed blinding rather than substituting a stale one. Because the first two lanes of $$\text{SpongeSqueeze}_3$$ coincide with $$\text{SpongeSqueeze}_2$$ (§4.3), a client that already reads lanes 0 and 1 keeps every value it decrypted before. + **Cross-channel agreement.** Where an auditor holds the key for both parties, the amount decrypts independently on each channel and the circuit constrains both to the same value, so the two MUST agree. An implementation SHOULD perform this comparison and treat disagreement as evidence that $$k$$ is not the auditor key for both parties of that event. -**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). Rotation itself needs no replay on the sender side: the next owner-initiated proof operation publishes a fresh balance checkpoint under the new key. +**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). The lane-2 opening of the sender channel is forward-only and **event-scoped**: it opens the spendable commitment as of the checkpoint that escrowed it, and lapses at the next merge that folds in an inbound confidential transfer (DESIGN_cont.md §8.1 *Sender-auditor opening capability*). An implementation MUST NOT present a stored lane-2 blinding as the current spendable blinding once such a merge has been observed. Rotation itself needs no replay on the sender side: the next owner-initiated proof operation publishes a fresh balance checkpoint under the new key. -An auditor facade MUST NOT be able to construct a spending witness, and MUST NOT be able to open a post-merge spendable balance, since merge folds the receiving randomness into a blinding that depends on $$vk$$. +An auditor facade MUST NOT be able to construct a spending witness. It MUST NOT be able to open a spendable balance past a merge that folded in an inbound confidential transfer, since that merge adds an $$r_{\text{transfer}}$$ the sender channel never carries; a deposits-only merge leaves the escrowed blinding valid, deposits contributing $$r = 0$$ (DESIGN.md §7.3). --- diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 05fb9b80b..05b0d0a78 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -97,7 +97,7 @@ The disclosure layer inherits the protocol's threat model (DESIGN.md §3.2) and **Holder is the prover for D-recipient and D-sender variants.** The holder is trusted only to produce *correct* proofs about events they choose to disclose. The holder is *not* trusted to be complete: they may withhold events. Recipients that require completeness must obtain it from the auditor (DESIGN_cont.md §8) or from out-of-band evidence. -**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen openings of each scoped account's $$C\_{\text{spend}}$$ and of every $$C\_a$$ (DESIGN_cont.md §8.1, §8.2, §8.5); the D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. +**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen opening of every $$C\_a$$ (DESIGN_cont.md §8.5) and, at each checkpoint event that escrows lane 2, of the account's $$C\_{\text{spend}}$$ as of that event (DESIGN_cont.md §8.1 *Sender-auditor opening capability*, §8.2). The latter is event-scoped, not standing: `revoke_spender` escrows no blinding, and a merge that folds in an inbound confidential transfer lapses the escrowed opening until the next checkpoint. The D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. **Disclosure recipient is honest-but-curious.** The recipient correctly verifies proofs and decrypts ciphertexts addressed to their key. The recipient may attempt to replay or rebroadcast proofs; nonce binding prevents reuse against other parties. @@ -229,7 +229,7 @@ The account holder is the recipient of an on-chain confidential transfer (either | # | Constraint | |:--|:---| -| D1 | $$vk\_A = \text{Poseidon}(\delta\_{\text{vk}}, sk\_A, \text{addr\\\_f})$$ (viewing key correctly derived, binds proof to contract; mirrors DESIGN.md R2/T2/W2/S2) | +| D1 | $$vk\_A = \text{Poseidon}(\delta\_{\text{vk}}, sk\_A, \text{addr\\\_f})$$ (viewing key correctly derived, binds proof to contract; mirrors DESIGN.md R2/T2/W2/S2/V2) | | D2 | $$\text{PVK}\_A = vk\_A \cdot H$$ (binds proof to on-chain account) | | D3 | $$s = \text{ECDH}(vk\_A, R\_e)$$ (recipient-side ECDH shared scalar, DESIGN.md §2.4) | | D4 | $$v\_{\text{transfer}} = \tilde{v} - \text{Poseidon}(\delta\_{\text{transfer\\\_amount}}, s, \sigma\_E)$$ (correct decryption of event amount; matches DESIGN.md T9 for `Transfer` and O9 for `SpenderTransfer`) | @@ -337,7 +337,7 @@ D-auditor does not bind to an account record; the auditor key already binds the **Verifier flow.** Follow §5.3 with `circuit_id = D-auditor` (or the chosen balance / randomness variant). Step 2 is skipped — no $$\text{PVK}\_A$$ lookup is needed. Step 3 resolves $$K\_{\text{aud}}$$ at the event's ledger: $$K\_{\text{aud,r}}$$ from the `auditor_id` on the event's `to` account when disclosing the recipient-side channel, or $$K\_{\text{aud,s}}$$ from the `auditor_id` on the `from` account when disclosing the sender-side channel. `from` is the funds' owner in both `Transfer` and `SpenderTransfer`, since the sender-auditor channel always tracks the owner (DESIGN.md §7.8). -**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's post-transfer $$C\_{\text{spend}}$$ rather than its value alone; A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. +**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone — a later merge over an inbound transfer lapses it (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. --- From 9890acffd6d6fd37621f52351dc73ac00252d839 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:07:28 +0200 Subject: [PATCH 04/12] feat(confidential): emit the allowance salts in delegation events MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `SetSpender` gains `sigma_a` and `SpenderTransfer` gains `sigma_a_new`, both already proof-bound public inputs. Without them the owner's auditor cannot derive `r_a` and open `C_a`, contradicting DESIGN_cont §8.5. Event assertions in the happy-path tests now compare the typed `#[contractevent]` struct rather than counting events. --- packages/tokens/src/confidential/mod.rs | 19 +++-- packages/tokens/src/confidential/storage.rs | 10 ++- packages/tokens/src/confidential/test.rs | 88 +++++++++++++++++++-- 3 files changed, 99 insertions(+), 18 deletions(-) diff --git a/packages/tokens/src/confidential/mod.rs b/packages/tokens/src/confidential/mod.rs index 1d7689747..536737be6 100644 --- a/packages/tokens/src/confidential/mod.rs +++ b/packages/tokens/src/confidential/mod.rs @@ -342,7 +342,8 @@ pub trait ConfidentialToken { /// /// * topics - `["withdraw", from: Address, to: Address]` /// * data - `[amount: i128, r_e_point: BytesN<64>, sigma: BytesN<32>, - /// b_tilde: BytesN<32>, b_tilde_aud_s: BytesN<32>]` + /// b_tilde: BytesN<32>, b_tilde_aud_s: BytesN<32>, r_tilde_aud_s: + /// BytesN<32>]` fn withdraw(e: &Env, from: Address, to: Address, amount: i128, data: Bytes) { from.require_auth(); @@ -369,7 +370,7 @@ pub trait ConfidentialToken { /// /// * topics - `["transfer", from: Address, to: Address]` /// * data - `[r_e_point, v_tilde, sigma, b_tilde, v_tilde_aud_r, - /// r_tilde_aud_r, v_tilde_aud_s, b_tilde_aud_s]` + /// r_tilde_aud_r, v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s]` fn confidential_transfer(e: &Env, from: Address, to: Address, data: Bytes) { from.require_auth(); @@ -400,8 +401,8 @@ pub trait ConfidentialToken { /// /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` - /// * data - `[r_e_point, v_tilde, sigma_a, v_tilde_aud_r, r_tilde_aud_r, - /// v_tilde_aud_s, a_tilde_aud_s]` + /// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, + /// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` fn confidential_transfer_from( e: &Env, spender: Address, @@ -447,8 +448,8 @@ pub trait ConfidentialToken { /// # Events /// /// * topics - `["set_spender", account: Address, spender: Address]` - /// * data - `[live_until_ledger: u32, r_e_point, sigma, b_tilde, - /// v_tilde_aud_s, b_tilde_aud_s]` + /// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, + /// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` fn set_spender( e: &Env, account: Address, @@ -756,6 +757,7 @@ pub struct SpenderTransfer { pub r_e_point: BytesN<64>, pub v_tilde: BytesN<32>, pub sigma_a: BytesN<32>, + pub sigma_a_new: BytesN<32>, pub v_tilde_aud_r: BytesN<32>, pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, @@ -773,6 +775,7 @@ pub fn emit_spender_transfer( r_e_point: &BytesN<64>, v_tilde: &BytesN<32>, sigma_a: &BytesN<32>, + sigma_a_new: &BytesN<32>, v_tilde_aud_r: &BytesN<32>, r_tilde_aud_r: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, @@ -786,6 +789,7 @@ pub fn emit_spender_transfer( r_e_point: r_e_point.clone(), v_tilde: v_tilde.clone(), sigma_a: sigma_a.clone(), + sigma_a_new: sigma_a_new.clone(), v_tilde_aud_r: v_tilde_aud_r.clone(), r_tilde_aud_r: r_tilde_aud_r.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), @@ -806,6 +810,7 @@ pub struct SetSpender { pub live_until_ledger: u32, pub r_e_point: BytesN<64>, pub sigma: BytesN<32>, + pub sigma_a: BytesN<32>, pub b_tilde: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, @@ -822,6 +827,7 @@ pub fn emit_set_spender( live_until_ledger: u32, r_e_point: &BytesN<64>, sigma: &BytesN<32>, + sigma_a: &BytesN<32>, b_tilde: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, @@ -834,6 +840,7 @@ pub fn emit_set_spender( live_until_ledger, r_e_point: r_e_point.clone(), sigma: sigma.clone(), + sigma_a: sigma_a.clone(), b_tilde: b_tilde.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), diff --git a/packages/tokens/src/confidential/storage.rs b/packages/tokens/src/confidential/storage.rs index f08c8cda8..60c8ef471 100644 --- a/packages/tokens/src/confidential/storage.rs +++ b/packages/tokens/src/confidential/storage.rs @@ -781,8 +781,8 @@ pub fn confidential_transfer( /// /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` -/// * data - `[r_e_point, v_tilde, sigma_a, v_tilde_aud_r, r_tilde_aud_r, -/// v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` +/// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, +/// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` /// /// # Security Warning /// @@ -857,6 +857,7 @@ pub fn confidential_transfer_from( &payload.r_e_point, &payload.v_tilde, &sigma_a, + &payload.sigma_a_new, &payload.v_tilde_aud_r, &payload.r_tilde_aud_r, &payload.v_tilde_aud_s, @@ -896,8 +897,8 @@ pub fn confidential_transfer_from( /// # Events /// /// * topics - `["set_spender", account: Address, spender: Address]` -/// * data - `[live_until_ledger: u32, r_e_point, sigma, b_tilde, v_tilde_aud_s, -/// b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` +/// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, +/// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` /// /// # Security Warning /// @@ -966,6 +967,7 @@ pub fn set_spender( live_until_ledger, &payload.r_e_point, &payload.sigma, + &payload.sigma_a, &payload.b_tilde, &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, diff --git a/packages/tokens/src/confidential/test.rs b/packages/tokens/src/confidential/test.rs index d6415bb72..849f8ca56 100644 --- a/packages/tokens/src/confidential/test.rs +++ b/packages/tokens/src/confidential/test.rs @@ -5,7 +5,7 @@ use soroban_sdk::{ testutils::{Address as _, Events, Ledger}, token::StellarAssetClient, xdr::ToXdr, - Address, Bytes, BytesN, Env, + Address, Bytes, BytesN, Env, Event, }; use crate::confidential::{ @@ -13,9 +13,10 @@ use crate::confidential::{ storage as token_storage, verifier::{CircuitType, ConfidentialVerifier}, ConfidentialAccount, ConfidentialToken, ConfidentialTokenClient, NoHooks, RegisterData, - RegisterPayload, RevokeSpenderData, RevokeSpenderPayload, SetSpenderData, SetSpenderPayload, - SpenderDelegation, SpenderTransferData, SpenderTransferPayload, TransferData, TransferPayload, - WithdrawData, WithdrawPayload, + RegisterPayload, RevokeSpenderData, RevokeSpenderPayload, SetSpender, SetSpenderData, + SetSpenderPayload, SpenderDelegation, SpenderTransfer, SpenderTransferData, + SpenderTransferPayload, Transfer, TransferData, TransferPayload, Withdraw, WithdrawData, + WithdrawPayload, }; // ################## TEST FIXTURES ################## @@ -542,7 +543,22 @@ fn withdraw_transfers_tokens_and_updates_spendable() { h.token.withdraw(&alice, &beneficiary, &300i128, &withdraw_data(&h.e)); // 1 SAC transfer event + 1 Withdraw event. - assert_eq!(h.e.events().all().events().len(), 2); + let events = h.e.events().all(); + assert_eq!(events.events().len(), 2); + assert_eq!( + events.events().get(1).unwrap(), + &Withdraw { + from: alice.clone(), + to: beneficiary.clone(), + amount: 300i128, + r_e_point: fixture_point(&h.e), + sigma: fixture_field(&h.e, 0xbb), + b_tilde: fixture_field(&h.e, 0xaa), + b_tilde_aud_s: fixture_field(&h.e, 0xcc), + r_tilde_aud_s: fixture_field(&h.e, 0xdd), + } + .to_xdr(&h.e, &h.token_addr) + ); let token_client = soroban_sdk::token::TokenClient::new(&h.e, &h.sac_addr); assert_eq!(token_client.balance(&beneficiary), 300); @@ -590,7 +606,25 @@ fn confidential_transfer_updates_both_sides() { h.token.merge(&alice); h.token.confidential_transfer(&alice, &bob, &transfer_data(&h.e)); - assert_eq!(h.e.events().all().events().len(), 1); + let events = h.e.events().all(); + assert_eq!(events.events().len(), 1); + assert_eq!( + events.events().first().unwrap(), + &Transfer { + from: alice.clone(), + to: bob.clone(), + r_e_point: fixture_point(&h.e), + v_tilde: fixture_field(&h.e, 0x11), + sigma: fixture_field(&h.e, 0x13), + b_tilde: fixture_field(&h.e, 0x12), + v_tilde_aud_r: fixture_field(&h.e, 0x14), + r_tilde_aud_r: fixture_field(&h.e, 0x15), + v_tilde_aud_s: fixture_field(&h.e, 0x16), + b_tilde_aud_s: fixture_field(&h.e, 0x17), + r_tilde_aud_s: fixture_field(&h.e, 0x18), + } + .to_xdr(&h.e, &h.token_addr) + ); // Sender's spendable balance was overwritten. let alice_acc = h.token.confidential_balance(&alice); @@ -612,7 +646,25 @@ fn set_spender_stores_delegation() { h.token.register(&spender, &1u32, ®ister_data(&h.e)); h.token.set_spender(&alice, &spender, &1_000u32, &set_spender_data(&h.e)); - assert_eq!(h.e.events().all().events().len(), 1); + let events = h.e.events().all(); + assert_eq!(events.events().len(), 1); + assert_eq!( + events.events().first().unwrap(), + &SetSpender { + account: alice.clone(), + spender: spender.clone(), + live_until_ledger: 1_000u32, + r_e_point: fixture_point(&h.e), + sigma: fixture_field(&h.e, 0x23), + sigma_a: fixture_field(&h.e, 0x24), + b_tilde: fixture_field(&h.e, 0x21), + v_tilde_aud_s: fixture_field(&h.e, 0x25), + b_tilde_aud_s: fixture_field(&h.e, 0x26), + r_tilde_aud_s: fixture_field(&h.e, 0x27), + dvk_cipher_aud: fixture_field(&h.e, 0x28), + } + .to_xdr(&h.e, &h.token_addr) + ); let delegation = h.token.get_spender_delegation(&alice, &spender); assert_eq!(delegation.live_until_ledger, 1_000); @@ -682,7 +734,27 @@ fn confidential_transfer_from_updates_delegation_and_recipient() { h.token.set_spender(&alice, &spender, &1_000u32, &set_spender_data(&h.e)); h.token.confidential_transfer_from(&spender, &alice, &bob, &spender_transfer_data(&h.e)); - assert_eq!(h.e.events().all().events().len(), 1); + let events = h.e.events().all(); + assert_eq!(events.events().len(), 1); + assert_eq!( + events.events().first().unwrap(), + &SpenderTransfer { + spender: spender.clone(), + from: alice.clone(), + to: bob.clone(), + r_e_point: fixture_point(&h.e), + v_tilde: fixture_field(&h.e, 0x31), + // The delegation's pre-transfer salt, read from storage. + sigma_a: fixture_field(&h.e, 0x24), + sigma_a_new: fixture_field(&h.e, 0x33), + v_tilde_aud_r: fixture_field(&h.e, 0x34), + r_tilde_aud_r: fixture_field(&h.e, 0x35), + v_tilde_aud_s: fixture_field(&h.e, 0x36), + a_tilde_aud_s: fixture_field(&h.e, 0x37), + dvk_cipher_aud: fixture_field(&h.e, 0x38), + } + .to_xdr(&h.e, &h.token_addr) + ); // Delegation allowance commitment was rotated. let delegation = h.token.get_spender_delegation(&alice, &spender); From cac57a75f094a59ea346029b4149db9afc6a1fa9 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:13:22 +0200 Subject: [PATCH 05/12] test(confidential): cover the lane-2 escrow constraints, drop the dead helper Adds the missing negative tests for W_a5, T_a9, S_a6, S14 and O_a9 -- each constraint could be deleted without failing a test. `encrypt_auditor_sender_balance` has no circuit caller since the sender channel went three-lane; `sponge_squeeze_2` is now the prefix of `sponge_squeeze_3` so their agreement is structural rather than test-pinned. Also corrects the RevokeSpender and `op_i` rationales, which did not hold. --- .../src/confidential/circuits/CLAUDE.md | 4 +- .../src/confidential/circuits/lib/src/lib.nr | 52 +++++------- .../confidential/circuits/lib/src/tests.nr | 25 +----- .../encrypt_auditor_sender_balance.json | 15 ---- .../lib/testdata/sponge_squeeze_2.json | 4 +- .../circuits/set_spender/src/main.nr | 14 ++-- .../circuits/set_spender/src/tests.nr | 82 ++++++++++++++++++- .../circuits/spender_transfer/src/tests.nr | 39 +++++++++ .../circuits/transfer/src/tests.nr | 41 +++++++++- .../circuits/withdraw/src/tests.nr | 57 ++++++++++--- 10 files changed, 238 insertions(+), 95 deletions(-) delete mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json diff --git a/packages/tokens/src/confidential/circuits/CLAUDE.md b/packages/tokens/src/confidential/circuits/CLAUDE.md index 69b209a41..9ebffb14a 100644 --- a/packages/tokens/src/confidential/circuits/CLAUDE.md +++ b/packages/tokens/src/confidential/circuits/CLAUDE.md @@ -36,9 +36,9 @@ Directory `transfer/` is package `circuit_transfer`; `gadgets/commit/` is `gadge `poseidon_with_domain` is the only Poseidon entry point in `lib/src/lib.nr`; calling the underlying hash directly is a violation of the library contract. The domain tag is always the first absorbed element. The numeric tag values are the cross-language contract with the SDK — see `../CLAUDE.md` and `../docs/DESIGN_cont.md` §13, which is their only authoritative source. -Sponge parameters, the canonical lane assignment, and the mode-exclusivity rule that follows from a single-block absorb are normative in `../docs/DESIGN.md` §2.5; the Noir sponge must match it exactly. The obligations that section places on this code: `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)`, and `encrypt_auditor_sender_balance` must keep taking lane 1. A divergence in any of the three silently changes every existing mask. +Sponge parameters, the canonical lane assignment, and the mode-exclusivity rule that follows from a single-block absorb are normative in `../docs/DESIGN.md` §2.5; the Noir sponge must match it exactly. The obligations that section places on this code: `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, and `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)` — which is why `sponge_squeeze_2` is defined as the prefix of `sponge_squeeze_3` rather than as a second permutation. A divergence in either silently changes every existing mask. -`AUDITOR_SENDER` is the only tag squeezed three-wide and `AUDITOR_RECIPIENT` the only one squeezed two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. +`AUDITOR_SENDER` is squeezed three-wide by every circuit that escrows lane 2 and two-wide only by RevokeSpender (V_a3); `AUDITOR_RECIPIENT` is always two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. ECDH must absorb both `S.x` and `S.y`; x-only extraction collapses `P` and `-P`. diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index ef83cd765..d7da898ca 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -139,9 +139,9 @@ pub mod domain { /// on spender transfers. Constraints W_a3 / T_a6 / S_a3 / O_a6 /// (`delta_aud_s`). /// - /// RevokeSpender (V_a3) still reads two lanes: it writes no checkpoint - /// that needs an escrow slot, and the two widths agree on every lane they - /// share (see [`sponge_squeeze_3`]). + /// RevokeSpender (V_a3) still reads two lanes, so the opening the auditor + /// holds is not renewed there and lapses until the next checkpoint; the + /// two widths agree on every lane they share (see [`sponge_squeeze_3`]). /// /// The tag stays in a single sponge mode per Section 2.5: /// `sponge_squeeze_2(d, s, sigma)[0]` is the same field element as @@ -336,13 +336,15 @@ pub fn encrypt_esc_dvk(dvk: Field, s: Field, op_i: Field) -> Field { /// /// A single-output Poseidon rather than a third sponge lane, because at /// `set_spender` lane 2 of the owner-auditor sponge is already taken by S_a6. -/// Costs one Poseidon2 permutation and zero scalar multiplications; the pad's -/// freshness comes from `s_a_s`, which is fresh per operation because `r_e` -/// is, and `op_i` is a nonce by parallel with S12. +/// Costs one Poseidon2 permutation and zero scalar multiplications. `op_i` is +/// a per-`(owner, spender)` constant that separates pads across delegations, +/// not a nonce: the pad's freshness comes solely from `s_a_s`, which is fresh +/// per operation because `r_e` is, and `r_e` is fresh only because the salt is +/// (Section 5.3, Section 9.6). /// /// This is what gives the auditor the opening of `C_a`: with `dvk_i` it can -/// derive `r_a = derive_allow_r(dvk_i, sigma_a)`. Any fold of `C_a` into -/// `C_spend` therefore leaves the auditor's accumulator in sync. +/// derive `r_a = derive_allow_r(dvk_i, sigma_a)` and open the allowance +/// commitment at the delegation's every state transition (Section 8.5). pub fn encrypt_esc_dvk_auditor(dvk: Field, s_a_s: Field, op_i: Field) -> Field { dvk + poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, [s_a_s, op_i]) } @@ -359,11 +361,16 @@ pub fn encrypt_esc_dvk_auditor(dvk: Field, s_a_s: Field, op_i: Field) -> Field { /// 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 SpenderTransfer (O_a2, O_a6). +/// Used by the recipient-auditor channel of Transfer (T_a2) and +/// SpenderTransfer (O_a2), and by RevokeSpender's sender-auditor channel +/// (V_a3), which escrows nothing in lane 2. Every other sender-auditor +/// channel reads [`sponge_squeeze_3`]. +/// +/// Defined as the two-lane prefix of [`sponge_squeeze_3`] so the two cannot +/// drift apart: a change to the IV or the absorb applies to both by +/// construction rather than by test. pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { - let iv: Field = 3 * POSEIDON2_IV_BASE; - let state = poseidon2_permutation([d, s, sigma, iv], 4); + let state = sponge_squeeze_3(d, s, sigma); [state[0], state[1]] } @@ -392,24 +399,3 @@ pub fn sponge_squeeze_3(d: Field, s: Field, sigma: Field) -> [Field; 3] { let state = poseidon2_permutation([d, s, sigma, iv], 4); [state[0], state[1], state[2]] } - -/// Sender-auditor encrypted balance checkpoint, packaged as a single call. -/// -/// `b_tilde_aud_s = v_new + sponge_squeeze_2(AUDITOR_SENDER, s_a_s, sigma)[1]`, -/// where `s_a_s` is the sender-auditor ECDH shared-secret scalar produced by -/// [`ecdh`]. The pad is lane 1 -- the slot every sender channel (Withdraw -/// W_a4, Transfer T_a8, SetSpender S_a5, SpenderTransfer O_a8, RevokeSpender -/// V_a5) uses for its balance/allowance mask. Lane 0 is reserved for amount masks, so a balance -/// checkpoint never shares a pad with an amount ciphertext even if a -/// `(r_e, sigma)` pair is mistakenly reused across operations (the pair must -/// be unique per proof; DESIGN Section 2.5). -/// -/// No operation circuit calls this today: every sender-auditor channel now -/// squeezes lane 2 as well (see [`sponge_squeeze_3`]) and reads its lanes from -/// one array. It is kept as the named, fixture-pinned form of the lane-1 -/// balance ciphertext for SDK consumers, and because -/// `sponge_squeeze_3(..)[1] == sponge_squeeze_2(..)[1]` makes the two -/// interchangeable. -pub fn encrypt_auditor_sender_balance(v_new: Field, s_a_s: Field, sigma: Field) -> Field { - v_new + sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s, sigma)[1] -} diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 1a5a6099a..cbe9ef368 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -1,6 +1,6 @@ use crate::{ assert_on_curve_non_identity, commit, derive_allow_r, derive_spend_r, derive_transfer_blind, domain, - dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_amount, encrypt_auditor_sender_balance, + dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_amount, encrypt_balance, encrypt_esc_dvk, encrypt_esc_dvk_auditor, G, H, poseidon_with_domain, pvk_from_vk, scalar_mul, sponge, sponge_squeeze_2, sponge_squeeze_3, vk_from_sk, }; @@ -207,12 +207,6 @@ 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); - - // The auditor balance checkpoint pads with the sponge's second squeeze - // (the balance slot), not the single-squeeze funnel. - let b_tilde_aud_s = encrypt_auditor_sender_balance(v, s, sigma); - let mask_aud_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s, sigma)[1]; - assert(b_tilde_aud_s - mask_aud_s == v); } #[test] @@ -393,10 +387,6 @@ fn print_fixtures() { let esc = encrypt_esc_dvk(dvk, s_ecdh, op_i); println(f"encrypt_esc_dvk = {esc}"); - // 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]; @@ -509,16 +499,9 @@ fn fixtures_match_testdata() { assert(ss2_r[0] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0); assert(ss2_r[1] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1); - // encrypt_auditor_sender_balance pads with the sponge's *second* - // squeeze -- the balance slot, matching the two-ciphertext sender - // channels (Transfer T_a6). The first squeeze stays reserved for amount - // masks so the checkpoint pad can never collide with an amount pad even - // under (r_e, sigma) reuse. Pin against the squeeze_2 vector so the - // helper and the raw sponge can't drift apart. - assert( - encrypt_auditor_sender_balance(v, s_ecdh, sigma) - == v + SPONGE_SQUEEZE_2_AUDITOR_SENDER_1, - ); + // Lane 0 is the single-squeeze funnel: the absorb fits one rate-3 block, + // so a tag used in both modes would collapse one onto the other + // (Section 2.5 mode exclusivity). assert( poseidon_with_domain(domain::AUDITOR_SENDER, [s_ecdh, sigma]) == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0, 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 deleted file mode 100644 index 2d98d5ead..000000000 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_auditor_sender_balance.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "primitive": "encrypt_auditor_sender_balance", - "design_doc_refs": ["Constraint W_a3", "Constraint W_a4"], - "description": "Sender-auditor encrypted balance checkpoint via the second squeeze of the sender channel sponge: b_tilde_aud_s = v_new + SpongeSqueeze_2(AUDITOR_SENDER, s_a_s, sigma)[1]. Used by Withdraw, the only sender-side proof whose auditor block emits a single ciphertext. The second squeeze is the balance slot shared with Transfer/SetSpender/RevokeSpender; the first-squeeze amount slot is left unused so the checkpoint pad never coincides with an amount pad, even under (r_e, sigma) reuse.", - "vectors": [ - { - "inputs": { - "v_new": "0x3e8", - "s_a_s": "0x12345", - "sigma": "0x01" - }, - "output": "0x27f3739a132c6353cd5af3edac0ac75faf7fc606acb61367774e4f764ec17f5f" - } - ] -} 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 e8aeca281..981da5887 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,7 +1,7 @@ { "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, 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 equals poseidon_with_domain(d, [s, sigma]) on the same inputs (single permutation, same state[0]).", + "design_doc_refs": ["Section 2.5", "Constraint T_a2", "Constraint O_a2", "Constraint V_a3"], + "description": "Poseidon2 sponge with two squeezes (one absorb of (d, s, 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 recipient-auditor channel uses d = AUDITOR_RECIPIENT (12) and is always read two-wide; the sender-auditor channel d = AUDITOR_SENDER (11) is read two-wide only by RevokeSpender and three-wide elsewhere (see sponge_squeeze_3), of which these two lanes are the prefix. The first squeeze equals poseidon_with_domain(d, [s, sigma]) on the same inputs (single permutation, same state[0]).", "vectors": [ { "inputs": { "d": "0x0b", "s": "0x12345", "sigma": "0x01" }, diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr index d95737b01..718a93ac7 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr @@ -64,13 +64,13 @@ mod tests; // Auditor-side escrow // of dvk_i. Hands the // owner's auditor the -// full opening of C_a. -// Folding an unopenable -// C_a into C_spend -// would desync the -// auditor's accumulator -// for the account -// permanently. +// full opening of C_a: +// with dvk_i it derives +// r_a and opens the +// allowance at every +// state transition of +// the delegation +// (Section 8.5). // A separate single- // output Poseidon // rather than a sponge diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr index b2616068b..18ecfd3cb 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr @@ -202,7 +202,7 @@ fn set_spender_auditor_fixtures_match_lib() { assert(encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I) == DVK_CIPHER_AUD); } -// `run_main` accepts all 24 public inputs as parameters -- nothing baked in. +// `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 @@ -1316,3 +1316,83 @@ fn rejects_tampered_b_tilde_aud_s() { DVK_CIPHER_AUD, ); } + +#[test(should_fail)] +fn rejects_tampered_dvk_cipher_aud() { + // dvk_cipher_aud mutated by +1: S14 fails because the auditor-side escrow + // is pinned to dvk_i + Poseidon(delta_esc_dvk_aud, s_a_s, op_i), which the + // prover's claim no longer matches. + run_main( + SK, + V, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + ADDR_F, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_A_X, + C_A_Y, + R_E_X, + ESCROWED_DVK_CIPHER, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + R_TILDE_AUD_S, + DVK_CIPHER_AUD + 1, + ); +} + +#[test(should_fail)] +fn rejects_tampered_r_tilde_aud_s() { + // r_tilde_aud_s mutated by +1: S_a6 fails because the lane-2 escrow is + // pinned to r' = derive_spend_r(vk, sigma), which the prover's claim no + // longer matches. + run_main( + SK, + V, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + ADDR_F, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_A_X, + C_A_Y, + R_E_X, + ESCROWED_DVK_CIPHER, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + R_TILDE_AUD_S + 1, + DVK_CIPHER_AUD, + ); +} diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr index d2fc71d94..a0f8d490e 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -1461,6 +1461,45 @@ fn rejects_tampered_a_tilde_aud_s() { ); } +#[test(should_fail)] +fn rejects_tampered_dvk_cipher_aud() { + // dvk_cipher_aud mutated by +1: O_a9 fails because the lane-2 escrow is + // pinned to dvk_i, which the prover's claim no longer matches. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TRANSFER, + 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_TRANSFER_X, + C_TRANSFER_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, + DVK_CIPHER_AUD + 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 diff --git a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr index dd2254803..78ec6851d 100644 --- a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr @@ -196,7 +196,7 @@ fn transfer_auditor_fixtures_match_lib() { assert(r_new + m_s[2] == R_TILDE_AUD_S); } -// `run_main` accepts all 24 public inputs as parameters -- nothing baked in. +// `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 @@ -1347,3 +1347,42 @@ fn rejects_tampered_b_tilde_aud_s() { R_TILDE_AUD_S, ); } + +#[test(should_fail)] +fn rejects_tampered_r_tilde_aud_s() { + // r_tilde_aud_s mutated by +1: T_a9 fails because the lane-2 escrow is + // pinned to r_A' = derive_spend_r(vk, sigma), which the prover's claim no + // longer matches. + run_main( + SK, + V, + R, + V_TRANSFER, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + ADDR_F, + PVK_B_X, + PVK_B_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_TRANSFER_X, + C_TRANSFER_Y, + R_E_X, + R_E_Y, + V_TILDE, + B_TILDE, + SIGMA, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + R_TILDE_AUD_S + 1, + ); +} diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index c182f5881..fed12f4f3 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -1,6 +1,6 @@ use crate::main; use stellar_confidential_lib::{ - commit, derive_spend_r, domain, ecdh, encrypt_auditor_sender_balance, encrypt_balance, H, + commit, derive_spend_r, domain, ecdh, encrypt_balance, H, scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -75,8 +75,9 @@ fn print_fixtures() { let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); let r_e_pt = scalar_mul(R_E, H); let s_a_s = ecdh(R_E, k_aud_s); - let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s, SIGMA); - let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let b_tilde_aud_s = V_NEW + m_s[1]; + let r_tilde_aud_s = r_new + m_s[2]; let cx = c_new.x; let cy = c_new.y; let kx = k_aud_s.x; @@ -117,10 +118,11 @@ 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 = ecdh(R_E, k_aud_s); - let b_tilde_aud_s = encrypt_auditor_sender_balance(V_NEW, s_a_s, SIGMA); let vk = vk_from_sk(SK, ADDR_F); let r_new = derive_spend_r(vk, SIGMA); - let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let b_tilde_aud_s = V_NEW + m_s[1]; + let r_tilde_aud_s = r_new + m_s[2]; 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); @@ -166,8 +168,9 @@ fn full_withdrawal() { 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 = ecdh(R_E, k_aud_s); - let b_tilde_aud_s = encrypt_auditor_sender_balance(0, s_a_s, SIGMA); - let r_tilde_aud_s = r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let b_tilde_aud_s = m_s[1]; + let r_tilde_aud_s = r_new + m_s[2]; main( SK, V, @@ -202,9 +205,9 @@ fn rejects_under_funded_withdrawal() { 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 = ecdh(R_E, k_aud_s); - let b_tilde_aud_s_invalid = encrypt_auditor_sender_balance(V - a_too_large, s_a_s, SIGMA); - let r_tilde_aud_s_invalid = - r_new + sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA)[2]; + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + let b_tilde_aud_s_invalid = (V - a_too_large) + m_s[1]; + let r_tilde_aud_s_invalid = r_new + m_s[2]; main( SK, V, @@ -534,9 +537,8 @@ fn rejects_identity_k_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 + // b_tilde_aud_s mutated by +1: W_a4 fails because the lane-1 pad derives + // a specific scalar that no longer matches the prover's claim. Closes the // tampered-ciphertext criterion in the issue. main( SK, @@ -562,6 +564,35 @@ fn rejects_tampered_b_tilde_aud_s() { ); } +#[test(should_fail)] +fn rejects_tampered_r_tilde_aud_s() { + // r_tilde_aud_s mutated by +1: W_a5 fails because the lane-2 escrow is + // pinned to r' = derive_spend_r(vk, sigma), which the prover's claim no + // longer matches. + main( + SK, + V, + R, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + ADDR_F, + 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, + R_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 From aa8e4705bf377d87e89873683375debabc74d3ed Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:19:25 +0200 Subject: [PATCH 06/12] docs(confidential): correct the auditor's opening scope and the tag-17 home MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The lane-2 escrow does not lapse at a merge: one auditor_id serves both of an account's channels, so the same key holds every inbound blinding and carries the opening forward by addition. §8.1, §9.4, SDK §11, OVERVIEW and SELECTIVE_DISCLOSURE said otherwise. Also fills in the event and payload tables with the fields this branch added, specifies the auditor-side dvk escrow in §8.5 (DESIGN.md is over its LaTeX budget), and makes the COMPLIANCE §5.3 clawback sketch consume and re-emit the escrow instead of leaving r_s unresolved. --- .../src/confidential/circuits/lib/src/lib.nr | 7 ++- .../src/confidential/docs/COMPLIANCE.md | 13 ++--- .../tokens/src/confidential/docs/DESIGN.md | 8 ++-- .../src/confidential/docs/DESIGN_cont.md | 47 ++++++++++++------- .../tokens/src/confidential/docs/OVERVIEW.md | 4 +- packages/tokens/src/confidential/docs/SDK.md | 6 +-- .../confidential/docs/SELECTIVE_DISCLOSURE.md | 10 ++-- 7 files changed, 57 insertions(+), 38 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index d7da898ca..6f41e32cc 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -159,13 +159,18 @@ pub mod domain { pub global ECDH_SHARED_SECRET: Field = 13; /// Auditor-side delegation-key escrow mask (owner-auditor ECDH): /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, s_a_s, op_i)`. - /// Constraint S14 (`delta_esc_dvk_aud`). + /// Constraint S14, Section 8.5 (`delta_esc_dvk_aud`). /// /// A separate tag from `ESCROWED_DELEGATION_VIEWING_KEY` = 10 even though /// reuse would be secure -- the two shared scalars differ, since `Y_op` /// and `K_aud_s` are independent points. Sharing the tag would drop one /// of the three legs of Section 5.3's "why reusing r_e is safe" argument; /// a fresh tag keeps that argument intact. + /// + /// Unlike the tags above it, this one shares its shared scalar with + /// `AUDITOR_SENDER` (both key off `s_a_s`). What separates them is Section + /// 2.5 mode exclusivity: this tag is single-output, tag 11 is multi-lane, + /// so the two never yield the same field element. pub global ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR: Field = 17; } diff --git a/packages/tokens/src/confidential/docs/COMPLIANCE.md b/packages/tokens/src/confidential/docs/COMPLIANCE.md index cc24f991f..4e111b648 100644 --- a/packages/tokens/src/confidential/docs/COMPLIANCE.md +++ b/packages/tokens/src/confidential/docs/COMPLIANCE.md @@ -208,24 +208,25 @@ The admin role here is the same access-control surface introduced in §1.1; depl 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_commitment` 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}}, addr\_f$$. +**Public inputs.** $$C\_{\text{spend}}, C\_{\text{receive}}, K\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}^{\text{old}}, \tilde{r}\_{\text{aud,s}}^{\text{old}}, R\_e^{\text{old}}, \sigma^{\text{old}}, \alpha, C\_{\text{spend}}^{\text{new}}, \tilde{b}\_{\text{aud,s}}^{\text{new}}, \tilde{r}\_{\text{aud,s}}^{\text{new}}, R\_e^{\text{new}}, \sigma^{\text{new}}, addr\_f$$. -**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). +**Private witnesses.** $$k\_{\text{aud,s}}, v\_s, r\_s, v\_r, r\_r, r\_s', 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}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{old}}), \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. +2. **Spendable-balance decryption.** $$(m\_{v,s}^{\text{old}}, m\_{b,s}^{\text{old}}, m\_{r,s}^{\text{old}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{old}}), \sigma^{\text{old}})$$, then $$v\_s = \tilde{b}\_{\text{aud,s}}^{\text{old}} - m\_{b,s}^{\text{old}}$$ and $$r\_s = \tilde{r}\_{\text{aud,s}}^{\text{old}} - m\_{r,s}^{\text{old}}$$, and $$C\_{\text{spend}} = v\_s \cdot G + r\_s \cdot H$$. The lane-2 escrow (`DESIGN.md` W\_a5 / T\_a9 / S\_a6) is what makes this a closed derivation: the circuit recovers the blinding from the auditor channel itself rather than from $$vk\_A$$, which it does not hold. The old checkpoint referenced here must be the account's most recent lane-2 escrow; `revoke_spender` publishes none (V\_a3), so a target frozen immediately after a revocation must be settled against the checkpoint preceding it. 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}}$$ is **sampled** by the auditor per `DESIGN.md` §2.2, not derived. This is the one operation in the protocol that samples its ephemeral scalar: `DESIGN.md` §5.3 derives $$r\_e$$ from the originator's viewing key, and the originator here is the auditor, which holds no account's viewing key. Then $$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}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{new}}), \sigma^{\text{new}})$$. +4. **Refreshed checkpoint.** $$r\_e^{\text{new}}$$ is **sampled** by the auditor per `DESIGN.md` §2.2, not derived. This is the one operation in the protocol that samples its ephemeral scalar: `DESIGN.md` §5.3 derives $$r\_e$$ from the originator's viewing key, and the originator here is the auditor, which holds no account's viewing key. Then $$R\_e^{\text{new}} = r\_e^{\text{new}} \cdot H$$, $$r\_e^{\text{new}} \neq 0$$, and, with $$(m\_{v,s}^{\text{new}}, m\_{b,s}^{\text{new}}, m\_{r,s}^{\text{new}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{new}}), \sigma^{\text{new}})$$, both $$\tilde{b}\_{\text{aud,s}}^{\text{new}} = (v\_s + v\_r - \alpha) + m\_{b,s}^{\text{new}}$$ and $$\tilde{r}\_{\text{aud,s}}^{\text{new}} = r\_s' + m\_{r,s}^{\text{new}}$$. Re-escrowing lane 2 is what keeps the auditor's standing opening (`DESIGN_cont.md` §8.1) intact across a seizure. +5. **New spendable commitment.** $$C\_{\text{spend}}^{\text{new}} = (v\_s + v\_r - \alpha) \cdot G + r\_s' \cdot H$$. The commitment is a public input rather than a value the contract recomputes, since $$v\_s$$, $$v\_r$$, and $$r\_s'$$ are all private witnesses. -**Post-verification.** The contract 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. +**Post-verification.** The contract stores the prover-supplied $$C\_{\text{spend}}^{\text{new}}$$, 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}}, \tilde{r}\_{\text{aud,s}}^{\text{new}}, R\_e^{\text{new}}, \sigma^{\text{new}})$$ so the auditor sees the new checkpoint and its blinding. **Anti-replay.** The contract 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 contract-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_cont.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. +Detailed encoding and the two-phase isolate-then-settle entry-point sequencing are deferred to a follow-up revision of this document. --- diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index bf76cfe4c..a2456163e 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -409,7 +409,7 @@ Because $$\sigma$$ is published in the event and $$vk$$ is held by the originato **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_spender` (§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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender 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 contract's enumeration of channels in §13 satisfies the domain-distinctness condition. +**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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender escrow), so masks across channels are independent under the PRF assumption on Poseidon (§3.2). The auditor-side $$dvk\_i$$ escrow (S14, $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$) is the one derivation that does *not* open a channel of its own -- it reuses the S\_a2 shared scalar -- so its separation from the sender-auditor sponge rests on §2.5 *Mode exclusivity* rather than on a distinct shared scalar; [DESIGN_cont.md](./DESIGN_cont.md) §8.5 argues it. 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 contract's enumeration of channels in §13 satisfies the domain-distinctness condition. ### 5.4 Anti-Poisoning Constraint @@ -696,7 +696,7 @@ The owner locks funds from their spendable balance into a per-spender escrow. Th | 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) | -| S14 | $$\text{dvk\\\_cipher\\\_aud} = \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i) + dvk\_i$$ (auditor-side escrow of the delegation viewing key over the S\_a2 shared scalar, Section 7.11; one Poseidon, no new scalar multiplication) | +| S14 | $$\text{dvk\\\_cipher\\\_aud} = \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i) + dvk\_i$$ (auditor-side escrow of the delegation viewing key over the S\_a2 shared scalar, Section 8.5; one Poseidon, no new scalar multiplication) | | S\_a1 | $$R\_e = r\_e \cdot H$$ (ephemeral key for auditor ECDH) | | S\_a2 | $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (owner-auditor ECDH shared scalar, §2.4) | | S\_a3 | $$(m\_v, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (owner-auditor channel masks) | @@ -749,7 +749,7 @@ The spender transfers from the owner's escrowed allowance to a recipient. | O\_a6 | $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ (owner-auditor channel masks) | | O\_a7 | $$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}$$ (owner-auditor encrypted transfer amount) | | O\_a8 | $$\tilde{a}\_{\text{aud,s}} = (v\_a - v\_{\text{transfer}}) + m\_{a,s}$$ (owner-auditor encrypted post-transfer allowance) | -| O\_a9 | $$\text{dvk\\\_cipher\\\_aud} = dvk\_i + m\_{r,s}$$ (owner-auditor re-escrow of the delegation viewing key, Section 7.11; $$dvk\_i$$ is already a witness by O2, so this is one field addition) | +| O\_a9 | $$\text{dvk\\\_cipher\\\_aud} = dvk\_i + m\_{r,s}$$ (owner-auditor re-escrow of the delegation viewing key, Section 8.5; $$dvk\_i$$ is already a witness by O2, so this is one field addition) | **Public inputs (25 fields):** @@ -834,6 +834,8 @@ The spender decrypts using $$sk\_{\text{op}}$$. The `set_spender` proof enforces 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. +The same proof escrows $$dvk\_i$$ a second time, to the owner's *auditor* rather than to the spender (S14), under its own domain tag and over the auditor shared scalar. That construction, its decryption path, and why it is a single-output pad rather than a sponge lane are specified in [DESIGN_cont.md](./DESIGN_cont.md) §8.5. + ### 7.12 Expiry and Revert Safety Allowance randomness includes `allowance_salt` to prevent deterministic-randomness reuse after reverted transactions. Delegation storage, expiry, and revocation semantics are specified in §6.2. diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index a1685ce89..d39313089 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -56,10 +56,11 @@ The capability is bounded in three ways: **Sender-auditor opening capability.** The lane-2 escrow hands the sender-auditor the *blinding* of the account's post-operation spendable balance directly, without $$vk\_A$$: together with the value in $$\tilde{b}\_{\text{aud,s}}$$ it is a full Pedersen opening of $$C\_{\text{spend}}'$$. It is available at exactly the three checkpoint operations that escrow lane 2 -- withdrawal (W\_a5), outgoing transfer (T\_a9), and `set_spender` (S\_a6) -- and is likewise bounded: - **Forward-only**, on the same grounds as the recipient side. -- **Event-scoped, not standing.** `revoke_spender` escrows no blinding (V\_a3 stays two-lane, DESIGN §7.9), so a revoke leaves the auditor with the post-reclaim *value* alone and no opening. -- **Lapsed by an inbound transfer.** An escrowed blinding survives a merge only when every inflow it folds in carried $$r = 0$$ -- that is, deposits only (Section 7.3), whose amounts are public, so the auditor can still open the post-merge $$C\_{\text{spend}}$$. A single inbound confidential transfer or spender-transfer folds in an $$r\_{\text{transfer}}$$ the sender-auditor does not hold, and the opening lapses until the next checkpoint. +- **Maintained across merges.** An account binds a single `auditor_id` (Section 6.1), so the key that decrypts the lane-2 escrow is the same key that decrypts the recipient channel of every inbound flow to that account. Merge adds both the values and the blindings ($$v\_{\text{spend}}' = v\_s + v\_r$$, $$r\_{\text{spend}}' = r\_s + r\_r$$, Section 7.4), and the auditor holds each addend: $$(v\_{\text{transfer},i}, r\_{\text{transfer},i})$$ from the recipient-channel reconstruction above, and $$(a\_j, 0)$$ from the public deposits. It therefore carries the escrowed opening forward through every merge by the same addition the contract performs, rather than losing it at one. +- **Not renewed by `revoke_spender`.** V\_a3 stays two-lane (DESIGN §7.9), so a revoke rewrites $$C\_{\text{spend}}$$ under a blinding the auditor never receives and leaves it with the post-reclaim *value* alone. The opening is re-acquired at the next checkpoint (W\_a5, T\_a9, S\_a6). +- **Rotation-scoped.** A newly activated key cannot decrypt escrows published under the previous one (Section 8.3). An auditor that carries its accumulated opening across the rotation keeps it; one that bootstraps from the new key alone re-acquires it at the next checkpoint. -Neither auditor therefore holds a standing opening of $$C\_{\text{spend}}$$; each holds one only as of a named event. +The recipient-side reconstruction is therefore event-scoped, while the sender-side escrow gives the account's auditor a **standing** opening of $$C\_{\text{spend}}$$, held continuously from its first checkpoint under the active key. These bounded openings are what enable 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, while the sender-auditor is the seize-enabling party for the spendable-balance side via $$\tilde{b}\_{\text{aud,s}}$$ and $$\tilde{r}\_{\text{aud,s}}$$. @@ -110,7 +111,15 @@ where $$s\_{a,r}$$, $$s\_{a,s}$$, and $$\sigma\_a$$ are recovered from the event The auditor tracks each allowance's current value through the per-event ciphertexts produced at every state-changing operation: `set_spender` 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_spender` reveals the reclaimed amount (Section 8.2). -**Allowance opening.** The owner's auditor also receives $$dvk\_i$$ itself: under $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ at `set_spender` (S14, DESIGN §7.11) and in lane 2 at every spender transfer (O\_a9, §8.4). Since the allowance blinding is $$r\_a = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a)$$ and $$\sigma\_a$$ is published in the event, the auditor reconstructs the full Pedersen opening of $$C\_a$$ at each of those events -- unlike the spendable side, with no merge to lapse it, since a delegation's only state transitions are the events themselves. +**Allowance opening.** The owner's auditor also receives $$dvk\_i$$ itself: under $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ at `set_spender` (S14, §8.5 *Auditor-side delegation-key escrow*) and in lane 2 at every spender transfer (O\_a9, §8.4). Since the allowance blinding is $$r\_a = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a)$$, and the salt that opens the commitment written by each event is published in that event -- $$\sigma\_a$$ on `SetSpender`, $$\sigma\_a'$$ on `SpenderTransfer` (§11.2) -- the auditor reconstructs the full Pedersen opening of $$C\_a$$ at each of those events, with no on-chain read. Unlike the spendable side there is no merge to fold in, since a delegation's only state transitions are the events themselves. + +**Auditor-side delegation-key escrow.** At `set_spender` the owner escrows $$dvk\_i$$ to its own auditor as well as to the spender: + +$$\text{dvk\\\_cipher\\\_aud} = dvk\_i + \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i)$$ + +reusing the S\_a2 shared scalar $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ rather than opening a new ECDH channel, so it costs one Poseidon and no scalar multiplication (DESIGN S14). The auditor recovers $$s\_{a,s}$$ from $$k\_{\text{aud,s}}$$ and the event's $$R\_e$$, and $$\text{op}\_i$$ from the event's `spender` topic, then subtracts. + +A single-output pad rather than a sponge lane, because lane 2 of this channel is already taken by the spendable blinding (S\_a6). Its separation from that channel is DESIGN §2.5 *Mode exclusivity*: $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ is a single-output tag and $$\delta\_{\text{aud\\\_s}}$$ a multi-lane one, so the two never produce the same field element from the same $$(s\_{a,s}, \cdot)$$. $$\text{op}\_i$$ is a per-$$(owner, spender)$$ constant that separates pads across delegations, not a nonce: the pad's freshness rests entirely on $$s\_{a,s}$$, hence on $$r\_e$$, hence on the salt (DESIGN §5.3, §9.6). Re-delegating to the same spender under a reused salt would republish a byte-identical ciphertext. **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. @@ -167,7 +176,9 @@ This invariant is maintained by: Two properties of that last capability deserve stating plainly. It is **retroactive**: the derivation is deterministic in $$(vk, \sigma)$$ and $$\sigma$$ is published, so a compromise today opens every transfer the account ever originated. And it **reaches outside the account's own state**: the commitments it opens were added to *recipients'* `receiving_commitment` values. The transfer *amounts* were already inferable from $$vk$$ alone, by differencing consecutive balance checkpoints and netting the inbound credits and merges between them, so amount visibility is not what changes; the openings are, and an opening is a self-verifying artifact that its holder can hand to any third party. This is why $$vk$$ cannot be treated as a safely shareable read-only credential ([SDK.md](./SDK.md) §13): a counterparty that needs visibility into an account's outbound transfers receives per-event disclosure proofs bound to it and to a nonce ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7), not the key. -**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 spender) from accounts that used the compromised key, but cannot construct openings of any commitment. If a recipient's auditor key is compromised, the attacker acquires the opening capability of Section 8.1 over every incoming transfer to accounts that used the compromised key, with the scope bounded there (forward-only, receiving-side only, reset by merge). 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. +**Auditor key compromise.** An account binds one key for both channels (Section 6.1), so a compromise is best read per account rather than per role. For every account that used the compromised key, the attacker acquires: all amounts and balance checkpoints; the standing opening of $$C\_{\text{spend}}$$ from the first post-activation checkpoint, maintained across merges exactly as Section 8.1 describes; the opening of $$C\_{\text{receive}}$$ between merges; and every $$dvk\_i$$, hence the opening of every $$C\_a$$ (Section 8.5). It cannot recover viewing keys, read data from before the key was active, or authorize any spending. After key rotation, new operations are protected by the new key. + +As with $$vk$$ above, what the openings add over the amounts is that an opening is a self-verifying artifact its holder can hand to any third party. An auditor key is therefore custodied at the same level as a viewing key, not at the level of a read-only amount feed ([SDK.md](./SDK.md) §13). ### 9.5 State Recovery @@ -221,10 +232,10 @@ pub enum CircuitType { | Circuit | What it proves | |:---|:---| | `Register` | Spending key well-formedness; contract-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})$$) | -| `SpenderTransfer` | 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; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | -| `SetSpender` | 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; contract-bound via $$vk$$ derivation | +| `Withdraw` | Balance sufficiency; new spendable commitment with deterministic randomness; encrypted balance scalar; sender-auditor ECDH ciphertexts (balance checkpoint + lane-2 escrow of the new spendable blinding); 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 + lane-2 escrow of the new spendable blinding); deterministic randomness for new sender balance; encrypted balance scalar; sender key ownership; range validity (balance $$\in [0, 2^{127})$$, amount $$\in [0, 2^{127})$$) | +| `SpenderTransfer` | 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 + lane-2 escrow of $$dvk\_i$$); deterministic randomness for new allowance; encrypted allowance scalar; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | +| `SetSpender` | Balance split; $$dvk\_i$$ derivation; ECDH escrow of $$dvk\_i$$ to the spender and to the owner's auditor; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint + lane-2 escrow of the new spendable blinding); owner key ownership; contract-bound via $$vk$$ derivation | | `RevokeSpender` | 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; contract-bound via $$vk$$ derivation | ### 10.3 Circuit Cost Analysis @@ -424,10 +435,10 @@ This table is authoritative: every entry is exactly the set of prover-supplied p | 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{transfer}}$$, $$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{transfer}}$$, $$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_spender` | $$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` | +| `withdraw` | $$C\_{\text{spend}}'$$, $$\tilde{b}$$, $$R\_e$$, $$\sigma$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, `proof` | +| `confidential_transfer` | $$C\_{\text{spend}}'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, `proof` | +| `confidential_transfer_from` | $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\text{dvk\\\_cipher\\\_aud}$$, `proof` | +| `set_spender` | $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$, `proof` | | `revoke_spender` | $$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 contract loads it from the `(from, spender)` 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_spender`, by contrast, has no prior delegation entry to load from, so its $$\sigma\_a$$ is prover-supplied and bound by S6. @@ -463,10 +474,10 @@ Each state-modifying operation emits a structured event. Events carry the data n | `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}}$$ | -| `SpenderTransfer` | `spender`, `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}}$$ | -| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ | +| `Withdraw` | `from`, `to`, `amount`, $$R\_e$$, $$\sigma$$, $$\tilde{b}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | +| `SpenderTransfer` | `spender`, `from`, `to`, $$R\_e$$, $$\tilde{v}$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | +| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\sigma\_a$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | | `RevokeSpender` | `account`, `spender`, $$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. @@ -525,7 +536,7 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the | $$\delta\_{\text{eph}}$$ | 14 | Deterministic ephemeral-scalar derivation (§5.3) | | $$\delta\_{\text{disc\\\_bind}}$$ | 15 | Disclosure ciphertext to the disclosure recipient, aggregate variant ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §10) | | $$\delta\_{\text{disc}}$$ | 16 | Disclosure ciphertext to the disclosure recipient ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4) | -| $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Delegation key escrow to the owner's auditor (S14, §7.11, §8.5) | +| $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Delegation key escrow to the owner's auditor (S14, §8.5) | This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16; it takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ across two channels so that every row of §5.3's *Why reusing $$r\_e$$ is safe* keeps a distinct tag. diff --git a/packages/tokens/src/confidential/docs/OVERVIEW.md b/packages/tokens/src/confidential/docs/OVERVIEW.md index 8c65079f2..2ce4ce9fb 100644 --- a/packages/tokens/src/confidential/docs/OVERVIEW.md +++ b/packages/tokens/src/confidential/docs/OVERVIEW.md @@ -129,7 +129,7 @@ The system supports **real-time auditing** via a dual-auditor model. Each accoun | Sender's post-transfer balance | No | Yes | | Withdrawal amount | n/a | Yes (publicly visible) | | Post-withdrawal balance | n/a | Yes | -| Post-operation spendable blinding factor | No | Yes, at withdrawal, outgoing transfer, and spender setup (enables opening the sender's spendable balance as of that event; not at spender revocation, and lapses at the next merge that folds in an inbound transfer) | +| Post-operation spendable blinding factor | No | Yes, at withdrawal, outgoing transfer, and spender setup (enables opening the sender's spendable balance from that event onward, since the same key also receives every inbound transfer's blinding; not renewed at spender revocation) | | Spender escrow / reclaim amount | n/a | Yes (owner's auditor) | | Post-transfer spender allowance | No (for spender transfers) | Yes (owner's auditor) | | Post-escrow / post-reclaim balance | n/a | Yes (owner's auditor), at spender setup and revocation respectively | @@ -145,7 +145,7 @@ Each auditor decrypts its ciphertexts by running the channel sponge (recipient-a - **Dual-auditor ciphertexts.** The ciphertexts each operation produces are enforced by its zero-knowledge proof, so they cannot be omitted or malformed, and no extra action is needed from users. - **Per-account scope.** Auditing one account reveals nothing about any other account. - **Recipient-side opening capability.** The recipient's auditor holds the per-transfer Pedersen blinding $r_{\text{transfer}}$, hence the full Pedersen opening of the recipient's receiving balance between merges, which is what enables the seizure/clawback flow specified in `COMPLIANCE.md` §5; the capability and its bounds are specified in `DESIGN_cont.md` §8.1. -- **Sender-side opening capability.** The sender's auditor holds the opening of the account's spendable balance as of each withdrawal, outgoing transfer, and spender setup, because those operations also encrypt the post-operation blinding factor to it. The capability is event-scoped, not standing, and its bounds are specified in `DESIGN_cont.md` §8.1. +- **Sender-side opening capability.** The sender's auditor holds the opening of the account's spendable balance as of each withdrawal, outgoing transfer, and spender setup, because those operations also encrypt the post-operation blinding factor to it. Because one key serves both of an account's auditor channels, the capability survives merges rather than expiring with the event; its bounds are specified in `DESIGN_cont.md` §8.1. - **Seamless auditor rotation.** When an auditor key is rotated, the new key immediately receives ciphertexts on subsequent operations. For the sender's auditor, the balance checkpoint at the next owner-initiated proof operation (transfer, withdrawal, set spender, or revoke spender) provides the current balance with no event replay or bootstrapping. - **Spender visibility.** The owner's auditor sees spender transfer amounts and post-transfer allowances via the same dual-auditor mechanism, and additionally sees escrowed and reclaimed amounts at `set_spender` and `revoke_spender`. - **Viewing vs. spending separation.** A viewing key cannot move or spend funds. Spending requires the separate spending key, which is never shared. diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index 2044df81c..38db9149f 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -461,7 +461,7 @@ A spender MUST NOT be able to reach the owner's spendable balance through any in ## 11. Auditor Client -An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. +An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read -- including the allowance salts, which the delegation events publish ($$\sigma_a$$ on `SetSpender`, $$\sigma_a'$$ on `SpenderTransfer`), so $$r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$$ and the opening of $$C_a$$ follow from the event alone (DESIGN_cont.md §8.5). For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. The two channels differ in what they yield (DESIGN_cont.md §8.1): @@ -476,9 +476,9 @@ An implementation MUST squeeze the sender / owner channel three-wide and MUST NO **Cross-channel agreement.** Where an auditor holds the key for both parties, the amount decrypts independently on each channel and the circuit constrains both to the same value, so the two MUST agree. An implementation SHOULD perform this comparison and treat disagreement as evidence that $$k$$ is not the auditor key for both parties of that event. -**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). The lane-2 opening of the sender channel is forward-only and **event-scoped**: it opens the spendable commitment as of the checkpoint that escrowed it, and lapses at the next merge that folds in an inbound confidential transfer (DESIGN_cont.md §8.1 *Sender-auditor opening capability*). An implementation MUST NOT present a stored lane-2 blinding as the current spendable blinding once such a merge has been observed. Rotation itself needs no replay on the sender side: the next owner-initiated proof operation publishes a fresh balance checkpoint under the new key. +**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). The lane-2 opening of the sender channel is forward-only and **standing**: it opens the spendable commitment as of the checkpoint that escrowed it and stays valid through merges, because one account key serves both channels (DESIGN_cont.md §8.1 *Sender-auditor opening capability*). Maintaining it is the client's job, not the protocol's: an implementation MUST add the lane-0 amount and lane-1 $$r_{\text{transfer}}$$ of every inbound `Transfer` and `SpenderTransfer` to its stored $$(v, r)$$, and MUST treat each `Deposit` as $$(\text{amount}, 0)$$ (DESIGN.md §7.3). It MUST invalidate the stored opening on a `RevokeSpender` event, which rewrites the spendable commitment under a blinding no channel carries, until the next lane-2 escrow. Across a key rotation an implementation MUST either carry the accumulated opening forward from the prior key or treat the account as unopened until the next checkpoint under the new key; the value side needs no replay either way, since the next owner-initiated proof publishes a fresh balance checkpoint. -An auditor facade MUST NOT be able to construct a spending witness. It MUST NOT be able to open a spendable balance past a merge that folded in an inbound confidential transfer, since that merge adds an $$r_{\text{transfer}}$$ the sender channel never carries; a deposits-only merge leaves the escrowed blinding valid, deposits contributing $$r = 0$$ (DESIGN.md §7.3). +An auditor facade MUST NOT be able to construct a spending witness. It *can*, however, open the spendable balance past a merge -- the inbound $$r_{\text{transfer}}$$ reaches the same key on the recipient channel -- so a facade that exposes openings exposes them for the account's whole history under the active key, not for isolated events. --- diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 05b0d0a78..fd81e44db 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -74,7 +74,7 @@ This document reuses the notation, key hierarchy, and commitment scheme from DES - $$sk\_A$$, $$vk\_A$$, $$\text{PVK}\_A$$: an account's spending key, viewing key, and public viewing key (DESIGN.md §4). - $$\text{addr\\\_f}$$: the contract's compressed address Field $$\text{address\\\_to\\\_field}(\text{contract})$$, bound into $$vk$$ derivation (DESIGN.md §2.7, §4.2). Stored once at construction in the contract's instance storage (DESIGN.md §3.5). - $$K\_{\text{aud,s}}$$, $$K\_{\text{aud,r}}$$, $$aud\_{sk}$$: the sender-side and recipient-side auditor Grumpkin public keys, and an auditor's secret key (DESIGN_cont.md §8.1, §8.3). Each account selects an `auditor_id` at registration; the same `auditor_id` may resolve to either role depending on the transfer's direction. -- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. +- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, the post-transfer allowance salt $$\sigma\_a'$$ is emitted alongside it, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. - $$H$$: the Grumpkin Pedersen generator used uniformly for key derivation and ECDH (DESIGN.md §2.3, §2.4). ### 2.1 Disclosure Recipient @@ -97,7 +97,7 @@ The disclosure layer inherits the protocol's threat model (DESIGN.md §3.2) and **Holder is the prover for D-recipient and D-sender variants.** The holder is trusted only to produce *correct* proofs about events they choose to disclose. The holder is *not* trusted to be complete: they may withhold events. Recipients that require completeness must obtain it from the auditor (DESIGN_cont.md §8) or from out-of-band evidence. -**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen opening of every $$C\_a$$ (DESIGN_cont.md §8.5) and, at each checkpoint event that escrows lane 2, of the account's $$C\_{\text{spend}}$$ as of that event (DESIGN_cont.md §8.1 *Sender-auditor opening capability*, §8.2). The latter is event-scoped, not standing: `revoke_spender` escrows no blinding, and a merge that folds in an inbound confidential transfer lapses the escrowed opening until the next checkpoint. The D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. +**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen opening of every $$C\_a$$ (DESIGN_cont.md §8.5) and, at each checkpoint event that escrows lane 2, of the account's $$C\_{\text{spend}}$$ as of that event (DESIGN_cont.md §8.1 *Sender-auditor opening capability*, §8.2). The latter is standing rather than event-scoped: the account's single auditor key also decrypts every inbound blinding, so the opening carries across merges, and only `revoke_spender` -- which escrows no blinding -- suspends it until the next checkpoint. The D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. **Disclosure recipient is honest-but-curious.** The recipient correctly verifies proofs and decrypts ciphertexts addressed to their key. The recipient may attempt to replay or rebroadcast proofs; nonce binding prevents reuse against other parties. @@ -174,7 +174,7 @@ Given a bundle for $$(P\_R, \nu)$$ that this verifier previously issued, the rec 1. **Resolve the event.** Look up $$\text{ref}\_E$$ via the indexer or via direct RPC of the transaction. The lookup MUST return exactly one event whose contract address equals the deployed confidential-token contract. Extract the event's payload fields verbatim: - For `Transfer`: `from`, `to`, $$R\_e$$, $$\sigma$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). - - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN_cont.md §11.2). + - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN_cont.md §11.2). Any other event type, or a `circuit_id` whose constraints reference a field the event does not carry, is rejected here. @@ -337,7 +337,7 @@ D-auditor does not bind to an account record; the auditor key already binds the **Verifier flow.** Follow §5.3 with `circuit_id = D-auditor` (or the chosen balance / randomness variant). Step 2 is skipped — no $$\text{PVK}\_A$$ lookup is needed. Step 3 resolves $$K\_{\text{aud}}$$ at the event's ledger: $$K\_{\text{aud,r}}$$ from the `auditor_id` on the event's `to` account when disclosing the recipient-side channel, or $$K\_{\text{aud,s}}$$ from the `auditor_id` on the `from` account when disclosing the sender-side channel. `from` is the funds' owner in both `Transfer` and `SpenderTransfer`, since the sender-auditor channel always tracks the owner (DESIGN.md §7.8). -**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone — a later merge over an inbound transfer lapses it (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. +**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. --- @@ -432,7 +432,7 @@ The confidential-token contract requires no new state-modifying entry points to |:---|:---|:---| | `confidential_balance(account) -> ConfidentialAccount` | Verifier extracts $$\text{PVK}\_A$$ (and $$\text{PVK}\_B$$ for D-sender, $$C\_{\text{spend}}$$ for D-balance) from the returned `ConfidentialAccount` tuple | Already exposed (DESIGN_cont.md §11.3); the struct carries every field this layer reads, so no narrower accessor is required | | Auditor contract's key lookup for `auditor_id` | Verifier looks up $$K\_{\text{aud,r}}$$ or $$K\_{\text{aud,s}}$$ | Already exposed (DESIGN_cont.md §8.3). The auditor contract MAY maintain a sequence of versioned keys per `auditor_id` with activation ledgers; the verifier MUST select the version whose activation ledger is the largest value not exceeding the disclosed event's ledger (DESIGN_cont.md §8.3, *Auditor's off-chain obligation*). | -| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ / $$\text{dvk\\\_cipher\\\_aud}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$. | +| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ / $$\text{dvk\\\_cipher\\\_aud}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, additionally emits $$\sigma\_a'$$, and uses $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$. | | Instance storage: $$\text{addr\\\_f}$$ | D-recipient, D-sender, and D-balance bind $$vk$$ derivation to the contract via $$\text{addr\\\_f}$$ | Computed once at construction (DESIGN.md §3.5); the verifier reproduces it from the contract address using the encoding in DESIGN.md §2.7 | These are the only on-chain dependencies. Disclosure proofs are otherwise self-contained off-chain artifacts. From 5309fe32e3b4029c2c7a12f7b92f6fdb26b9be67 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 14:20:04 +0200 Subject: [PATCH 07/12] docs(confidential): state what actually separates tag 17 from tag 11 --- packages/tokens/src/confidential/docs/DESIGN_cont.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index d39313089..7ca7a6e33 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -538,7 +538,7 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the | $$\delta\_{\text{disc}}$$ | 16 | Disclosure ciphertext to the disclosure recipient ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4) | | $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Delegation key escrow to the owner's auditor (S14, §8.5) | -This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16; it takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ across two channels so that every row of §5.3's *Why reusing $$r\_e$$ is safe* keeps a distinct tag. +This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16. It takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ because the two escrows key off different shared scalars ($$Y\_{\text{op}}$$ vs $$K\_{\text{aud,s}}$$), which is the distinct-domain leg of §5.3's *Why reusing $$r\_e$$ is safe*. It shares its shared scalar with $$\delta\_{\text{aud\\\_s}}$$ instead, and is separated from it by mode exclusivity rather than by a distinct scalar (§8.5). **Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag ever squeezed three-wide, and the widths it is read at agree on their shared lanes (§2.5 *Mode exclusivity*). 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. From 65663f30f8282101132305177e4b11f852343e3e Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 16:36:54 +0200 Subject: [PATCH 08/12] feat(confidential): escrow r_a to the owner's auditor instead of dvk_i MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit S14 and O_a9 escrowed the delegation viewing key, which is deterministic and permanent per (owner, spender) and survives revoke-then-re-delegate, so one leaked ciphertext opened every allowance state for that pair, past and future. Both now escrow the blinding of the allowance commitment the operation writes -- r_a at set_spender, r_a' at spender_transfer -- which is what the auditor actually needs to open C_a. Tag 17 becomes ESCROWED_ALLOWANCE_BLINDING_AUDITOR (value unchanged) and lane 2 is uniformly a commitment blinding, never a key. Zero ACIR delta: both blindings were already constrained witnesses in scope. Payload and event fields are renamed, so this is an SDK-visible ABI change. DESIGN_cont §8.5's forward-only and rotation-remediation claims were false and are replaced with an event-scoped claim: a rotated-in key holds no opening until the delegation's next state change, and a retired key that already holds one can still carry it through a public homomorphic fold. --- packages/tokens/src/confidential/CLAUDE.md | 2 +- .../src/confidential/circuits/CLAUDE.md | 2 + .../src/confidential/circuits/lib/src/lib.nr | 61 +++-- .../confidential/circuits/lib/src/tests.nr | 34 +-- .../testdata/encrypt_esc_allow_r_auditor.json | 15 ++ .../lib/testdata/encrypt_esc_dvk_auditor.json | 15 -- .../lib/testdata/sponge_squeeze_3.json | 2 +- .../circuits/set_spender/src/main.nr | 58 +++-- .../circuits/set_spender/src/tests.nr | 239 +++++++++++++++--- .../circuits/spender_transfer/src/main.nr | 54 +++- .../circuits/spender_transfer/src/tests.nr | 233 ++++++++++++++--- .../circuits/vks/set_spender.vk.json | 2 +- .../circuits/vks/spender_transfer.vk.json | 2 +- .../src/confidential/compliance/test.rs | 4 +- .../tokens/src/confidential/docs/DESIGN.md | 18 +- .../src/confidential/docs/DESIGN_cont.md | 43 ++-- .../tokens/src/confidential/docs/INDEXER.md | 10 + .../tokens/src/confidential/docs/OVERVIEW.md | 5 +- packages/tokens/src/confidential/docs/SDK.md | 10 +- .../confidential/docs/SELECTIVE_DISCLOSURE.md | 2 +- packages/tokens/src/confidential/mod.rs | 16 +- packages/tokens/src/confidential/storage.rs | 32 +-- packages/tokens/src/confidential/test.rs | 178 ++++++++++++- 23 files changed, 797 insertions(+), 240 deletions(-) create mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_allow_r_auditor.json delete mode 100644 packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json diff --git a/packages/tokens/src/confidential/CLAUDE.md b/packages/tokens/src/confidential/CLAUDE.md index 6c84e551b..5b3b3db38 100644 --- a/packages/tokens/src/confidential/CLAUDE.md +++ b/packages/tokens/src/confidential/CLAUDE.md @@ -71,5 +71,5 @@ The tags are a cross-language wire contract. `DESIGN_cont.md` §13 is their only - **Cite, do not restate.** Every drift bug in the recent history came from a second copy of something. When tempted to summarise a neighbouring section, write `§N` instead. - **Match the file's math style, not a global one.** `DESIGN*.md` and `SELECTIVE_DISCLOSURE.md` use `$$…$$` with backslash-escaped subscripts (`$$\mathbb{F}\_r$$`); `SDK.md` uses `$$…$$` unescaped; `OVERVIEW.md` uses single `$…$`; `INDEXER.md` and `COMPLIANCE.md` use backticked ASCII and no LaTeX. -- **Symbols are a maintained namespace.** `sk`/`vk`/`dvk_i`/`PVK`/`Y`; `r_e` and `R_e = r_e·H`; `σ` (operation salt) is distinct from `σ_a` (per-delegation allowance salt); tilde means ciphertext; `C_spend` / `C_receive` / `C_transfer` / `C_a`. An audit finding once required renaming the `tx` subscript to `transfer` across the whole module. +- **Symbols are a maintained namespace.** `sk`/`vk`/`dvk_i`/`PVK`/`Y`; `r_e` and `R_e = r_e·H`; `σ` (operation salt) is distinct from `σ_a` (per-delegation allowance salt); tilde means ciphertext; `C_spend` / `C_receive` / `C_transfer` / `C_a`; `r_a` is `C_a`'s blinding and `r_a'` the post-transfer one. An audit finding once required renaming the `tx` subscript to `transfer` across the whole module. - Prose is full-width — no hard wrapping. One paragraph or list item per line. diff --git a/packages/tokens/src/confidential/circuits/CLAUDE.md b/packages/tokens/src/confidential/circuits/CLAUDE.md index 9ebffb14a..3add5f8c0 100644 --- a/packages/tokens/src/confidential/circuits/CLAUDE.md +++ b/packages/tokens/src/confidential/circuits/CLAUDE.md @@ -40,6 +40,8 @@ Sponge parameters, the canonical lane assignment, and the mode-exclusivity rule `AUDITOR_SENDER` is squeezed three-wide by every circuit that escrows lane 2 and two-wide only by RevokeSpender (V_a3); `AUDITOR_RECIPIENT` is always two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. +Lane 2 carries **the blinding of a commitment the operation writes, never a key** — `r'` on W_a5 / T_a9 / S_a6, `r_a'` on O_a9. Tag 17 (`ESCROWED_ALLOWANCE_BLINDING_AUDITOR`) is the same idea off-sponge: SetSpender's lane 2 is already taken, so S14 escrows `r_a` under a single-output pad. Do not escrow `dvk_i` here: it is permanent per `(owner, spender)` and survives revoke-then-re-delegate, so one leaked ciphertext would open every allowance state for that pair, past and future (`../docs/DESIGN_cont.md` §8.5). + ECDH must absorb both `S.x` and `S.y`; x-only extraction collapses `P` and `-P`. The `G` and `H` generators are hardcoded but provenance-checked at runtime by the `print_generators` test; re-extract with `nargo test print_generators --show-output` rather than editing the constants by hand. diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index 6f41e32cc..ac52212fa 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -90,7 +90,7 @@ global POSEIDON2_IV_BASE: Field = 18446744073709551616; // 2^64 /// | `delta_aud_s` | 11 | `AUDITOR_SENDER` | /// | `delta_aud_r` | 12 | `AUDITOR_RECIPIENT` | /// | `delta_ecdh` | 13 | `ECDH_SHARED_SECRET` | -/// | `delta_esc_dvk_aud` | 17 | `ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR` | +/// | `delta_esc_allow_r_aud` | 17 | `ESCROWED_ALLOWANCE_BLINDING_AUDITOR` | /// /// Tags 14-16 are deliberately absent here: 14 is derived off-circuit and /// 15-16 belong to the off-chain disclosure layer. @@ -134,10 +134,10 @@ pub mod domain { /// Sender or owner-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). **Three lanes**, and the only tag squeezed /// three-wide: lane 0 is the amount mask (where applicable), lane 1 the - /// balance/allowance checkpoint mask, lane 2 the secret-escrow slot -- - /// the new spendable blinding on checkpoints, the delegation viewing key - /// on spender transfers. Constraints W_a3 / T_a6 / S_a3 / O_a6 - /// (`delta_aud_s`). + /// balance/allowance checkpoint mask, lane 2 the commitment-blinding + /// escrow slot -- the new spendable blinding on checkpoints, the new + /// allowance blinding on spender transfers. Constraints W_a3 / T_a6 / + /// S_a3 / O_a6 (`delta_aud_s`). /// /// RevokeSpender (V_a3) still reads two lanes, so the opening the auditor /// holds is not renewed there and lapses until the next checkpoint; the @@ -157,9 +157,9 @@ pub mod domain { /// `s = Poseidon2(ECDH_SHARED_SECRET, S.x, S.y)`. /// Section 2.4 (`delta_ecdh`). See [`ecdh`]. pub global ECDH_SHARED_SECRET: Field = 13; - /// Auditor-side delegation-key escrow mask (owner-auditor ECDH): - /// `Poseidon2(ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, s_a_s, op_i)`. - /// Constraint S14, Section 8.5 (`delta_esc_dvk_aud`). + /// Auditor-side allowance-blinding escrow mask (owner-auditor ECDH): + /// `Poseidon2(ESCROWED_ALLOWANCE_BLINDING_AUDITOR, s_a_s, op_i)`. + /// Constraint S14, Section 8.5 (`delta_esc_allow_r_aud`). /// /// A separate tag from `ESCROWED_DELEGATION_VIEWING_KEY` = 10 even though /// reuse would be secure -- the two shared scalars differ, since `Y_op` @@ -168,10 +168,13 @@ pub mod domain { /// a fresh tag keeps that argument intact. /// /// Unlike the tags above it, this one shares its shared scalar with - /// `AUDITOR_SENDER` (both key off `s_a_s`). What separates them is Section - /// 2.5 mode exclusivity: this tag is single-output, tag 11 is multi-lane, - /// so the two never yield the same field element. - pub global ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR: Field = 17; + /// `AUDITOR_SENDER` (both key off `s_a_s`). What separates them is that + /// they are different tags: 17 is absorbed here, 11 there, and the tag is + /// the first absorbed element, so no `(s_a_s, x)` pair yields the same + /// field element under both. Section 2.5 mode exclusivity is a separate, + /// per-tag hygiene rule that each of the two satisfies on its own -- this + /// one is only ever single-output, tag 11 only ever multi-lane. + pub global ESCROWED_ALLOWANCE_BLINDING_AUDITOR: Field = 17; } // ################## CORE PRIMITIVES ################## @@ -335,8 +338,8 @@ 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-side delegation-key escrow at `set_spender`. Constraint S14. -/// `dvk_cipher_aud = dvk + Poseidon2(ESC_DVK_AUD, s_a_s, op_i)` where +/// Auditor-side allowance-blinding escrow at `set_spender`. Constraint S14. +/// `r_a_tilde_aud_s = r_a + Poseidon2(ESC_ALLOW_R_AUD, s_a_s, op_i)` where /// `s_a_s = ecdh(r_e, K_aud_s)` is the *owner-auditor* shared scalar. /// /// A single-output Poseidon rather than a third sponge lane, because at @@ -347,11 +350,15 @@ pub fn encrypt_esc_dvk(dvk: Field, s: Field, op_i: Field) -> Field { /// per operation because `r_e` is, and `r_e` is fresh only because the salt is /// (Section 5.3, Section 9.6). /// -/// This is what gives the auditor the opening of `C_a`: with `dvk_i` it can -/// derive `r_a = derive_allow_r(dvk_i, sigma_a)` and open the allowance -/// commitment at the delegation's every state transition (Section 8.5). -pub fn encrypt_esc_dvk_auditor(dvk: Field, s_a_s: Field, op_i: Field) -> Field { - dvk + poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, [s_a_s, op_i]) +/// This is what gives the auditor the opening of `C_a`: `r_a` is the blinding +/// S6 derived and S7 committed to, so with the emitted `a_tilde_aud_s` the +/// auditor holds both halves of the allowance opening for the state this +/// operation writes (Section 8.5). The escrowed value is a per-state blinding, +/// not the key that generates every state: an auditor key that never saw this +/// event cannot derive it, and one that saw only this event learns nothing +/// about any other. +pub fn encrypt_esc_allow_r_auditor(r_a: Field, s_a_s: Field, op_i: Field) -> Field { + r_a + poseidon_with_domain(domain::ESCROWED_ALLOWANCE_BLINDING_AUDITOR, [s_a_s, op_i]) } // ################## AUDITOR-CHANNEL SPONGE ################## @@ -384,10 +391,11 @@ pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { /// /// Implements `SpongeSqueeze_3(delta, s, sigma)` from Section 2.5. Lane 0 is /// the amount mask and lane 1 the balance / allowance / randomness mask, as in -/// [`sponge_squeeze_2`]. **Lane 2 is the sender-auditor secret-escrow slot**: -/// it carries the new spendable blinding on the three checkpoint operations -/// (W_a5 / T_a9 / S_a6) and the delegation viewing key on spender transfers -/// (O_a9). +/// [`sponge_squeeze_2`]. **Lane 2 is the sender-auditor blinding-escrow +/// slot**: it carries the blinding of a commitment the operation writes, and +/// never a key -- the new spendable blinding on the three checkpoint +/// operations (W_a5 / T_a9 / S_a6), the new allowance blinding on spender +/// transfers (O_a9). /// /// `sponge_squeeze_3(d, s, sigma)[0..2] == sponge_squeeze_2(d, s, sigma)`, by /// construction: the absorb fits one rate-3 block, so all three outputs are @@ -396,9 +404,10 @@ pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { /// value and costs no extra constraints. The capacity lane at `state[3]` stays /// untouched. /// -/// Two different plaintexts share lane 2 across operations, which is not pad -/// reuse: the pad is fixed by `(s, sigma)` or `(s, sigma_a)`, both fresh per -/// operation, so no pad ever masks two values. +/// The blindings lane 2 carries still belong to different commitment roles -- +/// spendable on the checkpoints, allowance on spender transfers -- so the +/// uniformity is documentary, not a claim that a recipient can substitute one +/// for the other. Each is scoped to the operation that emitted it. pub fn sponge_squeeze_3(d: Field, s: Field, sigma: Field) -> [Field; 3] { let iv: Field = 3 * POSEIDON2_IV_BASE; let state = poseidon2_permutation([d, s, sigma, iv], 4); diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index cbe9ef368..96d0cded5 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -1,7 +1,7 @@ use crate::{ assert_on_curve_non_identity, commit, derive_allow_r, derive_spend_r, derive_transfer_blind, domain, dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_amount, - encrypt_balance, encrypt_esc_dvk, encrypt_esc_dvk_auditor, G, H, poseidon_with_domain, + encrypt_balance, encrypt_esc_allow_r_auditor, encrypt_esc_dvk, G, H, poseidon_with_domain, pvk_from_vk, scalar_mul, sponge, sponge_squeeze_2, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -264,20 +264,22 @@ fn sponge_squeeze_3_outputs_distinct() { } #[test] -fn esc_dvk_auditor_is_domain_separated_from_the_spender_escrow() { - // Tags 10 and 17 escrow the same plaintext (dvk_i) to two different - // parties. Reusing one tag for both would be secure in practice -- the - // shared scalars differ because Y_op and K_aud_s are independent points - // -- but the separation is what keeps Section 5.3's r_e-reuse argument - // whole, so it must be observable here. - let dvk: Field = 0xd0d0; +fn esc_allow_r_auditor_is_domain_separated_from_the_spender_escrow() { + // Tags 10 and 17 escrow different plaintexts -- the delegation key to the + // spender, the allowance blinding to the owner's auditor -- but both are + // additive pads over the same shape, so a shared tag would let a + // coincidence of inputs collapse one onto the other. Reusing one tag for + // both would be secure in practice, since the shared scalars differ (Y_op + // and K_aud_s are independent points), but the separation is what keeps + // Section 5.3's r_e-reuse argument whole, so it must be observable here. + let x: Field = 0xd0d0; let s: Field = 0x12345; let op_i: Field = 0xabcd; - assert(encrypt_esc_dvk(dvk, s, op_i) != encrypt_esc_dvk_auditor(dvk, s, op_i)); + assert(encrypt_esc_dvk(x, s, op_i) != encrypt_esc_allow_r_auditor(x, s, op_i)); // Both are additive one-time-pad encryptions and must round-trip. - let mask = poseidon_with_domain(domain::ESCROWED_DELEGATION_VIEWING_KEY_AUDITOR, [s, op_i]); - assert(encrypt_esc_dvk_auditor(dvk, s, op_i) - mask == dvk); + let mask = poseidon_with_domain(domain::ESCROWED_ALLOWANCE_BLINDING_AUDITOR, [s, op_i]); + assert(encrypt_esc_allow_r_auditor(x, s, op_i) - mask == x); } #[test] @@ -410,9 +412,9 @@ fn print_fixtures() { println(f"sponge_squeeze_3_AUDITOR_SENDER_1 = {ss3_s1}"); println(f"sponge_squeeze_3_AUDITOR_SENDER_2 = {ss3_s2}"); - // encrypt_esc_dvk_auditor(dvk, s, op_i) - let esc_aud = encrypt_esc_dvk_auditor(dvk, s_ecdh, op_i); - println(f"encrypt_esc_dvk_auditor = {esc_aud}"); + // encrypt_esc_allow_r_auditor(r_a, s, op_i) + let esc_aud = encrypt_esc_allow_r_auditor(r_a, s_ecdh, op_i); + println(f"encrypt_esc_allow_r_auditor = {esc_aud}"); // poseidon_with_domain(VK, [sk, addr_f]) -- direct funnel access, equals vk_from_sk let pwd = poseidon_with_domain(domain::VIEWING_KEY, [sk, addr_f]); @@ -521,8 +523,8 @@ fn fixtures_match_testdata() { assert(ss3_r[1] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1); assert( - encrypt_esc_dvk_auditor(dvk, s_ecdh, op_i) - == 0x0c3e3560bd6c62d83e90b731e346ac30eb4b87bee1a3218b2bb71b0645e4f09c, + encrypt_esc_allow_r_auditor(derive_allow_r(dvk, sigma_a), s_ecdh, op_i) + == 0x2d97a37cb8d103eb9231907e292a954e22e7fa56165a6b3b9f639252557bf7a2, ); // poseidon_with_domain funnel is the same operation that backs vk_from_sk. diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_allow_r_auditor.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_allow_r_auditor.json new file mode 100644 index 000000000..ef158fcd8 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_allow_r_auditor.json @@ -0,0 +1,15 @@ +{ + "primitive": "encrypt_esc_allow_r_auditor", + "design_doc_refs": ["Section 8.5", "Constraint S14"], + "description": "Auditor-side allowance-blinding escrow: r_a_tilde_aud_s = r_a + Poseidon2(ESC_ALLOW_R_AUD, s_a_s, op_i) where s_a_s = ecdh(r_e, K_aud_s) is the owner-auditor shared scalar. Same additive-pad shape as encrypt_esc_dvk but a different plaintext under a different tag (17 rather than 10) and a different shared scalar: this one hands the owner's auditor the blinding of the allowance commitment the operation writes, the other hands the spender the delegation viewing key. Paired with the emitted v_tilde_aud_s it is the full opening of C_a for that one state -- not the key from which every state's opening derives.", + "vectors": [ + { + "inputs": { + "r_a": "0x0afda20e062a277afc0c86e31ea22597ee2c094b1b9cd08b8d8a2a212a8af1ee", + "s_a_s": "0x12345", + "op_i": "0xabcd" + }, + "output": "0x2d97a37cb8d103eb9231907e292a954e22e7fa56165a6b3b9f639252557bf7a2" + } + ] +} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json b/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json deleted file mode 100644 index 5bdd648a2..000000000 --- a/packages/tokens/src/confidential/circuits/lib/testdata/encrypt_esc_dvk_auditor.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "primitive": "encrypt_esc_dvk_auditor", - "design_doc_refs": ["Section 7.11", "Constraint S14"], - "description": "Auditor-side delegation-key escrow: dvk_cipher_aud = dvk + Poseidon2(ESC_DVK_AUD, s_a_s, op_i) where s_a_s = ecdh(r_e, K_aud_s) is the owner-auditor shared scalar. Same plaintext and same shape as encrypt_esc_dvk, under a distinct tag (17 rather than 10) and a distinct shared scalar: this one goes to the owner's auditor, the other to the spender. Holding dvk_i is what lets the auditor derive r_a = derive_allow_r(dvk_i, sigma_a) and therefore open the allowance commitment C_a.", - "vectors": [ - { - "inputs": { - "dvk": "0x1a088264ebf7269160bbf34d5a3f94d7dec37efc609ef76c5dbfa8690af3eae9", - "s_a_s": "0x12345", - "op_i": "0xabcd" - }, - "output": "0x0c3e3560bd6c62d83e90b731e346ac30eb4b87bee1a3218b2bb71b0645e4f09c" - } - ] -} diff --git a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json index 21903ec79..1b8c773dc 100644 --- a/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json +++ b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json @@ -1,7 +1,7 @@ { "primitive": "sponge_squeeze_3", "design_doc_refs": ["Section 2.5", "Constraint W_a3", "Constraint T_a6", "Constraint S_a3", "Constraint O_a6"], - "description": "Poseidon2 sponge with three squeezes (one absorb of (d, s, sigma), permute, output (state[0], state[1], state[2])). Same absorb and same permutation as sponge_squeeze_2, so lanes 0 and 1 are bit-identical to that primitive's outputs on the same inputs -- verify that agreement before trusting an implementation. Canonical lane order: 0 = amount mask, 1 = balance / allowance / per-transfer randomness mask, 2 = the sender-auditor secret-escrow slot, which carries the new spendable blinding on the three checkpoint operations (Withdraw, Transfer sender side, SetSpender) and the delegation viewing key on spender transfers. Only the sender-auditor channel, d = AUDITOR_SENDER (11), is squeezed three-wide; the recipient-auditor channel, d = AUDITOR_RECIPIENT (12), stays at two lanes.", + "description": "Poseidon2 sponge with three squeezes (one absorb of (d, s, sigma), permute, output (state[0], state[1], state[2])). Same absorb and same permutation as sponge_squeeze_2, so lanes 0 and 1 are bit-identical to that primitive's outputs on the same inputs -- verify that agreement before trusting an implementation. Canonical lane order: 0 = amount mask, 1 = balance / allowance / per-transfer randomness mask, 2 = the sender-auditor blinding-escrow slot, which carries the blinding of a commitment the operation writes -- the new spendable blinding on the three checkpoint operations (Withdraw, Transfer sender side, SetSpender), the new allowance blinding on spender transfers -- and never a key. Only the sender-auditor channel, d = AUDITOR_SENDER (11), is squeezed three-wide; the recipient-auditor channel, d = AUDITOR_RECIPIENT (12), stays at two lanes.", "vectors": [ { "inputs": { "d": "0x0b", "s": "0x12345", "sigma": "0x01" }, diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr index 718a93ac7..71fc7bad4 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr @@ -1,7 +1,7 @@ use stellar_confidential_lib::{ H, assert_on_curve_non_identity, commit, derive_allow_r, derive_spend_r, domain, - dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_balance, encrypt_esc_dvk, - encrypt_esc_dvk_auditor, scalar_mul, sponge_squeeze_3, vk_from_sk, + dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_balance, encrypt_esc_allow_r_auditor, + encrypt_esc_dvk, scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -59,18 +59,21 @@ mod tests; // + Poseidon2(delta_esc_dvk, s_esc, op_i) // Masked dvk_i for the // spender. -// S14 dvk_cipher_aud = dvk_i -// + Poseidon2(delta_esc_dvk_aud, s_{a,s}, op_i) +// S14 r_a_tilde_aud_s = r_a +// + Poseidon2(delta_esc_allow_r_aud, s_{a,s}, op_i) // Auditor-side escrow -// of dvk_i. Hands the +// of the allowance +// blinding S6 derived +// and S7 committed to. +// With v_tilde_aud_s +// (S_a4) this hands the // owner's auditor the -// full opening of C_a: -// with dvk_i it derives -// r_a and opens the -// allowance at every -// state transition of -// the delegation -// (Section 8.5). +// opening of the C_a +// this operation writes +// (Section 8.5) -- +// one state, not the +// key that generates +// every state. // A separate single- // output Poseidon // rather than a sponge @@ -78,7 +81,9 @@ mod tests; // of this channel is // taken by S_a6. Costs // one Poseidon and zero -// scalar muls. +// scalar muls; r_a is +// already a witness +// (S6). // Unconditional -- there // is no delegation that // can exist without it. @@ -171,11 +176,15 @@ mod tests; // 24 r_tilde_aud_s r_tilde_aud_s Prover-supplied owner-auditor // escrow of the new spendable // blinding; emitted. -// 25 dvk_cipher_aud dvk_cipher_aud Prover-supplied owner-auditor -// escrow of dvk_i; emitted. -// Distinct from -// escrowed_dvk_cipher, which is -// the spender-side escrow. +// 25 r_a_tilde_aud_s r_a_tilde_aud_s Prover-supplied owner-auditor +// escrow of the new allowance +// blinding; emitted. Distinct +// from r_tilde_aud_s (index +// 24), which escrows the +// spendable blinding, and from +// escrowed_dvk_cipher, which +// hands the spender the +// delegation key. // // Private witnesses // ----------------- @@ -217,7 +226,7 @@ fn main( v_tilde_aud_s: pub Field, b_tilde_aud_s: pub Field, r_tilde_aud_s: pub Field, - dvk_cipher_aud: pub Field, + r_a_tilde_aud_s: pub Field, ) { // S13 -- runs first so the r_e = 0 attack is rejected before any // scalar mul against it could quietly produce the identity (R_e, S_{a,s}, @@ -301,7 +310,8 @@ fn main( // S_a2 (owner-auditor shared-secret scalar) let s_a_s = ecdh(r_e, k_aud_s); - // S_a3 (owner-channel masks: amount, balance, then the secret-escrow slot) + // S_a3 (owner-channel masks: amount, balance, then the blinding-escrow + // slot) let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma); // S_a4 @@ -314,7 +324,9 @@ fn main( assert(r_new + m_s[2] == r_tilde_aud_s); // S14 -- reuses s_a_s from S_a2, so no new scalar multiplication. Runs - // after the auditor block so the shared scalar is already in scope. - let dvk_cipher_aud_derived = encrypt_esc_dvk_auditor(dvk, s_a_s, op_i); - assert(dvk_cipher_aud_derived == dvk_cipher_aud); + // after the auditor block so the shared scalar is already in scope. The + // escrowed value is S6's r_a, the very blinding S7 committed under, so the + // auditor's opening matches the C_a this proof writes on-chain. + let r_a_tilde_aud_s_derived = encrypt_esc_allow_r_auditor(r_a, s_a_s, op_i); + assert(r_a_tilde_aud_s_derived == r_a_tilde_aud_s); } diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr index 18ecfd3cb..f8e2194c0 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr @@ -1,7 +1,7 @@ use crate::main; use stellar_confidential_lib::{ commit, derive_allow_r, derive_spend_r, domain, dvk_from_vk_op, ecdh, encrypt_allowance, - encrypt_balance, encrypt_esc_dvk, encrypt_esc_dvk_auditor, H, scalar_mul, + encrypt_balance, encrypt_esc_allow_r_auditor, encrypt_esc_dvk, H, scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -84,11 +84,13 @@ global ESCROWED_DVK_CIPHER: Field = 0x1ef89e3bd93ec1afed649d150e9d0ff5df72002759e260cb66b06604a4d7ebe0; global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c4397; -// Lane-2 secret-escrow slot (S_a6) and the auditor-side dvk escrow (S14). +// Lane-2 spendable-blinding escrow (S_a6) and the auditor-side allowance- +// blinding escrow (S14). Both are blindings of commitments this operation +// writes -- C_spend' and C_a respectively. global R_TILDE_AUD_S: Field = 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; -global DVK_CIPHER_AUD: Field = - 0x18dc4199771af4fe37c1993b95c8d3188fc0a58080185b4939d359555a6799e1; +global R_A_TILDE_AUD_S: Field = + 0x28991b7df68413693ffe3fd0d47b0793e65f3c6edb6907a7a152fe80d158927f; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c07c; @@ -119,7 +121,7 @@ fn print_fixtures() { let v_tilde_aud_s = V_A + m_s[0]; let b_tilde_aud_s = V_NEW + m_s[1]; let r_tilde_aud_s = r_new + m_s[2]; - let dvk_cipher_aud = encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I); + let r_a_tilde_aud_s = encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I); let yopx = y_op.x; let yopy = y_op.y; @@ -147,7 +149,7 @@ fn print_fixtures() { println(f"V_TILDE_AUD_S = {v_tilde_aud_s}"); println(f"B_TILDE_AUD_S = {b_tilde_aud_s}"); println(f"R_TILDE_AUD_S = {r_tilde_aud_s}"); - println(f"DVK_CIPHER_AUD = {dvk_cipher_aud}"); + println(f"R_A_TILDE_AUD_S = {r_a_tilde_aud_s}"); } #[test] @@ -182,6 +184,7 @@ fn set_spender_fixtures_match_lib() { fn set_spender_auditor_fixtures_match_lib() { let vk = vk_from_sk(SK, ADDR_F); let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); let y_op = scalar_mul(SK_OP, H); let s_esc = ecdh(R_E, y_op); let escrowed_dvk_cipher = encrypt_esc_dvk(dvk, s_esc, OP_I); @@ -199,7 +202,7 @@ fn set_spender_auditor_fixtures_match_lib() { assert(V_A + m_s[0] == V_TILDE_AUD_S); assert(V_NEW + m_s[1] == B_TILDE_AUD_S); assert(r_new + m_s[2] == R_TILDE_AUD_S); - assert(encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I) == DVK_CIPHER_AUD); + assert(encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I) == R_A_TILDE_AUD_S); } // `run_main` accepts every public input as a parameter -- nothing baked in. @@ -238,7 +241,7 @@ fn run_main( v_tilde_aud_s_in: Field, b_tilde_aud_s_in: Field, r_tilde_aud_s_in: Field, - dvk_cipher_aud_in: Field, + r_a_tilde_aud_s_in: Field, ) { main( sk_in, @@ -271,7 +274,7 @@ fn run_main( v_tilde_aud_s_in, b_tilde_aud_s_in, r_tilde_aud_s_in, - dvk_cipher_aud_in, + r_a_tilde_aud_s_in, ); } @@ -307,7 +310,7 @@ fn run_fixture() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -367,7 +370,7 @@ fn full_allowance_escrow() { V + m_s[0], 0 + m_s[1], r_new + m_s[2], - encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I), + encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -422,7 +425,7 @@ fn rejects_under_funded_escrow() { v_a_too_large + m_s[0], (V - v_a_too_large) + m_s[1], r_new + m_s[2], - encrypt_esc_dvk_auditor(dvk, s_a_s, OP_I), + encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -476,7 +479,7 @@ fn rejects_v_out_of_range() { V_TILDE_AUD_S, v_new_huge + m_s[1], r_new + m_s[2], - encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), + encrypt_esc_allow_r_auditor(derive_allow_r(dvk_from_vk_op(vk, OP_I), SIGMA_A), s_a_s, OP_I), ); } @@ -535,7 +538,7 @@ fn rejects_v_a_out_of_range() { v_a_huge + m_s[0], v_new_huge + m_s[1], r_new + m_s[2], - encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), + encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -589,7 +592,7 @@ fn rejects_wrong_sk() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -639,7 +642,7 @@ fn rejects_wrong_balance_opening() { V_TILDE_AUD_S, v_new_bad + m_s[1], r_new + m_s[2], - encrypt_esc_dvk_auditor(dvk_from_vk_op(vk, OP_I), s_a_s, OP_I), + encrypt_esc_allow_r_auditor(derive_allow_r(dvk_from_vk_op(vk, OP_I), SIGMA_A), s_a_s, OP_I), ); } @@ -691,7 +694,7 @@ fn rejects_wrong_addr_f() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -734,7 +737,7 @@ fn rejects_wrong_op_i() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -772,7 +775,7 @@ fn rejects_tampered_c_a() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -811,7 +814,7 @@ fn rejects_tampered_a_tilde() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -849,7 +852,7 @@ fn rejects_tampered_c_spend_new() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -887,7 +890,7 @@ fn rejects_tampered_b_tilde() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -928,7 +931,7 @@ fn rejects_tampered_escrowed_dvk_r_x() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -967,7 +970,7 @@ fn rejects_tampered_escrowed_dvk_cipher() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1010,7 +1013,7 @@ fn rejects_wrong_y_op() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1065,7 +1068,7 @@ fn rejects_r_e_zero() { V_A + m_s_zero[0], V_NEW + m_s_zero[1], r_new + m_s_zero[2], - encrypt_esc_dvk_auditor(dvk, s_a_s_zero, OP_I), + encrypt_esc_allow_r_auditor(derive_allow_r(dvk, SIGMA_A), s_a_s_zero, OP_I), ); } @@ -1120,7 +1123,7 @@ fn rejects_wrong_r_e() { V_A + m_s_bad[0], V_NEW + m_s_bad[1], r_new + m_s_bad[2], - encrypt_esc_dvk_auditor(dvk, s_a_s_bad, OP_I), + encrypt_esc_allow_r_auditor(derive_allow_r(dvk, SIGMA_A), s_a_s_bad, OP_I), ); } @@ -1159,7 +1162,7 @@ fn rejects_off_curve_k_aud_s() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1198,7 +1201,7 @@ fn rejects_identity_k_aud_s() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1237,7 +1240,7 @@ fn rejects_wrong_k_aud_s() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1275,7 +1278,7 @@ fn rejects_tampered_v_tilde_aud_s() { V_TILDE_AUD_S + 1, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } @@ -1313,15 +1316,175 @@ fn rejects_tampered_b_tilde_aud_s() { V_TILDE_AUD_S, B_TILDE_AUD_S + 1, R_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_r_a_tilde_aud_s() { + // r_a_tilde_aud_s mutated by +1: S14 fails because the auditor-side escrow + // is pinned to r_a + Poseidon(delta_esc_allow_r_aud, s_a_s, op_i), which + // the prover's claim no longer matches. + run_main( + SK, + V, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + ADDR_F, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_A_X, + C_A_Y, + R_E_X, + ESCROWED_DVK_CIPHER, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S + 1, + ); +} + +// Salt and ephemeral reuse (characterization) +// ------------------------------------------- +// Nothing in this circuit constrains `sigma`, `sigma_a`, or `r_e` to be fresh +// -- all three are prover-supplied and unchecked. The tests below pin what +// actually happens under reuse rather than asserting that it is prevented. If +// a future change starts enforcing uniqueness, they have to be updated +// deliberately. + +#[test] +fn repeated_sigma_a_republishes_the_same_allowance_state() { + // Two delegations to the same spender under the same sigma_a and the same + // v_a write a byte-identical C_a and a_tilde, even with a different owner + // balance, a different sigma, and a fresh r_e -- r_a and a_tilde are pure + // functions of (dvk_i, sigma_a) and v_a. An observer learns the allowance + // was reset to a value it has seen before, and no more: equal ciphertexts + // carry no differential. + let v_2: Field = 900; + let r_2: Field = 43; + let v_new_2: Field = v_2 - V_A; + let sigma_2: Field = 0x11; + let r_e_2: Field = R_E + 7; + + let vk = vk_from_sk(SK, ADDR_F); + let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); + let c_a = commit(V_A, r_a); + let a_tilde = encrypt_allowance(V_A, dvk, SIGMA_A); + assert(c_a.x == C_A_X); + assert(c_a.y == C_A_Y); + assert(a_tilde == A_TILDE); + + let c_spend_2 = commit(v_2, r_2); + let r_new_2 = derive_spend_r(vk, sigma_2); + let c_spend_new_2 = commit(v_new_2, r_new_2); + let b_tilde_2 = encrypt_balance(v_new_2, vk, sigma_2); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let escrowed_dvk_cipher_2 = encrypt_esc_dvk(dvk, ecdh(r_e_2, y_op), OP_I); + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s_2 = ecdh(r_e_2, k_aud_s); + let m_s_2 = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_2, sigma_2); + let r_e_2_pt = scalar_mul(r_e_2, H); + + run_main( + SK, + v_2, + r_2, + V_A, + r_e_2, + c_spend_2.x, + c_spend_2.y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + ADDR_F, + K_AUD_S_X, + K_AUD_S_Y, + c_spend_new_2.x, + c_spend_new_2.y, + C_A_X, + C_A_Y, + r_e_2_pt.x, + escrowed_dvk_cipher_2, + b_tilde_2, + A_TILDE, + sigma_2, + SIGMA_A, + r_e_2_pt.x, + r_e_2_pt.y, + V_A + m_s_2[0], + v_new_2 + m_s_2[1], + r_new_2 + m_s_2[2], + encrypt_esc_allow_r_auditor(r_a, s_a_s_2, OP_I), ); } +#[test] +fn repeated_r_e_and_sigma_repeat_every_owner_channel_pad() { + // Reusing (r_e, sigma) repeats s_a_s and therefore all three sponge lanes + // and S14's pad. Re-escrowing the same plaintexts under them republishes + // byte-identical ciphertexts -- no differential. + let vk = vk_from_sk(SK, ADDR_F); + let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); + let r_new = derive_spend_r(vk, SIGMA); + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA); + assert(V_A + m_s[0] == V_TILDE_AUD_S); + assert(V_NEW + m_s[1] == B_TILDE_AUD_S); + assert(r_new + m_s[2] == R_TILDE_AUD_S); + assert(encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I) == R_A_TILDE_AUD_S); + + // Change a plaintext under the repeated pad and the difference survives + // the encryption in the clear. This is the leak the per-operation + // freshness of r_e exists to prevent (Section 5.3); the circuit cannot + // catch it, because a reused r_e is a perfectly well-formed witness. + let v_a_2: Field = V_A + 25; + assert((v_a_2 + m_s[0]) - V_TILDE_AUD_S == 25); + + // The two allowance escrows do NOT leak the same way, and not because the + // pad is any fresher: r_a is a Poseidon image, so a change of v_a or + // sigma_a moves it unpredictably rather than by a known offset. What + // repeats byte-for-byte is only the case above, where nothing changed. + let r_a_2 = derive_allow_r(dvk, SIGMA_A + 1); + assert(encrypt_esc_allow_r_auditor(r_a_2, s_a_s, OP_I) != R_A_TILDE_AUD_S); +} + #[test(should_fail)] -fn rejects_tampered_dvk_cipher_aud() { - // dvk_cipher_aud mutated by +1: S14 fails because the auditor-side escrow - // is pinned to dvk_i + Poseidon(delta_esc_dvk_aud, s_a_s, op_i), which the - // prover's claim no longer matches. +fn rejects_escrow_of_blinding_for_the_wrong_sigma_a() { + // The escrowed blinding must be the one S6 derived and S7 committed under, + // not merely *a* well-formed allowance blinding for this delegation. Here + // the prover escrows derive_allow_r(dvk, SIGMA_A + 1) -- correctly padded, + // correctly tagged, derived from the right dvk_i -- while every other + // public stays canonical at SIGMA_A. S14 fires. + // + // This is what makes the escrow useful: the auditor opens the on-chain C_a + // with what it decrypts. An escrow that floated free of sigma_a would let + // a prover hand the auditor an opening of some other allowance state, and + // the mismatch would only surface when the auditor checked the commitment. + let vk = vk_from_sk(SK, ADDR_F); + let dvk = dvk_from_vk_op(vk, OP_I); + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s = ecdh(R_E, k_aud_s); run_main( SK, V, @@ -1353,7 +1516,7 @@ fn rejects_tampered_dvk_cipher_aud() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S, - DVK_CIPHER_AUD + 1, + encrypt_esc_allow_r_auditor(derive_allow_r(dvk, SIGMA_A + 1), s_a_s, OP_I), ); } @@ -1393,6 +1556,6 @@ fn rejects_tampered_r_tilde_aud_s() { V_TILDE_AUD_S, B_TILDE_AUD_S, R_TILDE_AUD_S + 1, - DVK_CIPHER_AUD, + R_A_TILDE_AUD_S, ); } diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr index aad3acb5a..443be493c 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr @@ -96,13 +96,38 @@ mod tests; // encrypted // post-transfer // allowance. -// O_a9 dvk_cipher_aud = dvk_i + m_r_s Owner-auditor -// re-escrow of the -// delegation viewing -// key. dvk_i is -// already a witness -// (O2), so this is +// O_a9 r_tilde_aud_s = r_a' + m_r_s Owner-auditor +// escrow of the NEW +// allowance blinding +// -- O10's r_a', +// which O11 commits +// under. Already a +// witness, so this is // one field addition. +// With a_tilde_aud_s +// (O_a8) it hands the +// owner's auditor the +// opening of the C_a' +// this operation +// writes. +// Note the apparent +// mismatch: the +// lane-2 pad keys off +// the CONSUMED salt +// sigma_a (O_a6), +// while the escrowed +// value is derived +// from the WRITTEN +// salt sigma_a'. That +// is sound -- the pad +// only needs to be +// fresh and unknown +// to anyone but the +// auditor, and +// (s_{a,s}, sigma_a) +// is both, since +// s_{a,s} is fresh +// per operation. // Buys one-operation // convergence for a // live delegation @@ -197,8 +222,9 @@ mod tests; // 23 a_tilde_aud_s a_tilde_aud_s Prover-supplied owner-auditor // encrypted post-transfer // allowance; emitted. -// 24 dvk_cipher_aud dvk_cipher_aud Prover-supplied owner-auditor -// re-escrow of dvk_i; emitted. +// 24 r_tilde_aud_s r_tilde_aud_s Prover-supplied owner-auditor +// escrow of the new allowance +// blinding r_a'; emitted. // // Private witnesses // ----------------- @@ -242,7 +268,7 @@ fn main( r_tilde_aud_r: pub Field, v_tilde_aud_s: pub Field, a_tilde_aud_s: pub Field, - dvk_cipher_aud: pub Field, + r_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. @@ -332,7 +358,7 @@ fn main( let s_a_s = ecdh(r_e, k_aud_s); // O_a6 (owner-channel masks: amount, post-transfer allowance, then the - // secret-escrow slot) + // blinding-escrow slot) let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma_a); // O_a7 @@ -341,7 +367,9 @@ fn main( // O_a8 assert(v_a_new + m_s[1] == a_tilde_aud_s); - // O_a9 -- dvk_i is already a private witness (O2), so this is one field - // addition: zero scalar muls, no new domain tag. - assert(dvk_i + m_s[2] == dvk_cipher_aud); + // O_a9 -- r_a_new is already constrained by O10 and committed by O11, so + // this is one field addition: zero scalar muls, no new domain tag. The + // escrowed blinding is the one the operation WRITES, not the one it + // consumes, so the auditor's opening matches the C_a' left on-chain. + assert(r_a_new + m_s[2] == r_tilde_aud_s); } diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr index a0f8d490e..2f6d62ce6 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -70,10 +70,12 @@ global R_TILDE_AUD_R: Field = 0x16cb31d142cf3005f8bd95ab3f59c3bda0da0de7e1d2ac16b4d038548a02b364; global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c42cf; -// Lane-2 secret-escrow slot (O_a9): the delegation viewing key, re-escrowed -// to the owner's auditor on every spender transfer. -global DVK_CIPHER_AUD: Field = - 0x10a1e8766310b1369a48b22e12f01b8610041bf25bc1807d01812491f9702f57; +// Lane-2 blinding-escrow slot (O_a9): the NEW allowance blinding r_a', the +// one O11 commits C_a' under, escrowed to the owner's auditor on every +// spender transfer. Note it is derived from SIGMA_A_NEW while the lane-2 pad +// keys off SIGMA_A -- see main.nr's O_a9 note. +global R_TILDE_AUD_S: Field = + 0x0382ab29c6554066d495613fdec52421d70c3bf49d0faaf25552266222af073a; global A_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; @@ -110,7 +112,7 @@ fn print_fixtures() { let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); let v_tilde_aud_s = V_TRANSFER + m_s[0]; let a_tilde_aud_s = V_A_NEW + m_s[1]; - let dvk_cipher_aud = DVK_I + m_s[2]; + let r_tilde_aud_s = r_a_new + m_s[2]; let yopx = y_op.x; let yopy = y_op.y; @@ -151,7 +153,7 @@ fn print_fixtures() { 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}"); - println(f"DVK_CIPHER_AUD = {dvk_cipher_aud}"); + println(f"R_TILDE_AUD_S = {r_tilde_aud_s}"); } #[test] @@ -206,7 +208,7 @@ fn spender_transfer_auditor_fixtures_match_lib() { assert(r_transfer + m_r[1] == R_TILDE_AUD_R); assert(V_TRANSFER + m_s[0] == V_TILDE_AUD_S); assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); - assert(DVK_I + m_s[2] == DVK_CIPHER_AUD); + assert(derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s[2] == R_TILDE_AUD_S); } // `run_main` accepts every public input as a parameter -- nothing baked in. @@ -245,7 +247,7 @@ fn run_main( r_tilde_aud_r_in: Field, v_tilde_aud_s_in: Field, a_tilde_aud_s_in: Field, - dvk_cipher_aud_in: Field, + r_tilde_aud_s_in: Field, ) { main( sk_op_in, @@ -278,7 +280,7 @@ fn run_main( r_tilde_aud_r_in, v_tilde_aud_s_in, a_tilde_aud_s_in, - dvk_cipher_aud_in, + r_tilde_aud_s_in, ); } @@ -314,7 +316,7 @@ fn run_fixture() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -385,7 +387,7 @@ fn second_transfer_against_same_allowance() { r_transfer + m_r[1], v_transfer_2 + m_s[0], v_a_new_2 + m_s[1], - DVK_I + m_s[2], + r_a_new_2 + m_s[2], ); } @@ -443,7 +445,7 @@ fn full_allowance_transfer() { r_transfer + m_r[1], V_A + m_s[0], 0 + m_s[1], - DVK_I + m_s[2], + r_a_new + m_s[2], ); } @@ -481,7 +483,7 @@ fn rejects_wrong_sk_op() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -555,7 +557,7 @@ fn rejects_wrong_dvk_against_contract() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -604,7 +606,7 @@ fn rejects_wrong_allowance_opening() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_bad + m_s[1], - DVK_I + m_s[2], + r_a_new + m_s[2], ); } @@ -663,7 +665,7 @@ fn rejects_v_a_new_out_of_range() { r_transfer + m_r[1], v_transfer_too_large + m_s[0], (V_A - v_transfer_too_large) + m_s[1], - DVK_I + m_s[2], + r_a_new + m_s[2], ); } @@ -718,7 +720,7 @@ fn rejects_v_a_out_of_range() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_huge + m_s[1], - DVK_I + m_s[2], + r_a_new + m_s[2], ); } @@ -777,7 +779,7 @@ fn rejects_v_transfer_out_of_range() { r_transfer + m_r[1], v_transfer_huge + m_s[0], v_a_new_huge + m_s[1], - DVK_I + m_s[2], + r_a_new + m_s[2], ); } @@ -818,7 +820,7 @@ fn rejects_wrong_recipient_pvk() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -866,7 +868,7 @@ fn rejects_poisoned_c_transfer() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -904,7 +906,7 @@ fn rejects_tampered_v_tilde() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -950,7 +952,7 @@ fn rejects_tampered_a_tilde_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -988,7 +990,7 @@ fn rejects_tampered_c_a_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1036,7 +1038,7 @@ fn rejects_sigma_a_new_equal_to_sigma_a() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1096,7 +1098,7 @@ fn rejects_r_e_zero() { r_transfer_zero + m_r_zero[1], V_TRANSFER + m_s_zero[0], V_A_NEW + m_s_zero[1], - DVK_I + m_s_zero[2], + derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s_zero[2], ); } @@ -1153,7 +1155,7 @@ fn rejects_wrong_r_e() { r_transfer_bad + m_r_bad[1], V_TRANSFER + m_s_bad[0], V_A_NEW + m_s_bad[1], - DVK_I + m_s_bad[2], + derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s_bad[2], ); } @@ -1192,7 +1194,7 @@ fn rejects_off_curve_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1231,7 +1233,7 @@ fn rejects_identity_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1268,7 +1270,7 @@ fn rejects_off_curve_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1305,7 +1307,7 @@ fn rejects_identity_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1343,7 +1345,7 @@ fn rejects_tampered_v_tilde_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1381,7 +1383,7 @@ fn rejects_tampered_r_tilde_aud_r() { R_TILDE_AUD_R + 1, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1419,7 +1421,7 @@ fn rejects_tampered_v_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S + 1, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1457,14 +1459,165 @@ fn rejects_tampered_a_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S + 1, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, + ); +} + +// Salt and ephemeral reuse (characterization) +// ------------------------------------------- +// The only freshness this circuit enforces is `sigma_a' != sigma_a`, and only +// against the immediate predecessor; `r_e` is unchecked. The tests below pin +// what happens under reuse rather than asserting it is prevented. + +#[test] +fn alternating_sigma_a_reproduces_an_earlier_allowance_blinding() { + // A -> B -> A is accepted: the salt-rotation check only rules out the + // state the transfer consumes. Since r_a is a pure function of + // (dvk_i, sigma_a), returning to salt A reproduces salt A's blinding + // exactly -- R_A, the blinding of the very first allowance commitment. The + // consequence is linkability, not unsoundness: if the value also + // coincided, C_a would be byte-identical to a commitment already on-chain. + // Here the value differs, so only the blinding repeats. + let v_transfer_2: Field = 400; + let v_a_new_2: Field = V_A_NEW - v_transfer_2; // 500 + let r_e_2: Field = R_E + 3; + + // Back to SIGMA_A after one hop through SIGMA_A_NEW. + let r_a_new_2 = derive_allow_r(DVK_I, SIGMA_A); + assert(r_a_new_2 == R_A); + + 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 = ecdh(r_e_2, pvk_recipient); + let r_transfer_2 = derive_transfer_blind(s, SIGMA_A_NEW); + let c_transfer_2 = commit(v_transfer_2, r_transfer_2); + let v_tilde_2 = encrypt_amount(v_transfer_2, s, SIGMA_A_NEW); + 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); + let r_e_2_pt = scalar_mul(r_e_2, H); + let s_a_r_2 = ecdh(r_e_2, k_aud_r); + let m_r_2 = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_2, SIGMA_A_NEW); + let s_a_s_2 = ecdh(r_e_2, k_aud_s); + let m_s_2 = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_2, SIGMA_A_NEW); + + run_main( + SK_OP, + DVK_I, + V_A_NEW, + derive_allow_r(DVK_I, SIGMA_A_NEW), + v_transfer_2, + r_e_2, + C_A_NEW_X, + C_A_NEW_Y, + SIGMA_A_NEW, + 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_transfer_2.x, + c_transfer_2.y, + r_e_2_pt.x, + r_e_2_pt.y, + v_tilde_2, + a_tilde_new_2, + SIGMA_A, + v_transfer_2 + m_r_2[0], + r_transfer_2 + m_r_2[1], + v_transfer_2 + m_s_2[0], + v_a_new_2 + m_s_2[1], + r_a_new_2 + m_s_2[2], + ); +} + +#[test] +fn repeated_r_e_and_sigma_a_repeat_every_channel_pad() { + // Reusing (r_e, sigma_a) repeats s, s_{a,r}, and s_{a,s}, and with them + // every mask on all three channels. Re-encrypting the same plaintexts + // under them republishes byte-identical ciphertexts -- no differential. + 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 = ecdh(R_E, pvk_recipient); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, ecdh(R_E, k_aud_r), SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, ecdh(R_E, k_aud_s), SIGMA_A); + assert(encrypt_amount(V_TRANSFER, s, SIGMA_A) == V_TILDE); + assert(V_TRANSFER + m_r[0] == V_TILDE_AUD_R); + assert(V_TRANSFER + m_s[0] == V_TILDE_AUD_S); + assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); + assert(derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s[2] == R_TILDE_AUD_S); + + // Change a plaintext under the repeated pad and the difference survives in + // the clear. Two transfers of different sizes under one (r_e, sigma_a) + // publish v_tilde values whose difference is the amount delta. The circuit + // cannot catch this -- a reused r_e is a well-formed witness -- so it is + // the honest client's obligation (Section 5.3, Section 9.6). + let v_transfer_2: Field = V_TRANSFER + 40; + assert((v_transfer_2 + m_s[0]) - V_TILDE_AUD_S == 40); +} + +#[test(should_fail)] +fn rejects_escrow_of_the_consumed_allowance_blinding() { + // The prover escrows R_A -- the blinding of the allowance this transfer + // CONSUMES -- instead of O10's r_a', the blinding of the allowance it + // writes. Correctly padded under the canonical lane-2 mask, so only O_a9 + // can catch it, and it does. + // + // The distinction matters because the auditor's job is to track live + // state: a_tilde_aud_s (O_a8) already carries the post-transfer value, so + // pairing it with the pre-transfer blinding yields an opening of nothing + // on-chain. The auditor already knew the consumed opening -- it decrypted + // it from the previous operation's event. + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_a_s = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TRANSFER, + 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_TRANSFER_X, + C_TRANSFER_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, + R_A + m_s[2], ); } #[test(should_fail)] -fn rejects_tampered_dvk_cipher_aud() { - // dvk_cipher_aud mutated by +1: O_a9 fails because the lane-2 escrow is - // pinned to dvk_i, which the prover's claim no longer matches. +fn rejects_tampered_r_tilde_aud_s() { + // r_tilde_aud_s mutated by +1: O_a9 fails because the lane-2 escrow is + // pinned to O10's r_a', which the prover's claim no longer matches. run_main( SK_OP, DVK_I, @@ -1496,7 +1649,7 @@ fn rejects_tampered_dvk_cipher_aud() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD + 1, + R_TILDE_AUD_S + 1, ); } @@ -1535,7 +1688,7 @@ fn rejects_wrong_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } @@ -1574,6 +1727,6 @@ fn rejects_wrong_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, - DVK_CIPHER_AUD, + R_TILDE_AUD_S, ); } diff --git a/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json b/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json index 66adbe4ba..04e179c65 100644 --- a/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000002a","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000001a","0x0000000000000000000000000000001ee8326ff6fa4d9eaa698e6140f07f937b","0x0000000000000000000000000000000000106a0a88936a807b905fd25fa6eae5","0x0000000000000000000000000000007c8a46d26563134c136509e706b746f1c4","0x00000000000000000000000000000000000590438230856dc4e29b2b487f82a6","0x0000000000000000000000000000008fda4fa9a21711c3439d8c7bdc8706a35a","0x000000000000000000000000000000000026ee93cae09f66482049b8ab9f18ee","0x000000000000000000000000000000fa6519a25edb2d71cde10a7979794e21ff","0x00000000000000000000000000000000000783403486a30c30781b1336b06de0","0x0000000000000000000000000000002c88f0e429c3aa1b48f7f93509eb30bc9a","0x00000000000000000000000000000000000d417179f4c315c350cc1107b9cfc9","0x000000000000000000000000000000b0b25c47633b74d1a2bf1cd00ffce7b12c","0x000000000000000000000000000000000007ce4d696b72e2d8452d52a7caf9de","0x0000000000000000000000000000002d12248226512f020e334317dae6eaf0c8","0x000000000000000000000000000000000018cccf723238de695bcde25904b27d","0x000000000000000000000000000000f88387a18ddc16904307058f7d7a883e4a","0x00000000000000000000000000000000001d06b3270da56a3607007ee7ad0f1e","0x000000000000000000000000000000ee1a79ad9afc1d02b840e27c5283e74499","0x000000000000000000000000000000000028d1b1924fe7cf03f605e701eccd20","0x000000000000000000000000000000a2e8413b26a238c1274c1c4bc1bf3fa366","0x000000000000000000000000000000000028554b198c6912b267e8a6bf0987da","0x000000000000000000000000000000725ddea9830a058064e593ae1150357563","0x00000000000000000000000000000000000be21052e0ca4bbc01af225ab441ef","0x00000000000000000000000000000039b1667579c4ac66601d7962273b2bd832","0x00000000000000000000000000000000001e82560f75396646bb960ee9ec317b","0x000000000000000000000000000000933174c97d07b3e2e562d13bd83ed809d0","0x00000000000000000000000000000000002c183f1ad7406f0e39afd5ae8a4d66","0x000000000000000000000000000000cf09eb98da45a5b7fd4fc995638326c2a7","0x00000000000000000000000000000000000465c7d5dfc1c170aa4885b2933dfa","0x0000000000000000000000000000004488817604cfb82b60e35ade6c2bfbbde4","0x000000000000000000000000000000000025df503deb0f9d455fd7fdbc35501d","0x0000000000000000000000000000005b0af382eaa78b050823e07021a2596730","0x000000000000000000000000000000000013296f3f21d5be36f6c3de4f7e12bb","0x00000000000000000000000000000013e4981e445ea861b4d191c94874786ced","0x000000000000000000000000000000000009f2a00d23e559fc77b790f178d644","0x0000000000000000000000000000005a04ff9ac166a13efe0bbb42af5a9370a2","0x00000000000000000000000000000000001d488faa8537a3184ed1838cfa8da9","0x0000000000000000000000000000006ebe4f355ba6a3fc1d7e57f657359fcf7e","0x00000000000000000000000000000000001970bb0ec1ae1d702597e0504befc0","0x000000000000000000000000000000ff7fa0375289b0c62312d5c20c6e9b9d82","0x0000000000000000000000000000000000242ff04c2fb35a5b4940794a5e0c9b","0x000000000000000000000000000000e24c5739a33f3016deeb3150ca6cd333ed","0x0000000000000000000000000000000000168f59e1c5afdc0b6f4351e098969d","0x0000000000000000000000000000008fabee483e7aa0b20e40295d2fee6e79da","0x0000000000000000000000000000000000121880c763fa3fd1bdfc39945bf18a","0x000000000000000000000000000000df647f1842d3f41f64ad17f3fef8cad92a","0x00000000000000000000000000000000000baa779152f64c30acd67fb6a96532","0x00000000000000000000000000000040611399572dd13dfc9e5711b25a957117","0x00000000000000000000000000000000000233a10ee44b41e3123d589039619e","0x000000000000000000000000000000ed142ab059b436c2440f0567e5985e038e","0x0000000000000000000000000000000000107423e6071c6594e7960804836a87","0x0000000000000000000000000000008c0b459dfd4818f2ce5d4622d460ac4b3b","0x000000000000000000000000000000000027f96010efb256b54377ea2cf2203b","0x00000000000000000000000000000026c87845540abf9fb61ca692bb9b09cbdf","0x000000000000000000000000000000000025253d1360866f06ffaf41fbbe02cd","0x00000000000000000000000000000007173b04261b3c7cd9d727708f105a9885","0x00000000000000000000000000000000001d7182ce722bde4dfaff0d58d75980","0x0000000000000000000000000000001b1f57f97ae7e7875b0692f61d07eaa2e9","0x000000000000000000000000000000000022e1317bfd6ab50566ca1997c94fe9","0x000000000000000000000000000000332b66feaefc3aaa35d31970310ecbb369","0x000000000000000000000000000000000023a7e369eab70d6081c8c3f5f3bbe9","0x000000000000000000000000000000a248122c42f86368af6a0c74a9a25d5f9c","0x000000000000000000000000000000000000ee8f7378796672556adecf8f6c63","0x000000000000000000000000000000e8c233c18d70dfd6675ae5dd7df5e29271","0x00000000000000000000000000000000002c56c41a07a3006c2830ac414004e7","0x000000000000000000000000000000c675e97962a8230f86a1093d714e9f0f05","0x00000000000000000000000000000000001b125d3316c14b62c532f6b8b2e4fe","0x00000000000000000000000000000089bd1899f66cf8f75b58a2b60a9de48cf7","0x00000000000000000000000000000000002706f62382cc7e17c7e531832ff966","0x000000000000000000000000000000d26a37225805d4e0170474e9e02188fb21","0x00000000000000000000000000000000001e50e6e7ad7be4d8dacbf29e940af4","0x000000000000000000000000000000efba449cb7d241c0dd520375104721726d","0x00000000000000000000000000000000001b095bb696a673bd0c5a116e27b404","0x0000000000000000000000000000006dba6a66fc696e4d1e69e3c6a93317ec77","0x000000000000000000000000000000000019ec4850bfd37b9ff6c6c37ee18ea0","0x000000000000000000000000000000e2f05188e6cf778a0e83cbd802df2137dc","0x00000000000000000000000000000000000648fd3d1938953473fbf1992a1a0d","0x0000000000000000000000000000004b8a4408e4550a88712539ac23123da4d9","0x00000000000000000000000000000000002d42bdc43b945b84bb790ce832b4fa","0x0000000000000000000000000000001c3b96a17adf9ede03ef8631fb3dd882e9","0x000000000000000000000000000000000002042c33e5a67a0466ab51daa1cd58","0x000000000000000000000000000000c718f214b89a0282b726a88d928a48a5e1","0x00000000000000000000000000000000001adfcfe9e07545d3f43ca4491dd2ef","0x000000000000000000000000000000b01dbb08a6e78d278fb9777e55f5062e2c","0x0000000000000000000000000000000000122a27907479081d47ce05e12a9bc2","0x0000000000000000000000000000008273c63c240753dd46d3ebe5905c877ceb","0x000000000000000000000000000000000029bf117651e308871fd01200cd3d97","0x000000000000000000000000000000e7c89b48a1f264687e29c5bb6a3b29b0ea","0x000000000000000000000000000000000018c2c4ecff68ed36990b085ae6eeb0","0x0000000000000000000000000000005206f07655d92b74b8f70b49b3c0d20b6d","0x000000000000000000000000000000000014e1b95e20c0363366a7d450ddb471","0x000000000000000000000000000000c3188cd7a04d6c8a8803f89c399e9bc958","0x00000000000000000000000000000000001480c491cfda58b06ac1de20233d25","0x00000000000000000000000000000017515b2bd236e2f9c584a7ce552dc8fcc2","0x000000000000000000000000000000000010cc2efa8f18c2b3612e01a8f94503","0x0000000000000000000000000000001f0a3f81411b1e06edf45c3394c8c296e2","0x0000000000000000000000000000000000093763194f12aae29dcb9f7b74552a","0x00000000000000000000000000000022eadbf390242527d6646603cb2ba8fb18","0x0000000000000000000000000000000000040bb072ad71de805dc3d35901d8fd","0x0000000000000000000000000000009969e938d09ae8a3e0b2e88252474433f1","0x000000000000000000000000000000000001a0c3834a281427a0bd40fbfad2c0","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f3f3adcb44841356ca07ede3ffdaa4e56f","0x00000000000000000000000000000000001ffaeed8a0fd4e93e92f31906d07db","0x000000000000000000000000000000947443af422077ce83dc603affb9d07832","0x0000000000000000000000000000000000247c8888f27a2569a7c50eafb17f6b"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x000000000000000000000000000000000000000000000000000000000000002a","0x0000000000000000000000000000000000000000000000000000000000000001","0x000000000000000000000000000000000000000000000000000000000000001a","0x0000000000000000000000000000001ee8326ff6fa4d9eaa698e6140f07f937b","0x0000000000000000000000000000000000106a0a88936a807b905fd25fa6eae5","0x0000000000000000000000000000007c8a46d26563134c136509e706b746f1c4","0x00000000000000000000000000000000000590438230856dc4e29b2b487f82a6","0x0000000000000000000000000000008fda4fa9a21711c3439d8c7bdc8706a35a","0x000000000000000000000000000000000026ee93cae09f66482049b8ab9f18ee","0x000000000000000000000000000000fa6519a25edb2d71cde10a7979794e21ff","0x00000000000000000000000000000000000783403486a30c30781b1336b06de0","0x0000000000000000000000000000002c88f0e429c3aa1b48f7f93509eb30bc9a","0x00000000000000000000000000000000000d417179f4c315c350cc1107b9cfc9","0x000000000000000000000000000000b0b25c47633b74d1a2bf1cd00ffce7b12c","0x000000000000000000000000000000000007ce4d696b72e2d8452d52a7caf9de","0x0000000000000000000000000000002d12248226512f020e334317dae6eaf0c8","0x000000000000000000000000000000000018cccf723238de695bcde25904b27d","0x000000000000000000000000000000f88387a18ddc16904307058f7d7a883e4a","0x00000000000000000000000000000000001d06b3270da56a3607007ee7ad0f1e","0x000000000000000000000000000000ee1a79ad9afc1d02b840e27c5283e74499","0x000000000000000000000000000000000028d1b1924fe7cf03f605e701eccd20","0x000000000000000000000000000000a2e8413b26a238c1274c1c4bc1bf3fa366","0x000000000000000000000000000000000028554b198c6912b267e8a6bf0987da","0x000000000000000000000000000000725ddea9830a058064e593ae1150357563","0x00000000000000000000000000000000000be21052e0ca4bbc01af225ab441ef","0x00000000000000000000000000000039b1667579c4ac66601d7962273b2bd832","0x00000000000000000000000000000000001e82560f75396646bb960ee9ec317b","0x000000000000000000000000000000933174c97d07b3e2e562d13bd83ed809d0","0x00000000000000000000000000000000002c183f1ad7406f0e39afd5ae8a4d66","0x000000000000000000000000000000cf09eb98da45a5b7fd4fc995638326c2a7","0x00000000000000000000000000000000000465c7d5dfc1c170aa4885b2933dfa","0x0000000000000000000000000000004488817604cfb82b60e35ade6c2bfbbde4","0x000000000000000000000000000000000025df503deb0f9d455fd7fdbc35501d","0x0000000000000000000000000000005b0af382eaa78b050823e07021a2596730","0x000000000000000000000000000000000013296f3f21d5be36f6c3de4f7e12bb","0x00000000000000000000000000000013e4981e445ea861b4d191c94874786ced","0x000000000000000000000000000000000009f2a00d23e559fc77b790f178d644","0x0000000000000000000000000000005a04ff9ac166a13efe0bbb42af5a9370a2","0x00000000000000000000000000000000001d488faa8537a3184ed1838cfa8da9","0x0000000000000000000000000000006ebe4f355ba6a3fc1d7e57f657359fcf7e","0x00000000000000000000000000000000001970bb0ec1ae1d702597e0504befc0","0x000000000000000000000000000000ff7fa0375289b0c62312d5c20c6e9b9d82","0x0000000000000000000000000000000000242ff04c2fb35a5b4940794a5e0c9b","0x000000000000000000000000000000e24c5739a33f3016deeb3150ca6cd333ed","0x0000000000000000000000000000000000168f59e1c5afdc0b6f4351e098969d","0x0000000000000000000000000000008fabee483e7aa0b20e40295d2fee6e79da","0x0000000000000000000000000000000000121880c763fa3fd1bdfc39945bf18a","0x000000000000000000000000000000df647f1842d3f41f64ad17f3fef8cad92a","0x00000000000000000000000000000000000baa779152f64c30acd67fb6a96532","0x00000000000000000000000000000040611399572dd13dfc9e5711b25a957117","0x00000000000000000000000000000000000233a10ee44b41e3123d589039619e","0x000000000000000000000000000000ed142ab059b436c2440f0567e5985e038e","0x0000000000000000000000000000000000107423e6071c6594e7960804836a87","0x0000000000000000000000000000008c0b459dfd4818f2ce5d4622d460ac4b3b","0x000000000000000000000000000000000027f96010efb256b54377ea2cf2203b","0x0000000000000000000000000000009fc85e0c56770304c47b796b55ac95f26e","0x00000000000000000000000000000000000c9f12f0b2dc198d1d714a54c15f07","0x000000000000000000000000000000f25195b7b261832155e87a05dcf0c0910b","0x00000000000000000000000000000000002ec2de20c7e0b55f993be81484ab72","0x000000000000000000000000000000785648e012fdba3ea8946ba29cd6b6289d","0x0000000000000000000000000000000000054fa9f89a5fdb8cc9a1cd7ec7167f","0x000000000000000000000000000000dcc51423d919d1b391abf47e023905742f","0x00000000000000000000000000000000001d15d476416457f487b97af414ea1a","0x000000000000000000000000000000a248122c42f86368af6a0c74a9a25d5f9c","0x000000000000000000000000000000000000ee8f7378796672556adecf8f6c63","0x000000000000000000000000000000e8c233c18d70dfd6675ae5dd7df5e29271","0x00000000000000000000000000000000002c56c41a07a3006c2830ac414004e7","0x000000000000000000000000000000c675e97962a8230f86a1093d714e9f0f05","0x00000000000000000000000000000000001b125d3316c14b62c532f6b8b2e4fe","0x00000000000000000000000000000089bd1899f66cf8f75b58a2b60a9de48cf7","0x00000000000000000000000000000000002706f62382cc7e17c7e531832ff966","0x000000000000000000000000000000d26a37225805d4e0170474e9e02188fb21","0x00000000000000000000000000000000001e50e6e7ad7be4d8dacbf29e940af4","0x000000000000000000000000000000efba449cb7d241c0dd520375104721726d","0x00000000000000000000000000000000001b095bb696a673bd0c5a116e27b404","0x0000000000000000000000000000006dba6a66fc696e4d1e69e3c6a93317ec77","0x000000000000000000000000000000000019ec4850bfd37b9ff6c6c37ee18ea0","0x000000000000000000000000000000e2f05188e6cf778a0e83cbd802df2137dc","0x00000000000000000000000000000000000648fd3d1938953473fbf1992a1a0d","0x0000000000000000000000000000004b8a4408e4550a88712539ac23123da4d9","0x00000000000000000000000000000000002d42bdc43b945b84bb790ce832b4fa","0x0000000000000000000000000000001c3b96a17adf9ede03ef8631fb3dd882e9","0x000000000000000000000000000000000002042c33e5a67a0466ab51daa1cd58","0x000000000000000000000000000000c718f214b89a0282b726a88d928a48a5e1","0x00000000000000000000000000000000001adfcfe9e07545d3f43ca4491dd2ef","0x000000000000000000000000000000b01dbb08a6e78d278fb9777e55f5062e2c","0x0000000000000000000000000000000000122a27907479081d47ce05e12a9bc2","0x0000000000000000000000000000008273c63c240753dd46d3ebe5905c877ceb","0x000000000000000000000000000000000029bf117651e308871fd01200cd3d97","0x000000000000000000000000000000e7c89b48a1f264687e29c5bb6a3b29b0ea","0x000000000000000000000000000000000018c2c4ecff68ed36990b085ae6eeb0","0x0000000000000000000000000000005206f07655d92b74b8f70b49b3c0d20b6d","0x000000000000000000000000000000000014e1b95e20c0363366a7d450ddb471","0x000000000000000000000000000000c3188cd7a04d6c8a8803f89c399e9bc958","0x00000000000000000000000000000000001480c491cfda58b06ac1de20233d25","0x00000000000000000000000000000017515b2bd236e2f9c584a7ce552dc8fcc2","0x000000000000000000000000000000000010cc2efa8f18c2b3612e01a8f94503","0x0000000000000000000000000000001f0a3f81411b1e06edf45c3394c8c296e2","0x0000000000000000000000000000000000093763194f12aae29dcb9f7b74552a","0x00000000000000000000000000000022eadbf390242527d6646603cb2ba8fb18","0x0000000000000000000000000000000000040bb072ad71de805dc3d35901d8fd","0x0000000000000000000000000000009969e938d09ae8a3e0b2e88252474433f1","0x000000000000000000000000000000000001a0c3834a281427a0bd40fbfad2c0","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000f3f3adcb44841356ca07ede3ffdaa4e56f","0x00000000000000000000000000000000001ffaeed8a0fd4e93e92f31906d07db","0x000000000000000000000000000000947443af422077ce83dc603affb9d07832","0x0000000000000000000000000000000000247c8888f27a2569a7c50eafb17f6b"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json index 8c94c4041..be9988f8e 100644 --- a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000b1e2f541998a2755e5213d3e971e91df06","0x00000000000000000000000000000000001c85aa14f04fe5cd6ba340f1e36f30","0x000000000000000000000000000000dd17a4a404747a820741028f49190e3643","0x00000000000000000000000000000000001c56196f530a939d9d97a3cc03ab0b","0x000000000000000000000000000000e138bb15582e87af9d51fbc7ed6e0e7695","0x00000000000000000000000000000000001e7f60c2349b166c96ad0d379b4ca3","0x000000000000000000000000000000b98deafa29f053f7ceb11d0d32de87dc7c","0x0000000000000000000000000000000000002ce030b942f99071c3b7b51a20c3","0x000000000000000000000000000000f88fb777e1dc9c467fbc86e05b6b8a8e8a","0x00000000000000000000000000000000000b3dcec21f9a3468ac8cff9f0dc6b2","0x0000000000000000000000000000004456c9e04f1ae5534b41cb9d4d0f05556a","0x00000000000000000000000000000000001e8b9a96be11a99546efa51b0adeed","0x0000000000000000000000000000008d0020e155991b2fdf7d87f21051e09bf1","0x00000000000000000000000000000000002faca69121bb28c07ff1f61acf5bc6","0x0000000000000000000000000000002bf8105c273f37166bf859778efe38beae","0x000000000000000000000000000000000003d52e114558e4968f6751b3244396","0x000000000000000000000000000000c8a6c7a4920d3415fab8e852d34e731f3d","0x0000000000000000000000000000000000169f38558aa5d5fbc8ccfc308ece82","0x0000000000000000000000000000007ab272b3b30d2aa5a04ab9ff8b03761300","0x0000000000000000000000000000000000048cbf034592a047b3f743cc06a9b3","0x00000000000000000000000000000092f9818005d01ae6aa95051e81a4d8fd7c","0x000000000000000000000000000000000019f5135e35bb7fd0bb0db2984503d7","0x0000000000000000000000000000001a8a976ae62ab0d4ebdf0f026a25320094","0x0000000000000000000000000000000000237782b32e0dd4f3385e0e58707578","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x00000000000000000000000000000083f8426178a4adb8759e565672d48d57b2","0x00000000000000000000000000000000000d2162b8663e51626b63b613f38d10","0x0000000000000000000000000000005abf0765b4ec2d9008d3a0b0e4507d896d","0x00000000000000000000000000000000001c9eb82857d4ed7141bbdd48792bf6","0x00000000000000000000000000000053644e331267af096b7fa639d567af46d5","0x0000000000000000000000000000000000169b62f18a62c1dd259c0e4c1e6ed7","0x000000000000000000000000000000e0c1b58069ea333eef6ab3adfdebfbff1b","0x0000000000000000000000000000000000194f5f1fb476a26932b74e9b3b0076","0x0000000000000000000000000000007f16b72a7cd37b7fad520beb587385af0c","0x00000000000000000000000000000000000b7c2b90171c94d593ce8c018f9d9f","0x0000000000000000000000000000006b97f4bcd4ffbb3f43a44c9b54da291227","0x000000000000000000000000000000000011e40fb9bb5d8e48416a30e2fbec52","0x00000000000000000000000000000045fe00b3b7a774a6f24792e2232d7cff7e","0x00000000000000000000000000000000001931e0d13d1f24ae08944ddf7477e9","0x000000000000000000000000000000a03c16e18ed20dc8b0359b35f488a561b5","0x00000000000000000000000000000000001e367c248e85a81becee8094786fa5","0x000000000000000000000000000000a9e97de75737843e2a615ddfcf184170b3","0x000000000000000000000000000000000012af86eb66bebd2a4d37c160d27a23","0x0000000000000000000000000000000be148149805efb06f7487733e2ca3f78d","0x000000000000000000000000000000000001623d48ab894fd1c64ab90c49fc39","0x000000000000000000000000000000c0e17eccf6222f6f5e80382b856c9069ca","0x0000000000000000000000000000000000049669495d53f7e25db94054b7bf66","0x000000000000000000000000000000063bf54ae8896ffcfcd5f483bce7ffbd2c","0x000000000000000000000000000000000011e1d2550ae5e4aa5f37048b93c3ac","0x00000000000000000000000000000022a5b91d96420560bfdf12ae5165751d8e","0x000000000000000000000000000000000023e6d77db8a0dec18f31aca6ade9c3","0x000000000000000000000000000000ce8ad7503f564d906acc22f7b0cb9dd19f","0x000000000000000000000000000000000017d33b927e0c32c36e85da3c2c0f31","0x0000000000000000000000000000008805aeb41843b56d14004556766019c8ee","0x00000000000000000000000000000000001c3ccfccedd90945ebed03bdf8055f","0x000000000000000000000000000000e1d30462a2f0338a3c2a60c597c8066728","0x000000000000000000000000000000000015539075e03a859b281b75c49ba7d0","0x000000000000000000000000000000229e8d446d2172bca1bde608f9b7af6940","0x00000000000000000000000000000000000f8f294047a4fe4f38bb3d32986e65","0x0000000000000000000000000000003547cd41239103ff65ed2aa52e662d1fa7","0x000000000000000000000000000000000029d698f9e85044e11bc2fa37409c12","0x0000000000000000000000000000003449790c59fc7a4349e1e55768a7066fd4","0x0000000000000000000000000000000000075204d5ba12a18730f9cca06c9d14","0x000000000000000000000000000000d74e3abd35dea7d5c3864b37381dacb1dd","0x0000000000000000000000000000000000105a75d044776abb9e403442460c86","0x000000000000000000000000000000aae1559412cb8f880d77c7267b8e5317fc","0x000000000000000000000000000000000029296657d4604dca5b5cfa3646301e","0x0000000000000000000000000000005d4b8efc3486782d15288b1bbb15565916","0x00000000000000000000000000000000000d04883311bd540e8eedc4f2c8d15a","0x000000000000000000000000000000957eb2ae0714fab81cb7bc641afd950611","0x0000000000000000000000000000000000219ba3ad43be1e3cbd9fce0ce50769","0x000000000000000000000000000000c0268a81e172ba6755637a92aa132948f6","0x00000000000000000000000000000000000cba22f3994d2bfac4ed66eccc1e0b","0x000000000000000000000000000000df174b538c33c37f6aae1f5376293eb040","0x00000000000000000000000000000000002e8e391362e10fcc3e73988410b62e","0x00000000000000000000000000000043520aea0acff78687103480b09c01a8f6","0x00000000000000000000000000000000002a69cc75b104ac7855fd7f75f184a3","0x000000000000000000000000000000267c9488653d674ef240ba580f33176340","0x000000000000000000000000000000000019e6a90b94d863a6d4c4e39e80e6c0","0x0000000000000000000000000000009c7b961d4157b3b26b9156a3932361c86e","0x0000000000000000000000000000000000275212059032c11bb72f6a50581c87","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000d678f1a0bcc54fa74011eefbef799f4a0d","0x000000000000000000000000000000000030439c58cae8bce4e184e2b8bda865","0x000000000000000000000000000000c8f80f131a32e400e35d3dad34964738b9","0x00000000000000000000000000000000000fcd9a67a0311379f8a005c79daec1"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000b1e2f541998a2755e5213d3e971e91df06","0x00000000000000000000000000000000001c85aa14f04fe5cd6ba340f1e36f30","0x000000000000000000000000000000dd17a4a404747a820741028f49190e3643","0x00000000000000000000000000000000001c56196f530a939d9d97a3cc03ab0b","0x000000000000000000000000000000e138bb15582e87af9d51fbc7ed6e0e7695","0x00000000000000000000000000000000001e7f60c2349b166c96ad0d379b4ca3","0x000000000000000000000000000000b98deafa29f053f7ceb11d0d32de87dc7c","0x0000000000000000000000000000000000002ce030b942f99071c3b7b51a20c3","0x000000000000000000000000000000528a55b629a204f061da96f9f06561d4f9","0x000000000000000000000000000000000023792b0987721b9128e0cf41d9b32c","0x0000000000000000000000000000008a430926dbdf33c991898be570409a110a","0x0000000000000000000000000000000000219cb1f43da57ea5d7d298bd0e7b49","0x00000000000000000000000000000092e74077b65ff0e232758cf9429592aaf5","0x00000000000000000000000000000000001282528955f7796f0a65e209114dd2","0x00000000000000000000000000000029590dd636d3648667c797ca389e874332","0x0000000000000000000000000000000000236ef8ad2e3fc5ce9dc9143315fb86","0x000000000000000000000000000000c8a6c7a4920d3415fab8e852d34e731f3d","0x0000000000000000000000000000000000169f38558aa5d5fbc8ccfc308ece82","0x0000000000000000000000000000007ab272b3b30d2aa5a04ab9ff8b03761300","0x0000000000000000000000000000000000048cbf034592a047b3f743cc06a9b3","0x00000000000000000000000000000092f9818005d01ae6aa95051e81a4d8fd7c","0x000000000000000000000000000000000019f5135e35bb7fd0bb0db2984503d7","0x0000000000000000000000000000001a8a976ae62ab0d4ebdf0f026a25320094","0x0000000000000000000000000000000000237782b32e0dd4f3385e0e58707578","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x00000000000000000000000000000083f8426178a4adb8759e565672d48d57b2","0x00000000000000000000000000000000000d2162b8663e51626b63b613f38d10","0x0000000000000000000000000000005abf0765b4ec2d9008d3a0b0e4507d896d","0x00000000000000000000000000000000001c9eb82857d4ed7141bbdd48792bf6","0x00000000000000000000000000000053644e331267af096b7fa639d567af46d5","0x0000000000000000000000000000000000169b62f18a62c1dd259c0e4c1e6ed7","0x000000000000000000000000000000e0c1b58069ea333eef6ab3adfdebfbff1b","0x0000000000000000000000000000000000194f5f1fb476a26932b74e9b3b0076","0x0000000000000000000000000000007f16b72a7cd37b7fad520beb587385af0c","0x00000000000000000000000000000000000b7c2b90171c94d593ce8c018f9d9f","0x0000000000000000000000000000006b97f4bcd4ffbb3f43a44c9b54da291227","0x000000000000000000000000000000000011e40fb9bb5d8e48416a30e2fbec52","0x00000000000000000000000000000045fe00b3b7a774a6f24792e2232d7cff7e","0x00000000000000000000000000000000001931e0d13d1f24ae08944ddf7477e9","0x000000000000000000000000000000a03c16e18ed20dc8b0359b35f488a561b5","0x00000000000000000000000000000000001e367c248e85a81becee8094786fa5","0x000000000000000000000000000000a9e97de75737843e2a615ddfcf184170b3","0x000000000000000000000000000000000012af86eb66bebd2a4d37c160d27a23","0x0000000000000000000000000000000be148149805efb06f7487733e2ca3f78d","0x000000000000000000000000000000000001623d48ab894fd1c64ab90c49fc39","0x000000000000000000000000000000c0e17eccf6222f6f5e80382b856c9069ca","0x0000000000000000000000000000000000049669495d53f7e25db94054b7bf66","0x000000000000000000000000000000063bf54ae8896ffcfcd5f483bce7ffbd2c","0x000000000000000000000000000000000011e1d2550ae5e4aa5f37048b93c3ac","0x000000000000000000000000000000dea7b01531d31241ab850cd5d66f7c0e15","0x00000000000000000000000000000000002696590fa94126f758939789a4f106","0x0000000000000000000000000000000113486b4b5551285a003bd39fa6c86fe5","0x00000000000000000000000000000000001a5832d94ed308197877dd081325b8","0x000000000000000000000000000000792d41e7d3f7eae77e8ee735d474243793","0x0000000000000000000000000000000000087af58bfadcc29c72cd4ec312329b","0x000000000000000000000000000000f11ae9376eb15d1aad265033c153e96a44","0x0000000000000000000000000000000000291651af3e46e2febc867996076804","0x000000000000000000000000000000229e8d446d2172bca1bde608f9b7af6940","0x00000000000000000000000000000000000f8f294047a4fe4f38bb3d32986e65","0x0000000000000000000000000000003547cd41239103ff65ed2aa52e662d1fa7","0x000000000000000000000000000000000029d698f9e85044e11bc2fa37409c12","0x0000000000000000000000000000003449790c59fc7a4349e1e55768a7066fd4","0x0000000000000000000000000000000000075204d5ba12a18730f9cca06c9d14","0x000000000000000000000000000000d74e3abd35dea7d5c3864b37381dacb1dd","0x0000000000000000000000000000000000105a75d044776abb9e403442460c86","0x00000000000000000000000000000050c32559396a96a2c975b2387cf9b0f0a8","0x00000000000000000000000000000000001560274dd48a2b5d24b6091ca1e9d5","0x000000000000000000000000000000926bdc48c50dff3424d82c0478e4d1a5ee","0x000000000000000000000000000000000008bf7f0695b740da94ed59263850ee","0x000000000000000000000000000000f78443226cf26cb8bd04a73b09d33c6481","0x00000000000000000000000000000000001e4f8f021bcc5df9de12fb67a01b94","0x000000000000000000000000000000704e51d181f28de21c8a8fca2978a3cef2","0x000000000000000000000000000000000018be339c2f9f1df76ce2b201b70385","0x000000000000000000000000000000df174b538c33c37f6aae1f5376293eb040","0x00000000000000000000000000000000002e8e391362e10fcc3e73988410b62e","0x00000000000000000000000000000043520aea0acff78687103480b09c01a8f6","0x00000000000000000000000000000000002a69cc75b104ac7855fd7f75f184a3","0x000000000000000000000000000000267c9488653d674ef240ba580f33176340","0x000000000000000000000000000000000019e6a90b94d863a6d4c4e39e80e6c0","0x0000000000000000000000000000009c7b961d4157b3b26b9156a3932361c86e","0x0000000000000000000000000000000000275212059032c11bb72f6a50581c87","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000d678f1a0bcc54fa74011eefbef799f4a0d","0x000000000000000000000000000000000030439c58cae8bce4e184e2b8bda865","0x000000000000000000000000000000c8f80f131a32e400e35d3dad34964738b9","0x00000000000000000000000000000000000fcd9a67a0311379f8a005c79daec1"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/compliance/test.rs b/packages/tokens/src/confidential/compliance/test.rs index c9bb4bcd4..40070b7dd 100644 --- a/packages/tokens/src/confidential/compliance/test.rs +++ b/packages/tokens/src/confidential/compliance/test.rs @@ -223,7 +223,7 @@ fn spender_transfer_payload(e: &Env) -> SpenderTransferPayload { r_tilde_aud_r: fr(e), v_tilde_aud_s: fr(e), a_tilde_aud_s: fr(e), - dvk_cipher_aud: fr(e), + r_tilde_aud_s: fr(e), } } @@ -240,7 +240,7 @@ fn set_spender_payload(e: &Env) -> SetSpenderPayload { v_tilde_aud_s: fr(e), b_tilde_aud_s: fr(e), r_tilde_aud_s: fr(e), - dvk_cipher_aud: fr(e), + r_a_tilde_aud_s: fr(e), } } diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index a2456163e..322455c2f 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -149,7 +149,7 @@ $$\text{SpongeSqueeze}\_n(\delta\_{\text{channel}}, s, \sigma) = \bigl(\text{sta where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is the number of rate lanes read; the capacity lane $$\text{state}[3]$$ is never squeezed. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, squeezed three-wide, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$, squeezed two-wide. No other arity is instantiated. Because the absorb is one block, $$\text{SpongeSqueeze}\_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}\_2(\delta, s, \sigma)$$: widening a channel adds a lane without changing the value of any existing one. -**Lane assignment.** Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **secret-escrow slot**; which plaintext it carries per operation, and why one lane over two kinds of plaintext is not pad reuse, is specified in Section 8.1 *The lane-2 slot*. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. +**Lane assignment.** Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **blinding-escrow slot**: it always carries the blinding of a commitment the operation writes, never a key. Which commitment per operation is specified in Section 8.1 *The lane-2 slot*. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. **Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two multi-lane tags; every other tag in Section 13 is used only with the single-output form above. Reading one multi-lane tag at two different widths is not a mode violation, by the prefix property above: the sender-auditor tag is squeezed three-wide on every operation that escrows lane 2 and two-wide on `RevokeSpender` (V\_a3), while the recipient-auditor tag is always two-wide. @@ -409,7 +409,7 @@ Because $$\sigma$$ is published in the event and $$vk$$ is held by the originato **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_spender` (§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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender escrow), so masks across channels are independent under the PRF assumption on Poseidon (§3.2). The auditor-side $$dvk\_i$$ escrow (S14, $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$) is the one derivation that does *not* open a channel of its own -- it reuses the S\_a2 shared scalar -- so its separation from the sender-auditor sponge rests on §2.5 *Mode exclusivity* rather than on a distinct shared scalar; [DESIGN_cont.md](./DESIGN_cont.md) §8.5 argues it. 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 contract's enumeration of channels in §13 satisfies the domain-distinctness condition. +**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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender escrow), so masks across channels are independent under the PRF assumption on Poseidon (§3.2). The auditor-side allowance-blinding escrow (S14, $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$) is the one derivation that does *not* open a channel of its own. It reuses the S\_a2 shared scalar, so it drops the distinct-scalar leg, and it absorbs $$\text{op}\_i$$, a per-delegation constant, rather than a per-operation nonce, so it drops the freshness leg too. Its separation therefore rests on the distinct-tag leg alone, plus the freshness of the shared scalar itself, inherited from $$r\_e$$ and hence from the salt. This is the thinnest construction in the system, and [DESIGN_cont.md](./DESIGN_cont.md) §8.5 states what it does and does not buy. 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 for every channel; the contract's enumeration of channels in §13 satisfies the domain-distinctness condition. S14 is the single derivation that stands on one of the three, as noted above. ### 5.4 Anti-Poisoning Constraint @@ -696,7 +696,7 @@ The owner locks funds from their spendable balance into a per-spender escrow. Th | 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) | -| S14 | $$\text{dvk\\\_cipher\\\_aud} = \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i) + dvk\_i$$ (auditor-side escrow of the delegation viewing key over the S\_a2 shared scalar, Section 8.5; one Poseidon, no new scalar multiplication) | +| S14 | $$\tilde{r}\_{a,\text{aud,s}} = \text{Poseidon}(\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}, s\_{a,s}, \text{op}\_i) + r\_a$$ (auditor-side escrow of S6's allowance blinding over the S\_a2 shared scalar, Section 8.5; the blinding is already a witness by S6, so this is one Poseidon and no new scalar multiplication) | | S\_a1 | $$R\_e = r\_e \cdot H$$ (ephemeral key for auditor ECDH) | | S\_a2 | $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (owner-auditor ECDH shared scalar, §2.4) | | S\_a3 | $$(m\_v, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (owner-auditor channel masks) | @@ -714,11 +714,11 @@ The owner locks funds from their spendable balance into a per-spender escrow. Th | $$\text{op}\_i$$ | $$\text{address\\\_to\\\_field}$$(`spender` argument), computed per-call by the contract (§2.7) | | $$\text{addr\\\_f}$$ | 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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | Prover-supplied, in this order; $$C\_{\text{spend}}'$$ written to owner's `spendable_commitment`, the delegation fields written to storage, the rest emitted in event | +| $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$ | Prover-supplied, in this order; $$C\_{\text{spend}}'$$ written to owner's `spendable_commitment`, 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_commitment` $$= C\_{\text{spend}}'$$ and stores the `SpenderDelegation`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}}, \text{dvk\\\_cipher\\\_aud})$$. +**Post-verification:** The contract verifies the proof, sets `spendable_commitment` $$= C\_{\text{spend}}'$$ and stores the `SpenderDelegation`. Emits event with $$(R\_e, \sigma, \tilde{b}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}}, \tilde{r}\_{a,\text{aud,s}})$$. ### 7.8 Spender Transfer @@ -749,7 +749,7 @@ The spender transfers from the owner's escrowed allowance to a recipient. | O\_a6 | $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ (owner-auditor channel masks) | | O\_a7 | $$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}$$ (owner-auditor encrypted transfer amount) | | O\_a8 | $$\tilde{a}\_{\text{aud,s}} = (v\_a - v\_{\text{transfer}}) + m\_{a,s}$$ (owner-auditor encrypted post-transfer allowance) | -| O\_a9 | $$\text{dvk\\\_cipher\\\_aud} = dvk\_i + m\_{r,s}$$ (owner-auditor re-escrow of the delegation viewing key, Section 8.5; $$dvk\_i$$ is already a witness by O2, so this is one field addition) | +| O\_a9 | $$\tilde{r}\_{\text{aud,s}} = r\_a' + m\_{r,s}$$ (owner-auditor escrow of the NEW allowance blinding, the one O11 commits under, Section 8.5; already a witness by O10, so this is one field addition. The lane-2 pad absorbs the consumed salt while the plaintext derives from the written one; §8.4 says why that is sound) | **Public inputs (25 fields):** @@ -760,11 +760,11 @@ The spender transfers from the owner's escrowed allowance to a recipient. | $$\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 spender's. The visibility model points balance- and allowance-checkpoint ciphertexts at the funds' owner. | -| $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | Prover-supplied, in this order; allowance fields written to delegation storage, $$C\_{\text{transfer}}$$ added to recipient's `receiving_commitment`, the rest emitted in event | +| $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | Prover-supplied, in this order; allowance fields written to delegation storage, $$C\_{\text{transfer}}$$ added to recipient's `receiving_commitment`, 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{transfer}}$$, $$r\_e$$. -**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores `new_allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}}, \text{dvk\\\_cipher\\\_aud})$$. +**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores `new_allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}}, \tilde{r}\_{\text{aud,s}})$$. **Ephemeral scalar.** The spender derives $$r\_e = \text{Poseidon}(\delta\_{\text{eph}}, vk\_{\text{op}}, \sigma\_a)$$ (§5.3) from its *own* viewing key rather than the owner's, so that the spender can later disclose it ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7). The circuit does not constrain the derivation; it does not constrain $$vk\_{\text{op}}$$ at all, per *Contract binding* below. One consequence follows for the owner: since the owner does not hold $$vk\_{\text{op}}$$, the owner cannot recompute $$r\_e$$ for a spender transfer and cannot disclose it without the spender's cooperation ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7, *Coverage asymmetry*). @@ -834,7 +834,7 @@ The spender decrypts using $$sk\_{\text{op}}$$. The `set_spender` proof enforces 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. -The same proof escrows $$dvk\_i$$ a second time, to the owner's *auditor* rather than to the spender (S14), under its own domain tag and over the auditor shared scalar. That construction, its decryption path, and why it is a single-output pad rather than a sponge lane are specified in [DESIGN_cont.md](./DESIGN_cont.md) §8.5. +The same proof also escrows to the owner's *auditor* (S14), under its own domain tag and over the auditor shared scalar -- but what it escrows is the allowance blinding $$r\_a$$, not a second copy of $$dvk\_i$$. The auditor needs the opening of $$C\_a$$, not the generator of every opening, and the delegation key is permanent per pair -- it survives revocation and re-delegation to the same address -- so escrowing it would make one leaked ciphertext a permanent capability. That construction, its decryption path, and why it is a single-output pad rather than a sponge lane are specified in [DESIGN_cont.md](./DESIGN_cont.md) §8.5. ### 7.12 Expiry and Revert Safety diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 7ca7a6e33..13405e131 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -23,7 +23,7 @@ $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}}) \qquad \text{(DESIGN §2.4)}$$ $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma)$$ $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}, \qquad \tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + m\_{r,r}$$ -**Sender's auditor** ($$K\_{\text{aud,s}}$$, from the sender's `auditor_id`) receives the transfer amount, the sender's post-transfer balance, and -- in the lane-2 secret-escrow slot (DESIGN §2.5 *Lane assignment*) -- the sender's post-transfer spendable blinding $$r\_A'$$: +**Sender's auditor** ($$K\_{\text{aud,s}}$$, from the sender's `auditor_id`) receives the transfer amount, the sender's post-transfer balance, and -- in the lane-2 blinding-escrow slot (DESIGN §2.5 *Lane assignment*) -- the sender's post-transfer spendable blinding $$r\_A'$$: $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}}) \qquad \text{(DESIGN §2.4)}$$ $$(m\_{v,s}, m\_{b,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ @@ -39,7 +39,7 @@ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_{\text{ 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{transfer}}, r\_{\text{transfer}})$$. -**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6) and the delegation viewing key $$dvk\_i$$ on spender transfers (O\_a9, Section 8.4). Carrying two kinds of plaintext under one lane is not pad reuse: the pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. +**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the blinding of a commitment the operation writes, and never a key: the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6), and the new allowance blinding $$r\_a'$$ on spender transfers (O\_a9, Section 8.4). The two blindings belong to different commitment roles, so the uniformity is a naming convention, not an interchangeability claim; each is scoped to the operation that emitted it. The pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. **Recipient-auditor opening capability.** Because the recipient-auditor recovers $$r\_{\text{transfer}}$$ for every inbound transfer, and because deposits add to `receiving_commitment` with $$r = 0$$ (Section 7.3), the recipient-auditor can reconstruct the full Pedersen opening of $$C\_{\text{receive}}$$ between merges: @@ -100,10 +100,12 @@ Each spender transfer produces auditor ciphertexts under two keys (constraints O $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma\_a)$$ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,r}} - m\_{v,r}, \qquad r\_{\text{transfer}} = \tilde{r}\_{\text{aud,r}} - m\_{r,r}$$ -The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the lane-2 secret-escrow slot -- the delegation viewing key $$dvk\_i$$ (O\_a9): +The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the lane-2 blinding-escrow slot -- the *new* allowance blinding $$r\_a'$$, the one O11 commits $$C\_a'$$ under (O\_a9): $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ -$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}, \qquad dvk\_i = \text{dvk\\\_cipher\\\_aud} - m\_{r,s}$$ +$$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}, \qquad r\_a' = \tilde{r}\_{\text{aud,s}} - m\_{r,s}$$ + +Note the pad and the plaintext key off different salts: the lane-2 mask absorbs the *consumed* $$\sigma\_a$$ (the whole sponge does, per O\_a6), while $$r\_a' = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a')$$ derives from the salt the operation writes. That is sound -- the pad needs only to be fresh and unpredictable to everyone but the auditor, and $$(s\_{a,s}, \sigma\_a)$$ is both, since $$s\_{a,s}$$ is fresh per operation -- and $$(\tilde{a}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$ therefore open the post-transfer $$C\_a'$$, the state left on-chain. 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 spender-transfer inbound flows: $$r\_{\text{transfer}}$$ from spender-transfers contributes to $$r\_r$$ in $$C\_{\text{receive}}$$ identically to owner-transfer inbound flows. @@ -111,17 +113,26 @@ where $$s\_{a,r}$$, $$s\_{a,s}$$, and $$\sigma\_a$$ are recovered from the event The auditor tracks each allowance's current value through the per-event ciphertexts produced at every state-changing operation: `set_spender` 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_spender` reveals the reclaimed amount (Section 8.2). -**Allowance opening.** The owner's auditor also receives $$dvk\_i$$ itself: under $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ at `set_spender` (S14, §8.5 *Auditor-side delegation-key escrow*) and in lane 2 at every spender transfer (O\_a9, §8.4). Since the allowance blinding is $$r\_a = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a)$$, and the salt that opens the commitment written by each event is published in that event -- $$\sigma\_a$$ on `SetSpender`, $$\sigma\_a'$$ on `SpenderTransfer` (§11.2) -- the auditor reconstructs the full Pedersen opening of $$C\_a$$ at each of those events, with no on-chain read. Unlike the spendable side there is no merge to fold in, since a delegation's only state transitions are the events themselves. +**Allowance opening.** The owner's auditor also receives the *blinding* of the allowance commitment each event writes: $$r\_a$$ under $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ at `set_spender` (S14, below) and $$r\_a'$$ in lane 2 at every spender transfer (O\_a9, §8.4). Paired with the value that event already publishes -- $$\tilde{v}\_{\text{aud,s}}$$ on `SetSpender`, $$\tilde{a}\_{\text{aud,s}}$$ on `SpenderTransfer` -- that is the full Pedersen opening of the $$C\_a$$ left on-chain, reconstructed from the event alone with no storage read. Unlike the spendable side there is no merge to fold in, since a delegation's only state transitions are the events themselves. + +What is escrowed is one state's blinding, not the generator of every state. $$dvk\_i = \text{Poseidon}(\delta\_{\text{dvk}}, vk, \text{op}\_i)$$ is deterministic and permanent for a $$(owner, spender)$$ pair -- neither the salt nor any generation counter enters it, so `revoke_spender` followed by re-delegation to the same address regenerates it exactly. A single leaked $$dvk\_i$$ ciphertext would therefore hand its holder every allowance value and blinding for that pair, past and future; a leaked $$r\_a$$ ciphertext costs one state. + +**Auditor-side allowance-blinding escrow.** At `set_spender` the owner escrows $$r\_a$$ -- the blinding S6 derived and S7 committed under -- to its own auditor: + +$$\tilde{r}\_{a,\text{aud,s}} = r\_a + \text{Poseidon}(\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}, s\_{a,s}, \text{op}\_i)$$ + +reusing the S\_a2 shared scalar $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ rather than opening a new ECDH channel, so it costs one Poseidon and no scalar multiplication (DESIGN S14). The auditor recovers $$s\_{a,s}$$ from $$k\_{\text{aud,s}}$$ and the event's $$R\_e$$, and $$\text{op}\_i$$ from the event's `spender` topic, then subtracts. Because the escrowed value is pinned to S6, the opening the auditor holds necessarily matches the $$C\_a$$ the same proof wrote. -**Auditor-side delegation-key escrow.** At `set_spender` the owner escrows $$dvk\_i$$ to its own auditor as well as to the spender: +A single-output pad rather than a sponge lane, because lane 2 of this channel is already taken by the spendable blinding (S\_a6). What separates it from that channel is tag distinctness: the tag is the first absorbed element, so no pair of remaining inputs makes $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ and $$\delta\_{\text{aud\\\_s}}$$ agree. DESIGN §2.5 *Mode exclusivity* is a separate per-tag obligation that each satisfies independently -- 17 is only ever single-output, 11 only ever multi-lane. $$\text{op}\_i$$ is a per-$$(owner, spender)$$ constant that separates pads across delegations, not a nonce: the pad's freshness rests entirely on $$s\_{a,s}$$, hence on $$r\_e$$, hence on the salt (DESIGN §5.3, §9.6). Re-delegating to the same spender under a reused salt would republish a byte-identical ciphertext. -$$\text{dvk\\\_cipher\\\_aud} = dvk\_i + \text{Poseidon}(\delta\_{\text{esc\\\_dvk\\\_aud}}, s\_{a,s}, \text{op}\_i)$$ +**Key rotation.** Decryption is scoped to events, and only to events. A key can decrypt an allowance ciphertext exactly when it was the active key at the moment that ciphertext was produced. Two consequences, and neither is stronger than that: -reusing the S\_a2 shared scalar $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ rather than opening a new ECDH channel, so it costs one Poseidon and no scalar multiplication (DESIGN S14). The auditor recovers $$s\_{a,s}$$ from $$k\_{\text{aud,s}}$$ and the event's $$R\_e$$, and $$\text{op}\_i$$ from the event's `spender` topic, then subtracts. +- A freshly rotated-in key holds no opening for a live delegation. It acquires one at that delegation's next state-changing operation, when a fresh escrow is produced under it, and not before -- rotation does not re-anchor anything on its own. +- A retired key does **not** lose track of state it already opened. Proofless folds -- merge (§7.4), and `revoke_spender` where the deployment runs it without a proof -- recompute $$C\_{\text{spend}}' = C\_{\text{spend}} + C\_a$$ publicly, emitting no new encrypted checkpoint, so a holder of an already-known opening carries it forward by the same addition the contract performs. Containment here is **event-scoped, not state-scoped**: rotation bounds what a key can newly decrypt, not what its holder can still compute. -A single-output pad rather than a sponge lane, because lane 2 of this channel is already taken by the spendable blinding (S\_a6). Its separation from that channel is DESIGN §2.5 *Mode exclusivity*: $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ is a single-output tag and $$\delta\_{\text{aud\\\_s}}$$ a multi-lane one, so the two never produce the same field element from the same $$(s\_{a,s}, \cdot)$$. $$\text{op}\_i$$ is a per-$$(owner, spender)$$ constant that separates pads across delegations, not a nonce: the pad's freshness rests entirely on $$s\_{a,s}$$, hence on $$r\_e$$, hence on the salt (DESIGN §5.3, §9.6). Re-delegating to the same spender under a reused salt would republish a byte-identical ciphertext. +What the escrow does buy against a leak is blast radius. Exposing one escrowed $$r\_a$$ exposes one allowance state; the auditor's other allowance states, and every other delegation under the same key, are unaffected. -**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. +**Archive dependence.** The auditor must have *observed* the event. There is no state-based fallback: the escrowed blinding lives in the event and nowhere in contract storage, so an auditor that missed a delegation event cannot recover that opening from `a_tilde` and $$\sigma\_a$$ the way a $$dvk\_i$$ holder could. This extends an existing dependency rather than creating one -- DESIGN.md §5.2 already makes a durable event archive normative, and the recipient-side reconstruction, the spendable checkpoint, and the merge carry-forward are all event-scoped already. [INDEXER.md](./INDEXER.md) §7.1 states the operational consequences: a consistency check of each reconstructed opening against the stored `allowance_commitment` is the practical detection mechanism for a missed or reordered event, and delegation-entry TTL bounds how long that check remains possible. --- @@ -234,8 +245,8 @@ pub enum CircuitType { | `Register` | Spending key well-formedness; contract-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 ciphertexts (balance checkpoint + lane-2 escrow of the new spendable blinding); 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 + lane-2 escrow of the new spendable blinding); deterministic randomness for new sender balance; encrypted balance scalar; sender key ownership; range validity (balance $$\in [0, 2^{127})$$, amount $$\in [0, 2^{127})$$) | -| `SpenderTransfer` | 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 + lane-2 escrow of $$dvk\_i$$); deterministic randomness for new allowance; encrypted allowance scalar; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | -| `SetSpender` | Balance split; $$dvk\_i$$ derivation; ECDH escrow of $$dvk\_i$$ to the spender and to the owner's auditor; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint + lane-2 escrow of the new spendable blinding); owner key ownership; contract-bound via $$vk$$ derivation | +| `SpenderTransfer` | 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 + lane-2 escrow of the new allowance blinding); deterministic randomness for new allowance; encrypted allowance scalar; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | +| `SetSpender` | Balance split; $$dvk\_i$$ derivation; ECDH escrow of $$dvk\_i$$ to the spender and of the allowance blinding to the owner's auditor; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint + lane-2 escrow of the new spendable blinding); owner key ownership; contract-bound via $$vk$$ derivation | | `RevokeSpender` | 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; contract-bound via $$vk$$ derivation | ### 10.3 Circuit Cost Analysis @@ -476,8 +487,8 @@ Each state-modifying operation emits a structured event. Events carry the data n | `Merge` | `account` | | `Withdraw` | `from`, `to`, `amount`, $$R\_e$$, $$\sigma$$, $$\tilde{b}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | -| `SpenderTransfer` | `spender`, `from`, `to`, $$R\_e$$, $$\tilde{v}$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | -| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\sigma\_a$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ | +| `SpenderTransfer` | `spender`, `from`, `to`, $$R\_e$$, $$\tilde{v}$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | +| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\sigma\_a$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$ | | `RevokeSpender` | `account`, `spender`, $$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. @@ -536,9 +547,9 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the | $$\delta\_{\text{eph}}$$ | 14 | Deterministic ephemeral-scalar derivation (§5.3) | | $$\delta\_{\text{disc\\\_bind}}$$ | 15 | Disclosure ciphertext to the disclosure recipient, aggregate variant ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §10) | | $$\delta\_{\text{disc}}$$ | 16 | Disclosure ciphertext to the disclosure recipient ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §4) | -| $$\delta\_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Delegation key escrow to the owner's auditor (S14, §8.5) | +| $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ | 17 | Allowance-blinding escrow to the owner's auditor (S14, §8.5) | -This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16. It takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ because the two escrows key off different shared scalars ($$Y\_{\text{op}}$$ vs $$K\_{\text{aud,s}}$$), which is the distinct-domain leg of §5.3's *Why reusing $$r\_e$$ is safe*. It shares its shared scalar with $$\delta\_{\text{aud\\\_s}}$$ instead, and is separated from it by mode exclusivity rather than by a distinct scalar (§8.5). +This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16. It takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ because the two escrows key off different shared scalars ($$Y\_{\text{op}}$$ vs $$K\_{\text{aud,s}}$$), which is the distinct-domain leg of §5.3's *Why reusing $$r\_e$$ is safe*. It shares its shared scalar with $$\delta\_{\text{aud\\\_s}}$$ instead, and what separates the two is simply that they are different tags: the tag is the first absorbed element, so no pair of remaining inputs makes tag 17 and tag 11 agree. Mode exclusivity is a separate per-tag rule that each satisfies on its own — 17 is only ever single-output, 11 only ever multi-lane (§8.5). **Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag ever squeezed three-wide, and the widths it is read at agree on their shared lanes (§2.5 *Mode exclusivity*). 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. diff --git a/packages/tokens/src/confidential/docs/INDEXER.md b/packages/tokens/src/confidential/docs/INDEXER.md index ac17d05ef..6cfdbceea 100644 --- a/packages/tokens/src/confidential/docs/INDEXER.md +++ b/packages/tokens/src/confidential/docs/INDEXER.md @@ -51,6 +51,7 @@ All events emitted by the confidential token (DESIGN_cont §11.2) with the follo | `Transfer` (recipient side) | Receiving-side replay: carries the recipient-channel ciphertexts for `(v_transfer, r_transfer)`. | | `SpenderTransfer` (recipient side) | Receiving-side replay, as above. | | `Merge` | Folds the receiving opening into the spendable opening; resets the receiving side. | +| `SetSpender`, `SpenderTransfer` (owner side) | **The auditor's only route to an allowance opening.** The escrowed allowance blinding rides these two events (`r_a_tilde_aud_s` on `SetSpender`, `r_tilde_aud_s` on `SpenderTransfer`) and appears nowhere in contract storage, so an auditor that misses one has no way to reconstruct the opening of the `C_a` it wrote — see §7 *Auditor recovery*. | | `Withdraw`, `Transfer` (sender side), `SetSpender`, `RevokeSpender` | **Checkpoints**: publish `(b_tilde, sigma)` for the owner's spendable balance. `SetSpender`/`RevokeSpender` are in scope as owner checkpoints only — a spender recovers allowance state from the on-chain delegation entry (`allowance_commitment`, `a_tilde`, `escrowed_dvk`, `allowance_salt`), not from the archive. The auditor-channel ciphertexts these events also carry are out of scope for wallet recovery. | A self-transfer — a `Transfer` whose `from` and `to` are the same account — carries both roles at once: it is a sender-side checkpoint and a recipient-side replay event, and recovery applies both (DESIGN §5.2). @@ -116,6 +117,15 @@ The indexer is trusted for **availability and completeness only** — never for - **Integrity fails closed.** Recovery ends with the wallet checking its reconstructed openings against the **on-chain** commitments (`C_spend =? v·G + r·H`, DESIGN §5.2 step 7). A tampered or incomplete history cannot produce a wrong balance that verifies; it produces a detectable mismatch. - **Withholding is the residual risk.** A malicious or broken indexer can deny recovery (a liveness failure, not a soundness one). Two structural mitigations: for the recent window the RPC is an independent source of the same events (the hybrid split of §1), so archive withholding bites only the pre-window history; and for that older history wallets SHOULD support multiple independent archive endpoints, with deployments running or contracting at least two. The auditor's forward-tracked openings (DESIGN_cont §8.1) are a second recovery source in principle, but no specification defines an auditor-to-wallet channel, so they do not relax this recommendation. +### 7.1 Auditor recovery + +An auditor's allowance tracking is strictly event-scoped and has no state-based fallback. The opening of a delegation's `C_a` is escrowed only in the event that wrote it (DESIGN_cont §8.5), so a missed, reordered, or unarchived `SetSpender` / `SpenderTransfer` leaves the auditor holding a value it cannot open until the delegation's next state-changing operation. This is a stronger dependency on the archive than the spendable side, where the auditor's opening survives merges by homomorphic carry-forward (DESIGN_cont §8.1). + +Two consequences for deployments: + +- **Consistency checking is the practical detection mechanism.** Soroban enforces nothing about archive completeness, ordering, or availability. An auditor client SHOULD verify each reconstructed allowance opening against the stored `allowance_commitment` — `C_a =? v_a·G + r_a·H` — which is the same fails-closed check §7 states for wallets. A mismatch is evidence of a missed, reordered, or pruned event rather than of a wrong balance. +- **Delegation TTL bounds the recovery anchor.** `live_until_ledger` governs spending authority and is independent of the delegation entry's persistent-entry TTL. If the entry itself is archived away, the commitment the auditor checks against is gone, so the consistency check above becomes unavailable exactly when it is most needed. Deployments SHOULD monitor delegation-entry TTL, not only `live_until_ledger`. + ## 8. Conformance and Versioning An implementation conforms to this specification iff it satisfies §3–§5 and exposes the normative capabilities C2, C3, and C4 (C1 is RECOMMENDED). This document is versioned with the protocol documentation set; breaking changes to the archived record shape or the normative capabilities bump the protocol documentation version and MUST be called out in release notes. diff --git a/packages/tokens/src/confidential/docs/OVERVIEW.md b/packages/tokens/src/confidential/docs/OVERVIEW.md index 2ce4ce9fb..693350a75 100644 --- a/packages/tokens/src/confidential/docs/OVERVIEW.md +++ b/packages/tokens/src/confidential/docs/OVERVIEW.md @@ -104,7 +104,7 @@ Spenders enable use cases like automated trading bots, payment processors, or cu |:-----|:----|:-------------| | **Setup** | | | | 1 | Owner | Specifies the spender address (which must already be a registered account in the contract, so its spending public key can be looked up for delegation key escrow), the allowance amount, and a `live_until_ledger` expiration. | -| 2 | Wallet | Generates a proof that the allowance is correctly carved out of the owner's spendable balance. The proof also covers derivation and ECDH escrow of a delegation viewing key (`dvk`) so the spender can independently track and decrypt its allowance state, and produces ciphertexts for the owner's auditor (escrow amount and post-operation balance checkpoint). | +| 2 | Wallet | Generates a proof that the allowance is correctly carved out of the owner's spendable balance. The proof also covers derivation and ECDH escrow of a delegation viewing key (`dvk`) so the spender can independently track and decrypt its allowance state, and produces ciphertexts for the owner's auditor (escrow amount, post-operation balance checkpoint, and the blinding of the allowance commitment it just wrote). | | 3 | Contract | Verifies the proof, deducts the allowance from the owner's spendable balance commitment, and stores the spender delegation (allowance commitment, encrypted allowance, escrowed `dvk`, allowance salt, expiration). Emits an event with the owner's post-operation balance checkpoint and the owner-auditor ciphertexts. | | **Operation** | | | | 4 | Spender | Initiates a confidential transfer from the escrowed allowance to any registered recipient. A proof accompanies each transfer, covering allowance sufficiency, ECDH-derived encryption for the recipient, and dual-auditor ciphertexts for the recipient's and owner's auditors. | @@ -133,7 +133,8 @@ The system supports **real-time auditing** via a dual-auditor model. Each accoun | Spender escrow / reclaim amount | n/a | Yes (owner's auditor) | | Post-transfer spender allowance | No (for spender transfers) | Yes (owner's auditor) | | Post-escrow / post-reclaim balance | n/a | Yes (owner's auditor), at spender setup and revocation respectively | -| Delegation viewing key `dvk` | No | Yes (owner's auditor), at spender setup and on every spender transfer (enables opening the allowance commitment) | +| Delegation viewing key `dvk` | No | No | +| Allowance blinding $r_a$ | No | Yes (owner's auditor), at spender setup and on every spender transfer — one state's blinding per event, which with the amount opens that event's allowance commitment | The table covers every auditor ciphertext the protocol produces; `DESIGN_cont.md` §8.1-§8.5 is the normative account, including the bounds on each opening capability. diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index 38db9149f..8a09f224a 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -101,7 +101,7 @@ The sponge construction, its width and rate, the IV placement, the padding rule, $$\text{poseidon\\\_with\\\_domain}(\delta, [x_1, \ldots, x_n]) = \text{sponge}([\delta, x_1, \ldots, x_n])$$ -Squeeze-slot assignment is canonical and MUST be followed: lane 0 is always an amount mask, lane 1 is always a balance, allowance, or per-transfer-randomness mask, and lane 2 is always the sender-auditor secret-escrow slot — the new spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`, the delegation viewing key $$dvk_i$$ on `SpenderTransfer` (DESIGN.md §2.5). Only the sender-auditor channel ($$\delta_{\text{aud\\\_s}}$$) is squeezed three-wide; the recipient channel ($$\delta_{\text{aud\\\_r}}$$) stays at two lanes. `Withdraw`, whose amount is public, takes lanes **1** and **2** and leaves lane 0 unused (DESIGN.md W_a3–W_a5), so a checkpoint pad can never coincide with an amount pad. +Squeeze-slot assignment is canonical and MUST be followed: lane 0 is always an amount mask, lane 1 is always a balance, allowance, or per-transfer-randomness mask, and lane 2 is always the sender-auditor blinding-escrow slot — the new spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`, the new allowance blinding $$r_a'$$ on `SpenderTransfer` (DESIGN.md §2.5). Lane 2 never carries a key. Only the sender-auditor channel ($$\delta_{\text{aud\\\_s}}$$) is squeezed three-wide; the recipient channel ($$\delta_{\text{aud\\\_r}}$$) stays at two lanes. `Withdraw`, whose amount is public, takes lanes **1** and **2** and leaves lane 0 unused (DESIGN.md W_a3–W_a5), so a checkpoint pad can never coincide with an amount pad. ### 4.4 Generators and commitments @@ -157,11 +157,11 @@ Secret scalars — $$\sigma$$, $$\sigma_a$$ — MUST be produced by the rejectio | $$\delta_{\text{eph}}$$ | 14 | No — derived off-circuit (DESIGN.md §5.3) | | $$\delta_{\text{disc\\\_bind}}$$ | 15 | No — off-chain disclosure only | | $$\delta_{\text{disc}}$$ | 16 | No — off-chain disclosure only | -| $$\delta_{\text{esc\\\_dvk\\\_aud}}$$ | 17 | Yes | +| $$\delta_{\text{esc\\\_allow\\\_r\\\_aud}}$$ | 17 | Yes | DESIGN_cont.md §13 assigns all seventeen values and is their only source; the right-hand column is this document's addition. $$\delta_{\text{disc\\\_bind}}$$ and $$\delta_{\text{disc}}$$ belong to the off-chain disclosure layer (SELECTIVE_DISCLOSURE.md §2.2). Tag 1 is absorbed by the contract rather than by a circuit — the contract derives $$\text{addr\\\_f}$$ and $$\text{op}_i$$ on-chain and the circuits receive them as opaque public inputs (DESIGN.md §2.7 *Usage sites*) — so it is part of the on-chain wire contract all the same. None of 14–16 is absorbed either in a circuit or on-chain, so none is part of the on-chain wire contract, but all three are part of the cross-client contract because two wallets serving the same account must agree on them (§6.3). -All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the multi-lane tags — 11 read three-wide wherever lane 2 is escrowed and two-wide on `RevokeSpender`, 12 always two-wide; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows $$dvk_i$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_dvk_auditor.json`. +All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the multi-lane tags — 11 read three-wide wherever lane 2 is escrowed and two-wide on `RevokeSpender`, 12 always two-wide; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows the allowance blinding $$r_a$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_allow_r_auditor.json`. ### 4.9 Address compression @@ -461,13 +461,13 @@ A spender MUST NOT be able to reach the owner's spendable balance through any in ## 11. Auditor Client -An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read -- including the allowance salts, which the delegation events publish ($$\sigma_a$$ on `SetSpender`, $$\sigma_a'$$ on `SpenderTransfer`), so $$r_a = \text{Poseidon}(\delta_{\text{allow\\\_r}}, dvk_i, \sigma_a)$$ and the opening of $$C_a$$ follow from the event alone (DESIGN_cont.md §8.5). For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. +An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. The allowance opening comes straight out of the event: the blinding of the $$C_a$$ that operation writes is escrowed in the event itself -- tag 17 on `SetSpender`, lane 2 on `SpenderTransfer` -- and the matching value is in the sender-channel ciphertext (DESIGN_cont.md §8.5). An auditor that did not observe the event holds no opening for that state and cannot derive one; there is no key from which the openings follow. For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. The two channels differ in what they yield (DESIGN_cont.md §8.1): | Channel | Lane 0 | Lane 1 | Lane 2 | |:--|:--|:--|:--| -| Sender / owner ($$\delta_{\text{aud\\\_s}}$$) | Transfer amount, or the escrowed amount for `SetSpender` and the reclaimed amount for `RevokeSpender` | Sender's post-operation balance, or post-operation allowance for a spender transfer | Post-operation spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`; $$dvk_i$$ on `SpenderTransfer`; nothing on `RevokeSpender`, which stays two-lane | +| Sender / owner ($$\delta_{\text{aud\\\_s}}$$) | Transfer amount, or the escrowed amount for `SetSpender` and the reclaimed amount for `RevokeSpender` | Sender's post-operation balance, or post-operation allowance for a spender transfer | Post-operation spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`; post-transfer allowance blinding $$r_a'$$ on `SpenderTransfer`; nothing on `RevokeSpender`, which stays two-lane | | Recipient ($$\delta_{\text{aud\\\_r}}$$) | Transfer amount | Per-transfer Pedersen randomness $$r_{\text{transfer}}$$ | — (channel is two-lane) | `Withdraw`, `SetSpender`, and `RevokeSpender` carry a sender-channel balance checkpoint whose pad is lane **1**. Only `Withdraw` leaves lane 0 unused, its amount being public (DESIGN.md W_a3, §4.3); `SetSpender` and `RevokeSpender` read lane 0 as well, for the escrowed and reclaimed amounts respectively (DESIGN.md S_a4, V_a4). diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index fd81e44db..44942dcda 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -307,7 +307,7 @@ A D-sender proof for a `SpenderTransfer` proves that the spender (not the owner) The auditor proves to a third party that an on-chain event corresponds to a transfer of amount $$v\_{\text{transfer}}$$ for one of the accounts under the auditor's scope. Used when the holder is uncooperative or when the disclosure recipient requires a guarantee that the auditor (not just the holder) has attested. -**Which auditor.** Every transfer carries ciphertexts under *two* auditor keys (DESIGN_cont.md §8.1): the recipient-side key $$K\_{\text{aud,r}}$$ (channel $$\delta\_{\text{aud\\\_r}}$$, two lanes yielding masks for $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$) and the sender-side key $$K\_{\text{aud,s}}$$ (channel $$\delta\_{\text{aud\\\_s}}$$, three lanes yielding masks for $$v\_{\text{transfer}}$$, the sender's post-transfer balance, and the secret-escrow slot of DESIGN.md §2.5). Whichever auditor is disclosing reuses the same shared-secret derivation they perform to read events natively; the circuit additionally encrypts the result to the disclosure recipient. +**Which auditor.** Every transfer carries ciphertexts under *two* auditor keys (DESIGN_cont.md §8.1): the recipient-side key $$K\_{\text{aud,r}}$$ (channel $$\delta\_{\text{aud\\\_r}}$$, two lanes yielding masks for $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$) and the sender-side key $$K\_{\text{aud,s}}$$ (channel $$\delta\_{\text{aud\\\_s}}$$, three lanes yielding masks for $$v\_{\text{transfer}}$$, the sender's post-transfer balance, and the blinding-escrow slot of DESIGN.md §2.5). Whichever auditor is disclosing reuses the same shared-secret derivation they perform to read events natively; the circuit additionally encrypts the result to the disclosure recipient. The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{\delta\_{\text{aud\\\_r}}, \delta\_{\text{aud\\\_s}}\\}$$ and the corresponding event ciphertext as $$\tilde{v}\_{\text{aud}} \in \\{\tilde{v}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}\\}$$. In each case the amount mask is lane 0 of the channel's sponge; the remaining lanes ($$m\_{r,r}$$ on the recipient channel; $$m\_{b,s}$$ and $$m\_{r,s}$$ on the sender channel) are computed and discarded for an amount disclosure, or used in place of lane 0 for the balance/randomness variants noted below. A3 is written with $$\text{SpongeSqueeze}\_2$$ because its first two lanes coincide with those of $$\text{SpongeSqueeze}\_3$$ on either channel (DESIGN.md §2.5); only a variant reading lane 2 squeezes three-wide. diff --git a/packages/tokens/src/confidential/mod.rs b/packages/tokens/src/confidential/mod.rs index 536737be6..405953ba6 100644 --- a/packages/tokens/src/confidential/mod.rs +++ b/packages/tokens/src/confidential/mod.rs @@ -402,7 +402,7 @@ pub trait ConfidentialToken { /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` /// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, - /// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` + /// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, r_tilde_aud_s]` fn confidential_transfer_from( e: &Env, spender: Address, @@ -449,7 +449,7 @@ pub trait ConfidentialToken { /// /// * topics - `["set_spender", account: Address, spender: Address]` /// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, - /// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` + /// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` fn set_spender( e: &Env, account: Address, @@ -762,7 +762,7 @@ pub struct SpenderTransfer { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, - pub dvk_cipher_aud: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Emits an `SpenderTransfer` event. @@ -780,7 +780,7 @@ pub fn emit_spender_transfer( r_tilde_aud_r: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, a_tilde_aud_s: &BytesN<32>, - dvk_cipher_aud: &BytesN<32>, + r_tilde_aud_s: &BytesN<32>, ) { SpenderTransfer { spender: spender.clone(), @@ -794,7 +794,7 @@ pub fn emit_spender_transfer( r_tilde_aud_r: r_tilde_aud_r.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), a_tilde_aud_s: a_tilde_aud_s.clone(), - dvk_cipher_aud: dvk_cipher_aud.clone(), + r_tilde_aud_s: r_tilde_aud_s.clone(), } .publish(e); } @@ -815,7 +815,7 @@ pub struct SetSpender { pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, pub r_tilde_aud_s: BytesN<32>, - pub dvk_cipher_aud: BytesN<32>, + pub r_a_tilde_aud_s: BytesN<32>, } /// Emits a `SetSpender` event. @@ -832,7 +832,7 @@ pub fn emit_set_spender( v_tilde_aud_s: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, r_tilde_aud_s: &BytesN<32>, - dvk_cipher_aud: &BytesN<32>, + r_a_tilde_aud_s: &BytesN<32>, ) { SetSpender { account: account.clone(), @@ -845,7 +845,7 @@ pub fn emit_set_spender( v_tilde_aud_s: v_tilde_aud_s.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), r_tilde_aud_s: r_tilde_aud_s.clone(), - dvk_cipher_aud: dvk_cipher_aud.clone(), + r_a_tilde_aud_s: r_a_tilde_aud_s.clone(), } .publish(e); } diff --git a/packages/tokens/src/confidential/storage.rs b/packages/tokens/src/confidential/storage.rs index 60c8ef471..ed48ef644 100644 --- a/packages/tokens/src/confidential/storage.rs +++ b/packages/tokens/src/confidential/storage.rs @@ -173,9 +173,9 @@ pub struct SpenderTransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, - /// Owner-auditor secret-escrow slot (sponge lane 2): the delegation - /// viewing key re-escrowed as `dvk_i + m_r_s` (constraint O_a9). - pub dvk_cipher_aud: BytesN<32>, + /// Owner-auditor blinding-escrow slot (sponge lane 2): the blinding of + /// the new allowance commitment, `r_a' + m_r_s` (constraint O_a9). + pub r_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -204,10 +204,12 @@ pub struct SetSpenderPayload { /// Owner-auditor secret-escrow slot (sponge lane 2): the blinding of the /// new spendable commitment, `r' + m_r_s` (constraint S_a6). pub r_tilde_aud_s: BytesN<32>, - /// Owner-auditor escrow of the delegation viewing key, - /// `dvk_i + Poseidon2(delta_esc_dvk_aud, s_a_s, op_i)` (constraint S14). - /// Distinct from `escrowed_dvk`, which is the *spender*-side escrow. - pub dvk_cipher_aud: BytesN<32>, + /// Owner-auditor escrow of the allowance blinding, + /// `r_a + Poseidon2(delta_esc_allow_r_aud, s_a_s, op_i)` (constraint + /// S14). Distinct from `r_tilde_aud_s` above, which escrows the *spendable* + /// blinding, and from `escrowed_dvk`, which hands the *spender* the + /// delegation viewing key. + pub r_a_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -782,7 +784,7 @@ pub fn confidential_transfer( /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` /// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, -/// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, dvk_cipher_aud]` +/// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, r_tilde_aud_s]` /// /// # Security Warning /// @@ -817,7 +819,7 @@ pub fn confidential_transfer_from( // C_a, sigma_a, Y_op, PVK_recipient, K_aud_r, K_aud_s, // C_a', C_transfer, R_e, v_tilde, a_tilde', sigma_a', // v_tilde_aud_r, r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, - // dvk_cipher_aud + // r_tilde_aud_s let mut pi = Bytes::new(e); append_point(&mut pi, &delegation.allowance_commitment); append_field(&mut pi, &delegation.allowance_salt); @@ -835,7 +837,7 @@ pub fn confidential_transfer_from( append_field(&mut pi, &payload.r_tilde_aud_r); append_field(&mut pi, &payload.v_tilde_aud_s); append_field(&mut pi, &payload.a_tilde_aud_s); - append_field(&mut pi, &payload.dvk_cipher_aud); + append_field(&mut pi, &payload.r_tilde_aud_s); verify(e, CircuitType::SpenderTransfer, &pi, proof); @@ -862,7 +864,7 @@ pub fn confidential_transfer_from( &payload.r_tilde_aud_r, &payload.v_tilde_aud_s, &payload.a_tilde_aud_s, - &payload.dvk_cipher_aud, + &payload.r_tilde_aud_s, ); } @@ -898,7 +900,7 @@ pub fn confidential_transfer_from( /// /// * topics - `["set_spender", account: Address, spender: Address]` /// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, -/// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, dvk_cipher_aud]` +/// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` /// /// # Security Warning /// @@ -923,7 +925,7 @@ pub fn set_spender( // C_spend, Y, Y_op, spender_id (op_i), addr_f, K_aud_s, // C_spend', C_a, escrowed_dvk, b_tilde, a_tilde, // sigma, sigma_a, R_e, v_tilde_aud_s, b_tilde_aud_s, - // r_tilde_aud_s, dvk_cipher_aud + // r_tilde_aud_s, r_a_tilde_aud_s let mut pi = Bytes::new(e); append_point(&mut pi, &owner.spendable_commitment); append_point(&mut pi, &owner.spending_public_key); @@ -942,7 +944,7 @@ pub fn set_spender( append_field(&mut pi, &payload.v_tilde_aud_s); append_field(&mut pi, &payload.b_tilde_aud_s); append_field(&mut pi, &payload.r_tilde_aud_s); - append_field(&mut pi, &payload.dvk_cipher_aud); + append_field(&mut pi, &payload.r_a_tilde_aud_s); verify(e, CircuitType::SetSpender, &pi, proof); @@ -972,7 +974,7 @@ pub fn set_spender( &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, &payload.r_tilde_aud_s, - &payload.dvk_cipher_aud, + &payload.r_a_tilde_aud_s, ); } diff --git a/packages/tokens/src/confidential/test.rs b/packages/tokens/src/confidential/test.rs index 849f8ca56..0216a51ca 100644 --- a/packages/tokens/src/confidential/test.rs +++ b/packages/tokens/src/confidential/test.rs @@ -13,14 +13,26 @@ use crate::confidential::{ storage as token_storage, verifier::{CircuitType, ConfidentialVerifier}, ConfidentialAccount, ConfidentialToken, ConfidentialTokenClient, NoHooks, RegisterData, - RegisterPayload, RevokeSpenderData, RevokeSpenderPayload, SetSpender, SetSpenderData, - SetSpenderPayload, SpenderDelegation, SpenderTransfer, SpenderTransferData, + RegisterPayload, RevokeSpender, RevokeSpenderData, RevokeSpenderPayload, SetSpender, + SetSpenderData, SetSpenderPayload, SpenderDelegation, SpenderTransfer, SpenderTransferData, SpenderTransferPayload, Transfer, TransferData, TransferPayload, Withdraw, WithdrawData, WithdrawPayload, }; // ################## TEST FIXTURES ################## +/// `-G = (1, r - Y)`, the negation of [`GRUMPKIN_G_BYTES`]. On-curve and +/// canonical, and distinct from `G`, so it stands in for a rotated-in auditor +/// key wherever a test needs two different key versions. +const GRUMPKIN_NEG_G_BYTES: [u8; 64] = [ + // x = 1 (32-byte big-endian) + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01, + // r - y (32-byte big-endian) + 0x30, 0x64, 0x4e, 0x72, 0xe1, 0x31, 0xa0, 0x26, 0xe9, 0x3c, 0xe7, 0x41, 0x7a, 0xdc, 0xfa, 0xf9, + 0xfb, 0x0c, 0xdb, 0x02, 0x88, 0xa1, 0x5d, 0xfc, 0xc0, 0xa2, 0x31, 0x06, 0x6d, 0xc0, 0xd8, 0xd5, +]; + /// Grumpkin generator `G = (1, Y)` with `Y = /// 17631683881184975370165255887551781615748388533673675138860`. Used as a /// canonical on-curve fixture for both auditor keys and account keys in @@ -38,6 +50,10 @@ fn fixture_point(e: &Env) -> BytesN<64> { BytesN::from_array(e, &GRUMPKIN_G_BYTES) } +fn rotated_fixture_point(e: &Env) -> BytesN<64> { + BytesN::from_array(e, &GRUMPKIN_NEG_G_BYTES) +} + fn fixture_field(e: &Env, byte: u8) -> BytesN<32> { let mut bytes = [byte; 32]; // Zero the top byte so the 256-bit value is bounded by 2^248 - 1, @@ -144,6 +160,35 @@ impl ConfidentialVerifier for ReplayGuardVerifier { } } +/// Records the auditor key each proof was verified against. +/// +/// The escrowed allowance opening is produced under whichever auditor key is +/// registered at the moment of the operation, so "which key version can read +/// this event" reduces, at the contract level, to "which `K_aud_s` went into +/// the public-input blob". This mock captures exactly that. `K_aud_s` sits at +/// limb 8 of the SetSpender blob and limb 9 of the SpenderTransfer and +/// RevokeSpender blobs (DESIGN §7.7 - §7.9). +#[contract] +struct KeyRecordingVerifier; + +#[contractimpl(contracttrait)] +impl ConfidentialVerifier for KeyRecordingVerifier { + fn register_verification_key(_e: &Env, _ct: CircuitType, _vk: Bytes, _op: Address) {} + + fn update_verification_key(_e: &Env, _ct: CircuitType, _vk: Bytes, _op: Address) {} + + fn verify_proof(e: &Env, ct: CircuitType, pi: Bytes, _proof: Bytes) -> bool { + let (key, offset) = match ct { + CircuitType::SetSpender => (symbol_short!("k_set"), 8u32 * 32), + CircuitType::SpenderTransfer => (symbol_short!("k_xfer"), 9u32 * 32), + CircuitType::RevokeSpender => (symbol_short!("k_revoke"), 9u32 * 32), + _ => return true, + }; + e.storage().instance().set(&key, &pi.slice(offset..offset + 64)); + true + } +} + #[contract] struct MockAuditor; @@ -167,6 +212,7 @@ struct Harness<'a> { token_admin: Address, sac: StellarAssetClient<'a>, sac_addr: Address, + auditor_addr: Address, } fn setup<'a>() -> Harness<'a> { @@ -201,7 +247,7 @@ fn setup_with_verifier_addr<'a>(e: Env, verifier_addr: Address) -> Harness<'a> { e.register(TokenContract, (sac_addr.clone(), verifier_addr.clone(), auditor_addr.clone())); let token = ConfidentialTokenClient::new(&e, &token_addr); - Harness { e, token, token_addr, token_admin, sac: sac_client, sac_addr } + Harness { e, token, token_addr, token_admin, sac: sac_client, sac_addr, auditor_addr } } fn register_data(e: &Env) -> Bytes { @@ -261,7 +307,7 @@ fn set_spender_data(e: &Env) -> Bytes { v_tilde_aud_s: fixture_field(e, 0x25), b_tilde_aud_s: fixture_field(e, 0x26), r_tilde_aud_s: fixture_field(e, 0x27), - dvk_cipher_aud: fixture_field(e, 0x28), + r_a_tilde_aud_s: fixture_field(e, 0x28), }, proof: Bytes::new(e), } @@ -281,7 +327,7 @@ fn spender_transfer_data(e: &Env) -> Bytes { r_tilde_aud_r: fixture_field(e, 0x35), v_tilde_aud_s: fixture_field(e, 0x36), a_tilde_aud_s: fixture_field(e, 0x37), - dvk_cipher_aud: fixture_field(e, 0x38), + r_tilde_aud_s: fixture_field(e, 0x38), }, proof: Bytes::new(e), } @@ -661,7 +707,7 @@ fn set_spender_stores_delegation() { v_tilde_aud_s: fixture_field(&h.e, 0x25), b_tilde_aud_s: fixture_field(&h.e, 0x26), r_tilde_aud_s: fixture_field(&h.e, 0x27), - dvk_cipher_aud: fixture_field(&h.e, 0x28), + r_a_tilde_aud_s: fixture_field(&h.e, 0x28), } .to_xdr(&h.e, &h.token_addr) ); @@ -719,6 +765,124 @@ fn get_spender_delegation_unknown_panics() { h.token.get_spender_delegation(&alice, &spender); } +// ################## AUDITOR-KEY ROTATION ################## + +#[test] +fn auditor_key_rotation_rescopes_the_escrowed_allowance_opening() { + // The auditor's allowance opening is escrowed per event, not per + // delegation: every state-changing operation re-encrypts it under + // whichever auditor key is registered at that moment. So rotation is + // event-scoped in both directions -- K2 gets nothing retroactively, and + // K1 keeps whatever it already decrypted. + // + // Proofs are mocked here, so what this pins is the contract-level half of + // that claim: which key version each operation's ciphertexts were produced + // for, and which operations produce an allowance escrow at all. + let e = Env::default(); + let verifier_addr = e.register(KeyRecordingVerifier, ()); + let h = setup_with_verifier_addr(e, verifier_addr.clone()); + let alice = Address::generate(&h.e); + let spender = Address::generate(&h.e); + let bob = Address::generate(&h.e); + + let k1 = fixture_point(&h.e); + let k2 = rotated_fixture_point(&h.e); + let recorded = |key: soroban_sdk::Symbol| -> Bytes { + h.e.as_contract(&verifier_addr, || h.e.storage().instance().get::<_, Bytes>(&key).unwrap()) + }; + + h.token.register(&alice, &1u32, ®ister_data(&h.e)); + h.token.register(&spender, &1u32, ®ister_data(&h.e)); + h.token.register(&bob, &1u32, ®ister_data(&h.e)); + + // Step 1 -- delegate under K1. S14's escrow of r_a rides this event. + // The event buffer is scoped to the last top-level invocation, so it has + // to be read before the `as_contract` peek at the verifier's storage. + h.token.set_spender(&alice, &spender, &1_000u32, &set_spender_data(&h.e)); + let set_events = h.e.events().all(); + assert_eq!( + set_events.events().first().unwrap(), + &SetSpender { + account: alice.clone(), + spender: spender.clone(), + live_until_ledger: 1_000u32, + r_e_point: fixture_point(&h.e), + sigma: fixture_field(&h.e, 0x23), + sigma_a: fixture_field(&h.e, 0x24), + b_tilde: fixture_field(&h.e, 0x21), + v_tilde_aud_s: fixture_field(&h.e, 0x25), + b_tilde_aud_s: fixture_field(&h.e, 0x26), + r_tilde_aud_s: fixture_field(&h.e, 0x27), + r_a_tilde_aud_s: fixture_field(&h.e, 0x28), + } + .to_xdr(&h.e, &h.token_addr) + ); + assert_eq!(recorded(symbol_short!("k_set")), k1.clone().into()); + + // Step 2 -- rotate the owner's auditor key. Nothing about the live + // delegation changes: no event, no new ciphertext, and the on-chain C_a is + // untouched. K2 holds no opening for it yet. + let auditor = + crate::confidential::auditor::ConfidentialAuditorClient::new(&h.e, &h.auditor_addr); + auditor.rotate_key(&1u32, &k2, &Address::generate(&h.e)); + let before = h.token.get_spender_delegation(&alice, &spender); + + // Step 3 -- the first post-rotation state change re-anchors K2. O_a9's + // escrow of r_a' is verified against K2, so this single event is what + // gives the rotated-in key an opening of the allowance it did not see + // created. + h.token.confidential_transfer_from(&spender, &alice, &bob, &spender_transfer_data(&h.e)); + let xfer_events = h.e.events().all(); + assert_eq!( + xfer_events.events().first().unwrap(), + &SpenderTransfer { + spender: spender.clone(), + from: alice.clone(), + to: bob.clone(), + r_e_point: fixture_point(&h.e), + v_tilde: fixture_field(&h.e, 0x31), + sigma_a: fixture_field(&h.e, 0x24), + sigma_a_new: fixture_field(&h.e, 0x33), + v_tilde_aud_r: fixture_field(&h.e, 0x34), + r_tilde_aud_r: fixture_field(&h.e, 0x35), + v_tilde_aud_s: fixture_field(&h.e, 0x36), + a_tilde_aud_s: fixture_field(&h.e, 0x37), + r_tilde_aud_s: fixture_field(&h.e, 0x38), + } + .to_xdr(&h.e, &h.token_addr) + ); + assert_eq!(recorded(symbol_short!("k_xfer")), k2.clone().into()); + // The delegation moved to a new state, so the opening K2 just received is + // an opening of the CURRENT C_a, not of the one K1 saw. (The commitments + // themselves are the same canonical fixture point under a mocked verifier; + // the salt is what distinguishes the two states here.) + let after = h.token.get_spender_delegation(&alice, &spender); + assert_ne!(before.allowance_salt, after.allowance_salt); + + // Step 4 -- revocation runs under K2 too, but RevokeSpender reads only two + // sponge lanes (V_a3): its event carries no allowance escrow at all. An + // auditor that missed step 3 gets no second chance here -- it has to fold + // the allowance it already holds into C_spend, or wait for the owner's + // next checkpoint. This is the field list, and the absence is the point. + h.token.revoke_spender(&alice, &spender, &revoke_spender_data(&h.e)); + let revoke_events = h.e.events().all(); + assert_eq!( + revoke_events.events().first().unwrap(), + &RevokeSpender { + account: alice.clone(), + spender: spender.clone(), + r_e_point: fixture_point(&h.e), + sigma: fixture_field(&h.e, 0x42), + b_tilde: fixture_field(&h.e, 0x41), + v_tilde_aud_s: fixture_field(&h.e, 0x43), + b_tilde_aud_s: fixture_field(&h.e, 0x44), + } + .to_xdr(&h.e, &h.token_addr) + ); + assert_eq!(recorded(symbol_short!("k_revoke")), k2.into()); + assert!(!h.token.is_spender(&alice, &spender)); +} + // ################## SPENDER TRANSFER ################## #[test] @@ -751,7 +915,7 @@ fn confidential_transfer_from_updates_delegation_and_recipient() { r_tilde_aud_r: fixture_field(&h.e, 0x35), v_tilde_aud_s: fixture_field(&h.e, 0x36), a_tilde_aud_s: fixture_field(&h.e, 0x37), - dvk_cipher_aud: fixture_field(&h.e, 0x38), + r_tilde_aud_s: fixture_field(&h.e, 0x38), } .to_xdr(&h.e, &h.token_addr) ); From 430d682930d751083c9817861418c7a07a74b012 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Thu, 27 Aug 2026 17:49:05 +0200 Subject: [PATCH 09/12] docs(confidential): fix stale auditor-side dvk_i escrow references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seven spots still described S14 and SpenderTransfer lane 2 as escrowing dvk_i. The SELECTIVE_DISCLOSURE §8 note was the substantive one: it ruled out a lane-2 disclosure variant on the grounds that the lane masks a key, which no longer holds now that it carries r_a'. --- packages/tokens/src/confidential/docs/DESIGN_cont.md | 6 +++--- .../tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 13405e131..412fb32cf 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -264,7 +264,7 @@ The dominant cost in Noir circuits is elliptic curve scalar multiplication. With | `SetSpender` | 7 | $$Y$$ (S1), $$C\_{\text{spend}}$$ opening (S3), $$C\_a$$ (S7), $$C\_{\text{spend}}'$$ (S10), $$R\_e$$ (S\_a1), $$dvk\_i$$ escrow ECDH (S12, §7.11), owner-auditor ECDH (S\_a2) | | `RevokeSpender` | 6 | $$Y$$ (V1), $$C\_a$$ opening (V4), $$C\_{\text{spend}}$$ opening (V5), $$C\_{\text{spend}}'$$ (V7), $$R\_e$$ (V\_a1), owner-auditor ECDH (V\_a2) | -`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The auditor-side escrow of $$dvk\_i$$ (S14) reuses the S\_a2 shared scalar and adds a Poseidon evaluation, not a call. The lane-2 escrows (W\_a5, T\_a9, S\_a6, O\_a9) read a third lane of a permutation each circuit already computes and cost one field addition apiece. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 95, `RevokeSpender` 123, `Transfer` 134, `SetSpender` 135, `SpenderTransfer` 136. +`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The auditor-side escrow of the allowance blinding $$r\_a$$ (S14) reuses the S\_a2 shared scalar and adds a Poseidon evaluation, not a call. The lane-2 escrows (W\_a5, T\_a9, S\_a6, O\_a9) read a third lane of a permutation each circuit already computes and cost one field addition apiece. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 95, `RevokeSpender` 123, `Transfer` 134, `SetSpender` 135, `SpenderTransfer` 136. 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). @@ -448,8 +448,8 @@ This table is authoritative: every entry is exactly the set of prover-supplied p | `register` | $$Y$$, $$\text{PVK}$$, `proof` | | `withdraw` | $$C\_{\text{spend}}'$$, $$\tilde{b}$$, $$R\_e$$, $$\sigma$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, `proof` | | `confidential_transfer` | $$C\_{\text{spend}}'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, `proof` | -| `confidential_transfer_from` | $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\text{dvk\\\_cipher\\\_aud}$$, `proof` | -| `set_spender` | $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$, `proof` | +| `confidential_transfer_from` | $$C\_a'$$, $$C\_{\text{transfer}}$$, $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, `proof` | +| `set_spender` | $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$, `proof` | | `revoke_spender` | $$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 contract loads it from the `(from, spender)` 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_spender`, by contrast, has no prior delegation entry to load from, so its $$\sigma\_a$$ is prover-supplied and bound by S6. diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 44942dcda..1ab37b748 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -74,7 +74,7 @@ This document reuses the notation, key hierarchy, and commitment scheme from DES - $$sk\_A$$, $$vk\_A$$, $$\text{PVK}\_A$$: an account's spending key, viewing key, and public viewing key (DESIGN.md §4). - $$\text{addr\\\_f}$$: the contract's compressed address Field $$\text{address\\\_to\\\_field}(\text{contract})$$, bound into $$vk$$ derivation (DESIGN.md §2.7, §4.2). Stored once at construction in the contract's instance storage (DESIGN.md §3.5). - $$K\_{\text{aud,s}}$$, $$K\_{\text{aud,r}}$$, $$aud\_{sk}$$: the sender-side and recipient-side auditor Grumpkin public keys, and an auditor's secret key (DESIGN_cont.md §8.1, §8.3). Each account selects an `auditor_id` at registration; the same `auditor_id` may resolve to either role depending on the transfer's direction. -- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, the post-transfer allowance salt $$\sigma\_a'$$ is emitted alongside it, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. +- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, the post-transfer allowance salt $$\sigma\_a'$$ is emitted alongside it, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, with lane 2's $$\tilde{r}\_{\text{aud,s}}$$ carrying the post-transfer allowance blinding $$r\_a'$$ rather than a spendable one (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. - $$H$$: the Grumpkin Pedersen generator used uniformly for key derivation and ECDH (DESIGN.md §2.3, §2.4). ### 2.1 Disclosure Recipient @@ -174,7 +174,7 @@ Given a bundle for $$(P\_R, \nu)$$ that this verifier previously issued, the rec 1. **Resolve the event.** Look up $$\text{ref}\_E$$ via the indexer or via direct RPC of the transaction. The lookup MUST return exactly one event whose contract address equals the deployed confidential-token contract. Extract the event's payload fields verbatim: - For `Transfer`: `from`, `to`, $$R\_e$$, $$\sigma$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). - - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN_cont.md §11.2). + - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). Any other event type, or a `circuit_id` whose constraints reference a field the event does not carry, is rejected here. @@ -337,7 +337,7 @@ D-auditor does not bind to an account record; the auditor key already binds the **Verifier flow.** Follow §5.3 with `circuit_id = D-auditor` (or the chosen balance / randomness variant). Step 2 is skipped — no $$\text{PVK}\_A$$ lookup is needed. Step 3 resolves $$K\_{\text{aud}}$$ at the event's ledger: $$K\_{\text{aud,r}}$$ from the `auditor_id` on the event's `to` account when disclosing the recipient-side channel, or $$K\_{\text{aud,s}}$$ from the `auditor_id` on the `from` account when disclosing the sender-side channel. `from` is the funds' owner in both `Transfer` and `SpenderTransfer`, since the sender-auditor channel always tracks the owner (DESIGN.md §7.8). -**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries $$\text{dvk\\\_cipher\\\_aud}$$ (DESIGN.md §7.8 O\_a9), which masks a key rather than a datum; no disclosure variant reads it, since revealing $$dvk\_i$$ would grant the recipient standing visibility into the delegation instead of one fact about one event. These variants are not separately tabulated. +**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries the post-transfer allowance blinding $$r\_a'$$ (DESIGN.md §7.8 O\_a9), so the same variant discloses the opening of $$C\_a'$$ rather than of $$C\_{\text{spend}}'$$ (DESIGN_cont.md §8.5). These variants are not separately tabulated. --- @@ -432,7 +432,7 @@ The confidential-token contract requires no new state-modifying entry points to |:---|:---|:---| | `confidential_balance(account) -> ConfidentialAccount` | Verifier extracts $$\text{PVK}\_A$$ (and $$\text{PVK}\_B$$ for D-sender, $$C\_{\text{spend}}$$ for D-balance) from the returned `ConfidentialAccount` tuple | Already exposed (DESIGN_cont.md §11.3); the struct carries every field this layer reads, so no narrower accessor is required | | Auditor contract's key lookup for `auditor_id` | Verifier looks up $$K\_{\text{aud,r}}$$ or $$K\_{\text{aud,s}}$$ | Already exposed (DESIGN_cont.md §8.3). The auditor contract MAY maintain a sequence of versioned keys per `auditor_id` with activation ledgers; the verifier MUST select the version whose activation ledger is the largest value not exceeding the disclosed event's ledger (DESIGN_cont.md §8.3, *Auditor's off-chain obligation*). | -| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ / $$\text{dvk\\\_cipher\\\_aud}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, additionally emits $$\sigma\_a'$$, and uses $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$ and $$\text{dvk\\\_cipher\\\_aud}$$ in place of $$\tilde{r}\_{\text{aud,s}}$$. | +| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, additionally emits $$\sigma\_a'$$, and uses $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$. | | Instance storage: $$\text{addr\\\_f}$$ | D-recipient, D-sender, and D-balance bind $$vk$$ derivation to the contract via $$\text{addr\\\_f}$$ | Computed once at construction (DESIGN.md §3.5); the verifier reproduces it from the contract address using the encoding in DESIGN.md §2.7 | These are the only on-chain dependencies. Disclosure proofs are otherwise self-contained off-chain artifacts. From a79b431175525e5b98d3b5497e04ba29a036564e Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:04:02 +0200 Subject: [PATCH 10/12] =?UTF-8?q?docs(confidential):=20drop=20the=20restat?= =?UTF-8?q?ed=20blast-radius=20paragraph=20in=20=C2=A78.5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The leak scope it describes is already stated by the dvk_i comparison paragraph above it, which ends "a leaked r_a ciphertext costs one state". --- packages/tokens/src/confidential/docs/DESIGN_cont.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 412fb32cf..1410ba95c 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -130,8 +130,6 @@ A single-output pad rather than a sponge lane, because lane 2 of this channel is - A freshly rotated-in key holds no opening for a live delegation. It acquires one at that delegation's next state-changing operation, when a fresh escrow is produced under it, and not before -- rotation does not re-anchor anything on its own. - A retired key does **not** lose track of state it already opened. Proofless folds -- merge (§7.4), and `revoke_spender` where the deployment runs it without a proof -- recompute $$C\_{\text{spend}}' = C\_{\text{spend}} + C\_a$$ publicly, emitting no new encrypted checkpoint, so a holder of an already-known opening carries it forward by the same addition the contract performs. Containment here is **event-scoped, not state-scoped**: rotation bounds what a key can newly decrypt, not what its holder can still compute. -What the escrow does buy against a leak is blast radius. Exposing one escrowed $$r\_a$$ exposes one allowance state; the auditor's other allowance states, and every other delegation under the same key, are unaffected. - **Archive dependence.** The auditor must have *observed* the event. There is no state-based fallback: the escrowed blinding lives in the event and nowhere in contract storage, so an auditor that missed a delegation event cannot recover that opening from `a_tilde` and $$\sigma\_a$$ the way a $$dvk\_i$$ holder could. This extends an existing dependency rather than creating one -- DESIGN.md §5.2 already makes a durable event archive normative, and the recipient-side reconstruction, the spendable checkpoint, and the merge carry-forward are all event-scoped already. [INDEXER.md](./INDEXER.md) §7.1 states the operational consequences: a consistency check of each reconstructed opening against the stored `allowance_commitment` is the practical detection mechanism for a missed or reordered event, and delegation-entry TTL bounds how long that check remains possible. --- From d35f34734b1098559536e38d05ae37787bce6b80 Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:30:08 +0200 Subject: [PATCH 11/12] fix(confidential): key spender-transfer pads to the fresh allowance salt A reverted `confidential_transfer_from` leaves the delegation entry untouched, so the stored `sigma_a` recurred on the retry along with the ephemeral scalar and every channel pad derived from it, O_a9's lane[2] included; a retry that changed the amount published the difference in the clear. O7, O9, O_a2 and O_a6 now absorb the prover-chosen `sigma_a'`, which `SpenderTransfer` emits in place of the stored salt, and `SetSpender` drops its salt since no auditor path reads either. Documents the rotation assert as O14 and regenerates the spender-transfer VK and fixtures. --- .../circuits/spender_transfer/src/main.nr | 84 ++++---- .../circuits/spender_transfer/src/tests.nr | 183 ++++++++++-------- .../circuits/vks/spender_transfer.vk.json | 2 +- .../tokens/src/confidential/docs/DESIGN.md | 29 +-- .../src/confidential/docs/DESIGN_cont.md | 26 +-- packages/tokens/src/confidential/docs/SDK.md | 6 +- .../confidential/docs/SELECTIVE_DISCLOSURE.md | 18 +- packages/tokens/src/confidential/mod.rs | 10 +- packages/tokens/src/confidential/storage.rs | 25 +-- packages/tokens/src/confidential/test.rs | 51 ++++- 10 files changed, 236 insertions(+), 198 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr index 443be493c..a5cb8187a 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/main.nr @@ -36,20 +36,23 @@ mod tests; // O6 R_e = r_e * H Ephemeral public // key. // O7 r_transfer = Poseidon2(delta_transfer_blind, s, -// sigma_a) Anti-poisoning +// sigma_a') Anti-poisoning // binding for the // transfer blinding. // O8 C_transfer = v_transfer * G + r_transfer * H Transfer // commitment. // O9 v_tilde = v_transfer // + Poseidon2(delta_transfer_amount, s, -// sigma_a) Encrypted amount +// sigma_a') Encrypted amount // (emitted). // Allowance state (new) -// -- sigma_a' != sigma_a Salt rotation -// (implementation -// hardening; not in -// Section 7.8). +// O14 sigma_a' != sigma_a Nonce rotation: +// rejects keying the +// pads to the salt the +// previous transfer +// already used +// (Section 6.2 +// *Transfer nonce*). // O10 r_a' = Poseidon2(delta_allow_r, dvk_i, // sigma_a') New allowance // randomness. @@ -63,14 +66,18 @@ mod tests; // Nonzero ephemeral // O13 r_e != 0 Rules out R_e = O // and S, S_{a,r}, -// S_{a,s} = O. +// S_{a,s} = O; every +// pad would collapse +// to a constant +// function of +// sigma_a'. // Auditor block (dual-channel visibility, Section 8.1 + Section 8.4) // O_a1 s_{a,r} = ecdh(r_e, K_aud_r) Recipient-auditor // ECDH shared scalar // (reuses r_e). // O_a2 (m_v_r, m_r_r) // = SpongeSqueeze_2(delta_aud_r, -// s_{a,r}, sigma_a) Recipient-channel +// s_{a,r}, sigma_a') Recipient-channel // sponge: two masks. // O_a3 v_tilde_aud_r = v_transfer + m_v_r Recipient-auditor // encrypted amount. @@ -88,7 +95,7 @@ mod tests; // 8.4). // O_a6 (m_v_s, m_a_s, m_r_s) // = SpongeSqueeze_3(delta_aud_s, -// s_{a,s}, sigma_a) Owner-channel +// s_{a,s}, sigma_a') Owner-channel // sponge: three masks. // O_a7 v_tilde_aud_s = v_transfer + m_v_s Owner-auditor // encrypted amount. @@ -109,25 +116,13 @@ mod tests; // owner's auditor the // opening of the C_a' // this operation -// writes. -// Note the apparent -// mismatch: the -// lane-2 pad keys off -// the CONSUMED salt -// sigma_a (O_a6), -// while the escrowed -// value is derived -// from the WRITTEN -// salt sigma_a'. That -// is sound -- the pad -// only needs to be -// fresh and unknown -// to anyone but the -// auditor, and -// (s_{a,s}, sigma_a) -// is both, since -// s_{a,s} is fresh -// per operation. +// writes. Pad and +// plaintext both key +// off sigma_a', under +// different domain +// tags and different +// secrets (s_{a,s} +// vs dvk_i). // Buys one-operation // convergence for a // live delegation @@ -153,15 +148,19 @@ mod tests; // input and was constructed with contract-specific randomness, a proof // generated against one contract'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 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'`. +// Channel nonce (Section 6.2 *Transfer nonce*) +// -------------------------------------------- +// The stored `sigma_a` opens the current allowance (O3) and nothing else. +// Every pad this circuit derives absorbs the prover-chosen replacement +// `sigma_a'` instead: the recipient ECDH chain (O7 / O9), both auditor-channel +// sponges (O_a2 / O_a6, hence O_a9's lane[2] pad), and the new-allowance +// derivations (O10 / O12). A reverted call leaves the delegation entry +// untouched, so `sigma_a` is forced on the retry; keying the pads to it would +// repeat r_e and every mask, and a retry that changed the amount would publish +// the difference in the clear. `sigma_a'` is constrained only against +// prover-supplied state, so a retry re-samples it freely. O14 rejects the one +// adjacent collision; global non-repetition of `sigma_a'` over the +// delegation's lifetime is a client obligation (SDK.md Section 10.4). // // Point-validation doctrine (Section 10.8) // ---------------------------------------- @@ -210,6 +209,7 @@ mod tests; // 18 a_tilde_new a_tilde' Prover-supplied encrypted new // allowance scalar; stored. // 19 sigma_a_new sigma_a' Prover-supplied fresh salt; +// channel nonce for every pad; // written to allowance_salt. // 20 v_tilde_aud_r v_tilde_aud_r Prover-supplied recipient- // auditor encrypted amount; @@ -310,7 +310,7 @@ fn main( let s = ecdh(r_e, pvk_recipient); // O7 (anti-poisoning, Section 5.4) - let r_transfer = derive_transfer_blind(s, sigma_a); + let r_transfer = derive_transfer_blind(s, sigma_a_new); // O8 let c_transfer_derived = commit(v_transfer, r_transfer); @@ -318,10 +318,10 @@ fn main( assert(c_transfer_derived.y == c_transfer_y); // O9 - let v_tilde_derived = encrypt_amount(v_transfer, s, sigma_a); + let v_tilde_derived = encrypt_amount(v_transfer, s, sigma_a_new); assert(v_tilde_derived == v_tilde); - // Salt rotation (implementation hardening). + // O14 assert(sigma_a_new != sigma_a); // O10 @@ -346,7 +346,7 @@ fn main( let s_a_r = ecdh(r_e, k_aud_r); // O_a2 (recipient-channel masks: amount, then r_transfer) - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, sigma_a); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, sigma_a_new); // O_a3 assert(v_transfer + m_r[0] == v_tilde_aud_r); @@ -359,7 +359,7 @@ fn main( // O_a6 (owner-channel masks: amount, post-transfer allowance, then the // blinding-escrow slot) - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma_a); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma_a_new); // O_a7 assert(v_transfer + m_s[0] == v_tilde_aud_s); diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr index 2f6d62ce6..dde220ca0 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -7,11 +7,12 @@ 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_TRANSFER / V_A_NEW -// give a non-trivial `v_a - v_transfer` so O4 is exercised on a real subtraction. +// SIGMA_A is the stored allowance salt and opens C_a (O3) and nothing else. +// SIGMA_A_NEW is the channel nonce: the recipient ECDH chain (O7 / O9), both +// auditor sponges (O_a2 / O_a6), and the post-transfer allowance derivations +// (O10, O12) all absorb it -- see main.nr "Channel nonce". V_A / V_TRANSFER / +// V_A_NEW give a non-trivial `v_a - v_transfer` 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 @@ -57,27 +58,26 @@ global K_AUD_S_X: Field = 0x22502c7b20b64aeaaaa4d4fa5f2f8600f9734e828f7284a8d143 global K_AUD_S_Y: Field = 0x0419089353d24334f03f4a1c9c9b19282e32f6879c5c69b9cdffba6a13a7c5ed; global C_A_NEW_X: Field = 0x2d19aa5933ba41ebadb28d4c784ca4e405a1a6efcb5cf15e2c8d5e9a0552f178; global C_A_NEW_Y: Field = 0x0214719083236d5ecfc551fbc2b6d04fa8c95b8986fcfc7bbb8c0ff3cecd3dd1; -global C_TRANSFER_X: Field = 0x26677e8f24cbbc929b8be4a8d470d4a0e54a3c8a351ceef295e6b99b2898ed1d; -global C_TRANSFER_Y: Field = 0x089153eeedb04e49b206f7121341fdcb842a6ca19fb0f938167834dd10d42a97; +global C_TRANSFER_X: Field = 0x278bb372b27575f08ab88b27d967546c867287e9e64b58a482b4f6433ddf8bdd; +global C_TRANSFER_Y: Field = 0x01021c5c2d8c3d736a8d87555ddd4cef3abc8297657ae139ab0968ff9cfebbfd; global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; -global V_TILDE: Field = 0x0b3b7be1cd27249ec6b32b4ecb840079e0354b8675e94aade6519e5428473ffa; +global V_TILDE: Field = 0x06656a0ee0a408f201f01030e6a6941a55cb2829549b5f5dd8ed233eb61a7880; global A_TILDE_NEW: Field = 0x1d2e286a8d510a7c4164d0142ceea3202a957d49248426435f311a348f681147; global V_TILDE_AUD_R: Field = - 0x1150b1568e3047a5d263b9c24b00f49ca3971880e2fd09e9e6448207136bbb41; + 0x274f7ec87c29f7e31d3240257d563a77373efd3f976bb197f0505fc3eff581dd; global R_TILDE_AUD_R: Field = - 0x16cb31d142cf3005f8bd95ab3f59c3bda0da0de7e1d2ac16b4d038548a02b364; + 0x11504bfdaa7e3dfb4b38918d6ae2ba379abfda0a53ebc00fd1b561e805407c96; global V_TILDE_AUD_S: Field = - 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c42cf; + 0x08ad26221dfe94a593dae37d3efdbe93a503a3cfe35bd10c3ab60061267c20fc; // Lane-2 blinding-escrow slot (O_a9): the NEW allowance blinding r_a', the // one O11 commits C_a' under, escrowed to the owner's auditor on every -// spender transfer. Note it is derived from SIGMA_A_NEW while the lane-2 pad -// keys off SIGMA_A -- see main.nr's O_a9 note. +// spender transfer. Pad and plaintext both key off SIGMA_A_NEW. global R_TILDE_AUD_S: Field = - 0x0382ab29c6554066d495613fdec52421d70c3bf49d0faaf25552266222af073a; + 0x19b2c06c2ec9cacaa9f36876d6b988d186ad82b2f47cff17dbe6e4632d79b4d7; global A_TILDE_AUD_S: Field = - 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; + 0x29499e6f15cad25108f6b3931d8f3cd70cd0c0000ca03921509c5466feb773bb; #[test] fn print_fixtures() { @@ -95,21 +95,21 @@ fn print_fixtures() { let r_e_pt = scalar_mul(R_E, H); let s = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let c_transfer = commit(V_TRANSFER, r_transfer); - let v_tilde = encrypt_amount(V_TRANSFER, s, SIGMA_A); + let v_tilde = encrypt_amount(V_TRANSFER, s, SIGMA_A_NEW); 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 = ecdh(R_E, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A_NEW); let v_tilde_aud_r = V_TRANSFER + m_r[0]; let r_tilde_aud_r = r_transfer + m_r[1]; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); let v_tilde_aud_s = V_TRANSFER + m_s[0]; let a_tilde_aud_s = V_A_NEW + m_s[1]; let r_tilde_aud_s = r_a_new + m_s[2]; @@ -163,9 +163,9 @@ fn spender_transfer_fixtures_match_lib() { let c_a = commit(V_A, r_a); let pvk_recipient = scalar_mul(VK_RECIPIENT, H); let s = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let c_transfer = commit(V_TRANSFER, r_transfer); - let v_tilde = encrypt_amount(V_TRANSFER, s, SIGMA_A); + let v_tilde = encrypt_amount(V_TRANSFER, s, SIGMA_A_NEW); 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); @@ -192,11 +192,11 @@ fn spender_transfer_auditor_fixtures_match_lib() { let r_e_pt = scalar_mul(R_E, H); let pvk_recipient = scalar_mul(VK_RECIPIENT, H); let s = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let s_a_r = ecdh(R_E, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A_NEW); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); assert(k_aud_r.x == K_AUD_R_X); assert(k_aud_r.y == K_AUD_R_Y); @@ -328,11 +328,11 @@ fn matches_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_transfer. Exercises the case where the spender - // 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. + // sigma_a) with a different v_transfer. This is the retry / second-attempt + // shape: the stored sigma_a is forced, so a fresh sigma_a_new is the only + // thing that can make the pads fresh. Every pad here is keyed to + // sigma_a_new_2 and r_e_2, so none of the canonical fixture's ciphertexts + // recur even though C_a, v_a and sigma_a are identical. let v_transfer_2: Field = 250; let v_a_new_2: Field = V_A - v_transfer_2; // 750 let sigma_a_new_2: Field = 0x03; @@ -344,17 +344,17 @@ fn second_transfer_against_same_allowance() { let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s = ecdh(r_e_2, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, sigma_a_new_2); let c_transfer = commit(v_transfer_2, r_transfer); - let v_tilde_2 = encrypt_amount(v_transfer_2, s, SIGMA_A); + let v_tilde_2 = encrypt_amount(v_transfer_2, s, sigma_a_new_2); 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 = ecdh(r_e_2, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, sigma_a_new_2); let s_a_s = ecdh(r_e_2, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma_a_new_2); run_main( SK_OP, @@ -403,16 +403,16 @@ fn full_allowance_transfer() { 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 = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let c_transfer = commit(V_A, r_transfer); - let v_tilde = encrypt_amount(V_A, s, SIGMA_A); + let v_tilde = encrypt_amount(V_A, s, SIGMA_A_NEW); 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 = ecdh(R_E, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A_NEW); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, @@ -573,7 +573,7 @@ fn rejects_wrong_allowance_opening() { 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 = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, @@ -623,16 +623,16 @@ fn rejects_v_a_new_out_of_range() { 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 = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let c_transfer = commit(v_transfer_too_large, r_transfer); - let v_tilde = encrypt_amount(v_transfer_too_large, s, SIGMA_A); + let v_tilde = encrypt_amount(v_transfer_too_large, s, SIGMA_A_NEW); let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); let c_a_new_invalid = commit(V_A - v_transfer_too_large, r_a_new); let a_tilde_new_invalid = encrypt_allowance(V_A - v_transfer_too_large, DVK_I, SIGMA_A_NEW); let s_a_r = ecdh(R_E, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A_NEW); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, @@ -687,7 +687,7 @@ fn rejects_v_a_out_of_range() { 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 = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, @@ -737,16 +737,16 @@ fn rejects_v_transfer_out_of_range() { 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 = ecdh(R_E, pvk_recipient); - let r_transfer = derive_transfer_blind(s, SIGMA_A); + let r_transfer = derive_transfer_blind(s, SIGMA_A_NEW); let c_transfer = commit(v_transfer_huge, r_transfer); - let v_tilde = encrypt_amount(v_transfer_huge, s, SIGMA_A); + let v_tilde = encrypt_amount(v_transfer_huge, s, SIGMA_A_NEW); 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 = ecdh(R_E, k_aud_r); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r, SIGMA_A_NEW); let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, @@ -829,12 +829,12 @@ fn rejects_poisoned_c_transfer() { // Anti-poisoning (Section 5.4): a prover that commits C_transfer with arbitrary // blinding -- here r_transfer + 1 -- desynchronizes the recipient's accumulated // blinding. O7 + O8 together close this attack: r_transfer is uniquely - // determined by Poseidon2(delta_transfer_blind, s, sigma_a), so any C_transfer not + // determined by Poseidon2(delta_transfer_blind, s, sigma_a'), so any C_transfer not // built from that exact r_transfer fails O8. let pvk_recipient = EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; let s = ecdh(R_E, pvk_recipient); - let r_transfer_poisoned = derive_transfer_blind(s, SIGMA_A) + 1; + let r_transfer_poisoned = derive_transfer_blind(s, SIGMA_A_NEW) + 1; let c_transfer_poisoned = commit(V_TRANSFER, r_transfer_poisoned); run_main( @@ -996,17 +996,30 @@ 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`: + // O14: the circuit rejects `sigma_a' == sigma_a`, which would key every + // pad to the salt the previous transfer already used. With + // `sigma_a' = sigma_a`: // r_a_new = derive_allow_r(dvk_i, sigma_a_new) = r_a // c_a_new = (V_A - V_TRANSFER) * G + r_a * H // a_tilde_new = encrypt_allowance(V_A - V_TRANSFER, dvk_i, sigma_a_new) - // All downstream publics are recomputed consistently so the only - // firing constraint is `assert(sigma_a_new != sigma_a)`. + // and every channel pad absorbs SIGMA_A. 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); + 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 = ecdh(R_E, pvk_recipient); + let r_transfer = derive_transfer_blind(s, SIGMA_A); + let c_transfer = commit(V_TRANSFER, r_transfer); + let v_tilde = encrypt_amount(V_TRANSFER, s, SIGMA_A); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, ecdh(R_E, k_aud_r), SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, ecdh(R_E, k_aud_s), SIGMA_A); + run_main( SK_OP, DVK_I, @@ -1027,18 +1040,18 @@ fn rejects_sigma_a_new_equal_to_sigma_a() { K_AUD_S_Y, c_a_new.x, c_a_new.y, - C_TRANSFER_X, - C_TRANSFER_Y, + c_transfer.x, + c_transfer.y, R_E_X, R_E_Y, - V_TILDE, + v_tilde, a_tilde_new, SIGMA_A, - V_TILDE_AUD_R, - R_TILDE_AUD_R, - V_TILDE_AUD_S, - A_TILDE_AUD_S, - R_TILDE_AUD_S, + V_TRANSFER + m_r[0], + r_transfer + m_r[1], + V_TRANSFER + m_s[0], + V_A_NEW + m_s[1], + r_a_new + m_s[2], ); } @@ -1048,7 +1061,7 @@ fn rejects_r_e_zero() { // shared point in this transfer (S, S_{a,r}, S_{a,s} all become O), so // each ecdh(0, .) returns the ECDH-domain hash of the identity point: // s_zero, s_a_r_zero, and s_a_s_zero are identity-derived constants, - // making every mask a knowable constant function of sigma_a. Every + // making every mask a knowable constant function of sigma_a'. Every // r_e-dependent public (R_e = (0, 0) identity-encoding, C_transfer / // v_tilde derived against s_zero, and both auditor channels' ciphertexts // derived against s_a_r_zero / s_a_s_zero) is recomputed so the @@ -1059,13 +1072,13 @@ fn rejects_r_e_zero() { 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_zero = ecdh(0, pvk_recipient); - let r_transfer_zero = derive_transfer_blind(s_zero, SIGMA_A); + let r_transfer_zero = derive_transfer_blind(s_zero, SIGMA_A_NEW); let c_transfer_zero = commit(V_TRANSFER, r_transfer_zero); - let v_tilde_zero = encrypt_amount(V_TRANSFER, s_zero, SIGMA_A); + let v_tilde_zero = encrypt_amount(V_TRANSFER, s_zero, SIGMA_A_NEW); let s_a_r_zero = ecdh(0, k_aud_r); - let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_zero, SIGMA_A); + let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_zero, SIGMA_A_NEW); let s_a_s_zero = ecdh(0, k_aud_s); - let m_s_zero = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA_A); + let m_s_zero = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_zero, SIGMA_A_NEW); run_main( SK_OP, @@ -1116,13 +1129,13 @@ fn rejects_wrong_r_e() { 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_bad = ecdh(r_e_bad, pvk_recipient); - let r_transfer_bad = derive_transfer_blind(s_bad, SIGMA_A); + let r_transfer_bad = derive_transfer_blind(s_bad, SIGMA_A_NEW); let c_transfer_bad = commit(V_TRANSFER, r_transfer_bad); - let v_tilde_bad = encrypt_amount(V_TRANSFER, s_bad, SIGMA_A); + let v_tilde_bad = encrypt_amount(V_TRANSFER, s_bad, SIGMA_A_NEW); let s_a_r_bad = ecdh(r_e_bad, k_aud_r); - let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_bad, SIGMA_A); + let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_bad, SIGMA_A_NEW); let s_a_s_bad = ecdh(r_e_bad, k_aud_s); - let m_s_bad = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA_A); + let m_s_bad = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_bad, SIGMA_A_NEW); run_main( SK_OP, @@ -1482,7 +1495,8 @@ fn alternating_sigma_a_reproduces_an_earlier_allowance_blinding() { let v_a_new_2: Field = V_A_NEW - v_transfer_2; // 500 let r_e_2: Field = R_E + 3; - // Back to SIGMA_A after one hop through SIGMA_A_NEW. + // Back to SIGMA_A after one hop through SIGMA_A_NEW; the pads follow the + // written salt, so they key off SIGMA_A too. let r_a_new_2 = derive_allow_r(DVK_I, SIGMA_A); assert(r_a_new_2 == R_A); @@ -1491,16 +1505,16 @@ fn alternating_sigma_a_reproduces_an_earlier_allowance_blinding() { 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 = ecdh(r_e_2, pvk_recipient); - let r_transfer_2 = derive_transfer_blind(s, SIGMA_A_NEW); + let r_transfer_2 = derive_transfer_blind(s, SIGMA_A); let c_transfer_2 = commit(v_transfer_2, r_transfer_2); - let v_tilde_2 = encrypt_amount(v_transfer_2, s, SIGMA_A_NEW); + let v_tilde_2 = encrypt_amount(v_transfer_2, s, SIGMA_A); 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); let r_e_2_pt = scalar_mul(r_e_2, H); let s_a_r_2 = ecdh(r_e_2, k_aud_r); - let m_r_2 = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_2, SIGMA_A_NEW); + let m_r_2 = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_2, SIGMA_A); let s_a_s_2 = ecdh(r_e_2, k_aud_s); - let m_s_2 = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_2, SIGMA_A_NEW); + let m_s_2 = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s_2, SIGMA_A); run_main( SK_OP, @@ -1538,8 +1552,8 @@ fn alternating_sigma_a_reproduces_an_earlier_allowance_blinding() { } #[test] -fn repeated_r_e_and_sigma_a_repeat_every_channel_pad() { - // Reusing (r_e, sigma_a) repeats s, s_{a,r}, and s_{a,s}, and with them +fn repeated_r_e_and_sigma_a_new_repeat_every_channel_pad() { + // Reusing (r_e, sigma_a') repeats s, s_{a,r}, and s_{a,s}, and with them // every mask on all three channels. Re-encrypting the same plaintexts // under them republishes byte-identical ciphertexts -- no differential. let pvk_recipient = @@ -1547,19 +1561,20 @@ fn repeated_r_e_and_sigma_a_repeat_every_channel_pad() { 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 = ecdh(R_E, pvk_recipient); - let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, ecdh(R_E, k_aud_r), SIGMA_A); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, ecdh(R_E, k_aud_s), SIGMA_A); - assert(encrypt_amount(V_TRANSFER, s, SIGMA_A) == V_TILDE); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, ecdh(R_E, k_aud_r), SIGMA_A_NEW); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, ecdh(R_E, k_aud_s), SIGMA_A_NEW); + assert(encrypt_amount(V_TRANSFER, s, SIGMA_A_NEW) == V_TILDE); assert(V_TRANSFER + m_r[0] == V_TILDE_AUD_R); assert(V_TRANSFER + m_s[0] == V_TILDE_AUD_S); assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); assert(derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s[2] == R_TILDE_AUD_S); // Change a plaintext under the repeated pad and the difference survives in - // the clear. Two transfers of different sizes under one (r_e, sigma_a) + // the clear. Two transfers of different sizes under one (r_e, sigma_a') // publish v_tilde values whose difference is the amount delta. The circuit - // cannot catch this -- a reused r_e is a well-formed witness -- so it is - // the honest client's obligation (Section 5.3, Section 9.6). + // cannot catch this -- a reused r_e is a well-formed witness and O14 sees + // only the adjacent salt -- so it is the honest client's obligation + // (Section 5.3, Section 9.6, SDK.md Section 10.4). let v_transfer_2: Field = V_TRANSFER + 40; assert((v_transfer_2 + m_s[0]) - V_TILDE_AUD_S == 40); } @@ -1578,7 +1593,7 @@ fn rejects_escrow_of_the_consumed_allowance_blinding() { // it from the previous operation's event. let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; let s_a_s = ecdh(R_E, k_aud_s); - let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A); + let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, SIGMA_A_NEW); run_main( SK_OP, DVK_I, diff --git a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json index be9988f8e..f81ac038c 100644 --- a/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json +++ b/packages/tokens/src/confidential/circuits/vks/spender_transfer.vk.json @@ -1 +1 @@ -["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000b1e2f541998a2755e5213d3e971e91df06","0x00000000000000000000000000000000001c85aa14f04fe5cd6ba340f1e36f30","0x000000000000000000000000000000dd17a4a404747a820741028f49190e3643","0x00000000000000000000000000000000001c56196f530a939d9d97a3cc03ab0b","0x000000000000000000000000000000e138bb15582e87af9d51fbc7ed6e0e7695","0x00000000000000000000000000000000001e7f60c2349b166c96ad0d379b4ca3","0x000000000000000000000000000000b98deafa29f053f7ceb11d0d32de87dc7c","0x0000000000000000000000000000000000002ce030b942f99071c3b7b51a20c3","0x000000000000000000000000000000528a55b629a204f061da96f9f06561d4f9","0x000000000000000000000000000000000023792b0987721b9128e0cf41d9b32c","0x0000000000000000000000000000008a430926dbdf33c991898be570409a110a","0x0000000000000000000000000000000000219cb1f43da57ea5d7d298bd0e7b49","0x00000000000000000000000000000092e74077b65ff0e232758cf9429592aaf5","0x00000000000000000000000000000000001282528955f7796f0a65e209114dd2","0x00000000000000000000000000000029590dd636d3648667c797ca389e874332","0x0000000000000000000000000000000000236ef8ad2e3fc5ce9dc9143315fb86","0x000000000000000000000000000000c8a6c7a4920d3415fab8e852d34e731f3d","0x0000000000000000000000000000000000169f38558aa5d5fbc8ccfc308ece82","0x0000000000000000000000000000007ab272b3b30d2aa5a04ab9ff8b03761300","0x0000000000000000000000000000000000048cbf034592a047b3f743cc06a9b3","0x00000000000000000000000000000092f9818005d01ae6aa95051e81a4d8fd7c","0x000000000000000000000000000000000019f5135e35bb7fd0bb0db2984503d7","0x0000000000000000000000000000001a8a976ae62ab0d4ebdf0f026a25320094","0x0000000000000000000000000000000000237782b32e0dd4f3385e0e58707578","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x00000000000000000000000000000083f8426178a4adb8759e565672d48d57b2","0x00000000000000000000000000000000000d2162b8663e51626b63b613f38d10","0x0000000000000000000000000000005abf0765b4ec2d9008d3a0b0e4507d896d","0x00000000000000000000000000000000001c9eb82857d4ed7141bbdd48792bf6","0x00000000000000000000000000000053644e331267af096b7fa639d567af46d5","0x0000000000000000000000000000000000169b62f18a62c1dd259c0e4c1e6ed7","0x000000000000000000000000000000e0c1b58069ea333eef6ab3adfdebfbff1b","0x0000000000000000000000000000000000194f5f1fb476a26932b74e9b3b0076","0x0000000000000000000000000000007f16b72a7cd37b7fad520beb587385af0c","0x00000000000000000000000000000000000b7c2b90171c94d593ce8c018f9d9f","0x0000000000000000000000000000006b97f4bcd4ffbb3f43a44c9b54da291227","0x000000000000000000000000000000000011e40fb9bb5d8e48416a30e2fbec52","0x00000000000000000000000000000045fe00b3b7a774a6f24792e2232d7cff7e","0x00000000000000000000000000000000001931e0d13d1f24ae08944ddf7477e9","0x000000000000000000000000000000a03c16e18ed20dc8b0359b35f488a561b5","0x00000000000000000000000000000000001e367c248e85a81becee8094786fa5","0x000000000000000000000000000000a9e97de75737843e2a615ddfcf184170b3","0x000000000000000000000000000000000012af86eb66bebd2a4d37c160d27a23","0x0000000000000000000000000000000be148149805efb06f7487733e2ca3f78d","0x000000000000000000000000000000000001623d48ab894fd1c64ab90c49fc39","0x000000000000000000000000000000c0e17eccf6222f6f5e80382b856c9069ca","0x0000000000000000000000000000000000049669495d53f7e25db94054b7bf66","0x000000000000000000000000000000063bf54ae8896ffcfcd5f483bce7ffbd2c","0x000000000000000000000000000000000011e1d2550ae5e4aa5f37048b93c3ac","0x000000000000000000000000000000dea7b01531d31241ab850cd5d66f7c0e15","0x00000000000000000000000000000000002696590fa94126f758939789a4f106","0x0000000000000000000000000000000113486b4b5551285a003bd39fa6c86fe5","0x00000000000000000000000000000000001a5832d94ed308197877dd081325b8","0x000000000000000000000000000000792d41e7d3f7eae77e8ee735d474243793","0x0000000000000000000000000000000000087af58bfadcc29c72cd4ec312329b","0x000000000000000000000000000000f11ae9376eb15d1aad265033c153e96a44","0x0000000000000000000000000000000000291651af3e46e2febc867996076804","0x000000000000000000000000000000229e8d446d2172bca1bde608f9b7af6940","0x00000000000000000000000000000000000f8f294047a4fe4f38bb3d32986e65","0x0000000000000000000000000000003547cd41239103ff65ed2aa52e662d1fa7","0x000000000000000000000000000000000029d698f9e85044e11bc2fa37409c12","0x0000000000000000000000000000003449790c59fc7a4349e1e55768a7066fd4","0x0000000000000000000000000000000000075204d5ba12a18730f9cca06c9d14","0x000000000000000000000000000000d74e3abd35dea7d5c3864b37381dacb1dd","0x0000000000000000000000000000000000105a75d044776abb9e403442460c86","0x00000000000000000000000000000050c32559396a96a2c975b2387cf9b0f0a8","0x00000000000000000000000000000000001560274dd48a2b5d24b6091ca1e9d5","0x000000000000000000000000000000926bdc48c50dff3424d82c0478e4d1a5ee","0x000000000000000000000000000000000008bf7f0695b740da94ed59263850ee","0x000000000000000000000000000000f78443226cf26cb8bd04a73b09d33c6481","0x00000000000000000000000000000000001e4f8f021bcc5df9de12fb67a01b94","0x000000000000000000000000000000704e51d181f28de21c8a8fca2978a3cef2","0x000000000000000000000000000000000018be339c2f9f1df76ce2b201b70385","0x000000000000000000000000000000df174b538c33c37f6aae1f5376293eb040","0x00000000000000000000000000000000002e8e391362e10fcc3e73988410b62e","0x00000000000000000000000000000043520aea0acff78687103480b09c01a8f6","0x00000000000000000000000000000000002a69cc75b104ac7855fd7f75f184a3","0x000000000000000000000000000000267c9488653d674ef240ba580f33176340","0x000000000000000000000000000000000019e6a90b94d863a6d4c4e39e80e6c0","0x0000000000000000000000000000009c7b961d4157b3b26b9156a3932361c86e","0x0000000000000000000000000000000000275212059032c11bb72f6a50581c87","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000d678f1a0bcc54fa74011eefbef799f4a0d","0x000000000000000000000000000000000030439c58cae8bce4e184e2b8bda865","0x000000000000000000000000000000c8f80f131a32e400e35d3dad34964738b9","0x00000000000000000000000000000000000fcd9a67a0311379f8a005c79daec1"] \ No newline at end of file +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000029","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000019","0x000000000000000000000000000000b1e2f541998a2755e5213d3e971e91df06","0x00000000000000000000000000000000001c85aa14f04fe5cd6ba340f1e36f30","0x000000000000000000000000000000dd17a4a404747a820741028f49190e3643","0x00000000000000000000000000000000001c56196f530a939d9d97a3cc03ab0b","0x000000000000000000000000000000e138bb15582e87af9d51fbc7ed6e0e7695","0x00000000000000000000000000000000001e7f60c2349b166c96ad0d379b4ca3","0x000000000000000000000000000000b98deafa29f053f7ceb11d0d32de87dc7c","0x0000000000000000000000000000000000002ce030b942f99071c3b7b51a20c3","0x000000000000000000000000000000528a55b629a204f061da96f9f06561d4f9","0x000000000000000000000000000000000023792b0987721b9128e0cf41d9b32c","0x0000000000000000000000000000008a430926dbdf33c991898be570409a110a","0x0000000000000000000000000000000000219cb1f43da57ea5d7d298bd0e7b49","0x00000000000000000000000000000092e74077b65ff0e232758cf9429592aaf5","0x00000000000000000000000000000000001282528955f7796f0a65e209114dd2","0x00000000000000000000000000000029590dd636d3648667c797ca389e874332","0x0000000000000000000000000000000000236ef8ad2e3fc5ce9dc9143315fb86","0x000000000000000000000000000000c8a6c7a4920d3415fab8e852d34e731f3d","0x0000000000000000000000000000000000169f38558aa5d5fbc8ccfc308ece82","0x0000000000000000000000000000007ab272b3b30d2aa5a04ab9ff8b03761300","0x0000000000000000000000000000000000048cbf034592a047b3f743cc06a9b3","0x00000000000000000000000000000092f9818005d01ae6aa95051e81a4d8fd7c","0x000000000000000000000000000000000019f5135e35bb7fd0bb0db2984503d7","0x0000000000000000000000000000001a8a976ae62ab0d4ebdf0f026a25320094","0x0000000000000000000000000000000000237782b32e0dd4f3385e0e58707578","0x0000000000000000000000000000007d23bc9c11fe98e0188a8228651e8c8612","0x00000000000000000000000000000000001824dc044196d321168c8386ebc647","0x000000000000000000000000000000fd200b620e62036d89553e3b1def7ec8b4","0x000000000000000000000000000000000025ca5d6f72d2a5c0539cef16bc5772","0x00000000000000000000000000000083f8426178a4adb8759e565672d48d57b2","0x00000000000000000000000000000000000d2162b8663e51626b63b613f38d10","0x0000000000000000000000000000005abf0765b4ec2d9008d3a0b0e4507d896d","0x00000000000000000000000000000000001c9eb82857d4ed7141bbdd48792bf6","0x00000000000000000000000000000053644e331267af096b7fa639d567af46d5","0x0000000000000000000000000000000000169b62f18a62c1dd259c0e4c1e6ed7","0x000000000000000000000000000000e0c1b58069ea333eef6ab3adfdebfbff1b","0x0000000000000000000000000000000000194f5f1fb476a26932b74e9b3b0076","0x0000000000000000000000000000007f16b72a7cd37b7fad520beb587385af0c","0x00000000000000000000000000000000000b7c2b90171c94d593ce8c018f9d9f","0x0000000000000000000000000000006b97f4bcd4ffbb3f43a44c9b54da291227","0x000000000000000000000000000000000011e40fb9bb5d8e48416a30e2fbec52","0x00000000000000000000000000000045fe00b3b7a774a6f24792e2232d7cff7e","0x00000000000000000000000000000000001931e0d13d1f24ae08944ddf7477e9","0x000000000000000000000000000000a03c16e18ed20dc8b0359b35f488a561b5","0x00000000000000000000000000000000001e367c248e85a81becee8094786fa5","0x000000000000000000000000000000a9e97de75737843e2a615ddfcf184170b3","0x000000000000000000000000000000000012af86eb66bebd2a4d37c160d27a23","0x0000000000000000000000000000000be148149805efb06f7487733e2ca3f78d","0x000000000000000000000000000000000001623d48ab894fd1c64ab90c49fc39","0x000000000000000000000000000000c0e17eccf6222f6f5e80382b856c9069ca","0x0000000000000000000000000000000000049669495d53f7e25db94054b7bf66","0x000000000000000000000000000000063bf54ae8896ffcfcd5f483bce7ffbd2c","0x000000000000000000000000000000000011e1d2550ae5e4aa5f37048b93c3ac","0x000000000000000000000000000000c893d71e03cdbbcccba83e3686748261fa","0x00000000000000000000000000000000002a7b4f7de7010bbf608cb8b59223c0","0x0000000000000000000000000000005e3974920dd9781e8b9b2e81540233392d","0x0000000000000000000000000000000000084df620d5d72a1ec67897952cd34b","0x000000000000000000000000000000c92cf866aee5a0ea4648909ef6d68fd296","0x00000000000000000000000000000000001fca1e1587e296c15350f05e90d9c8","0x000000000000000000000000000000a2005b5d09a1823e4ee2a0351a9efff114","0x000000000000000000000000000000000007c766a5124ce00865594065e092bf","0x000000000000000000000000000000229e8d446d2172bca1bde608f9b7af6940","0x00000000000000000000000000000000000f8f294047a4fe4f38bb3d32986e65","0x0000000000000000000000000000003547cd41239103ff65ed2aa52e662d1fa7","0x000000000000000000000000000000000029d698f9e85044e11bc2fa37409c12","0x0000000000000000000000000000003449790c59fc7a4349e1e55768a7066fd4","0x0000000000000000000000000000000000075204d5ba12a18730f9cca06c9d14","0x000000000000000000000000000000d74e3abd35dea7d5c3864b37381dacb1dd","0x0000000000000000000000000000000000105a75d044776abb9e403442460c86","0x00000000000000000000000000000050c32559396a96a2c975b2387cf9b0f0a8","0x00000000000000000000000000000000001560274dd48a2b5d24b6091ca1e9d5","0x000000000000000000000000000000926bdc48c50dff3424d82c0478e4d1a5ee","0x000000000000000000000000000000000008bf7f0695b740da94ed59263850ee","0x000000000000000000000000000000f78443226cf26cb8bd04a73b09d33c6481","0x00000000000000000000000000000000001e4f8f021bcc5df9de12fb67a01b94","0x000000000000000000000000000000704e51d181f28de21c8a8fca2978a3cef2","0x000000000000000000000000000000000018be339c2f9f1df76ce2b201b70385","0x000000000000000000000000000000df174b538c33c37f6aae1f5376293eb040","0x00000000000000000000000000000000002e8e391362e10fcc3e73988410b62e","0x00000000000000000000000000000043520aea0acff78687103480b09c01a8f6","0x00000000000000000000000000000000002a69cc75b104ac7855fd7f75f184a3","0x000000000000000000000000000000267c9488653d674ef240ba580f33176340","0x000000000000000000000000000000000019e6a90b94d863a6d4c4e39e80e6c0","0x0000000000000000000000000000009c7b961d4157b3b26b9156a3932361c86e","0x0000000000000000000000000000000000275212059032c11bb72f6a50581c87","0x0000000000000000000000000000008e59b97dd6154e534733ad92ad3a68a5d1","0x0000000000000000000000000000000000273947962d9e45c3ffe701e8dfb3d2","0x00000000000000000000000000000094d4ac37423f4543d43c6d532fba449ef1","0x000000000000000000000000000000000017ed9d63172a2eb4131c9f16db109a","0x00000000000000000000000000000072cb7beeabccc6c0373d512fc2a1fcd7e0","0x000000000000000000000000000000000001d67d4a0eb2e248680352726f0375","0x00000000000000000000000000000063ff686ee61e643ec81d1c8943337798b6","0x0000000000000000000000000000000000223b3eb65e0e3b442921ef14846d6a","0x000000000000000000000000000000a4c5c09617703654854f4cb71647d3ea68","0x000000000000000000000000000000000030451a0765f5e203bdedf2cfca899b","0x000000000000000000000000000000a71843a019f1987c83e489f228f4d22c33","0x00000000000000000000000000000000002eb0772b27bdffc2103f1878526700","0x000000000000000000000000000000c6b6d92eb8c0c7b04720ca665ef4e83f9a","0x00000000000000000000000000000000002edd5fa4e5172459005ff4f8a17593","0x0000000000000000000000000000009013a1c81bffd29f7d6ae91954fac52e3d","0x00000000000000000000000000000000001009ca6d2b80745a813790fc281bb5","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000d678f1a0bcc54fa74011eefbef799f4a0d","0x000000000000000000000000000000000030439c58cae8bce4e184e2b8bda865","0x000000000000000000000000000000c8f80f131a32e400e35d3dad34964738b9","0x00000000000000000000000000000000000fcd9a67a0311379f8a005c79daec1"] \ No newline at end of file diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index 322455c2f..a0cac05d0 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -153,7 +153,7 @@ where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is **Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two multi-lane tags; every other tag in Section 13 is used only with the single-output form above. Reading one multi-lane tag at two different widths is not a mode violation, by the prefix property above: the sender-auditor tag is squeezed three-wide on every operation that escrows lane 2 and two-wide on `RevokeSpender` (V\_a3), while the recipient-auditor tag is always two-wide. -The sponge masks are deterministic in $$(s, \sigma)$$, where $$s$$ is the ECDH shared scalar of Section 2.4, so reusing the pair across two operations reuses every pad slot they share, and a slot whose plaintext is known in one operation (e.g. a transfer amount known to its recipient) decrypts the other operation's ciphertext in that slot. The canonical slot assignment above limits the blast radius of such reuse to same-slot pairs, but does not eliminate it; provers and wallets MUST use a fresh $$(r\_e, \sigma)$$ for every proof. Because $$r\_e$$ is derived from the originator's viewing key and the salt rather than drawn independently (§5.3), a fresh salt is the only thing that makes the pair fresh: the salt carries the entire requirement, and Section 9.6's retry rule is what discharges it. +The sponge masks are deterministic in $$(s, \sigma)$$, where $$s$$ is the ECDH shared scalar of Section 2.4, so reusing the pair across two operations reuses every pad slot they share, and a slot whose plaintext is known in one operation (e.g. a transfer amount known to its recipient) decrypts the other operation's ciphertext in that slot. The canonical slot assignment above limits the blast radius of such reuse to same-slot pairs, but does not eliminate it; provers and wallets MUST use a fresh $$(r\_e, \sigma)$$ for every proof. Because $$r\_e$$ is derived from the originator's viewing key and the salt rather than drawn independently (§5.3), a fresh salt is the only thing that makes the pair fresh: the salt carries the entire requirement, and Section 9.6's retry rule is what discharges it. The salt that carries it is whichever one the operation's pads absorb -- $$\sigma$$ for owner-initiated operations, the prover-chosen $$\sigma\_a'$$ for spender transfers (§6.2 *Transfer nonce*) -- never a salt loaded from storage, which a revert leaves unchanged. All references to "Poseidon" in this document denote this Poseidon2 instantiation. @@ -409,7 +409,7 @@ Because $$\sigma$$ is published in the event and $$vk$$ is held by the originato **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_spender` (§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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender escrow), so masks across channels are independent under the PRF assumption on Poseidon (§3.2). The auditor-side allowance-blinding escrow (S14, $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$) is the one derivation that does *not* open a channel of its own. It reuses the S\_a2 shared scalar, so it drops the distinct-scalar leg, and it absorbs $$\text{op}\_i$$, a per-delegation constant, rather than a per-operation nonce, so it drops the freshness leg too. Its separation therefore rests on the distinct-tag leg alone, plus the freshness of the shared scalar itself, inherited from $$r\_e$$ and hence from the salt. This is the thinnest construction in the system, and [DESIGN_cont.md](./DESIGN_cont.md) §8.5 states what it does and does not buy. 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 for every channel; the contract's enumeration of channels in §13 satisfies the domain-distinctness condition. S14 is the single derivation that stands on one of the three, as noted above. +**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{transfer\\\_blind}}/\delta\_{\text{transfer\\\_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 spender escrow), so masks across channels are independent under the PRF assumption on Poseidon (§3.2). The auditor-side allowance-blinding escrow (S14, $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$) is the one derivation that does *not* open a channel of its own. It reuses the S\_a2 shared scalar, so it drops the distinct-scalar leg, and it absorbs $$\text{op}\_i$$, a per-delegation constant, rather than a per-operation nonce, so it drops the freshness leg too. Its separation therefore rests on the distinct-tag leg alone, plus the freshness of the shared scalar itself, inherited from $$r\_e$$ and hence from the salt. This is the thinnest construction in the system, and [DESIGN_cont.md](./DESIGN_cont.md) §8.5 states what it does and does not buy. The channel masks are used as one-time pads against fresh per-transfer randomness ($$\sigma$$, or $$\sigma\_a'$$ for spender transfers), 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 for every channel; the contract's enumeration of channels in §13 satisfies the domain-distinctness condition. S14 is the single derivation that stands on one of the three, as noted above. ### 5.4 Anti-Poisoning Constraint @@ -494,9 +494,11 @@ $$dvk\_i$$ encrypted under the spender'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_spender` and replaced by the spender on every `confidential_transfer_from` (the spender 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. +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_spender` and replaced by the spender on every `confidential_transfer_from` (the spender samples a fresh $$\sigma\_a'$$ 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 spender 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 scalar $$s$$ (or $$s\_{a,r}$$, $$s\_{a,s}$$, §2.4), 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. +**Transfer nonce.** The stored $$\sigma\_a$$ opens the current allowance (O3) and nothing else. Every pad a spender transfer derives absorbs the prover-chosen replacement $$\sigma\_a'$$ instead: the recipient ECDH encryption (O7, O9), both auditor channel sponges (O\_a2, O\_a6, and with the latter the lane[2] escrow O\_a9), and the ephemeral scalar (§7.8). $$\sigma\_a'$$ is therefore the nonce the `SpenderTransfer` event emits, and the salt it replaces is never emitted. + +The split is what makes a retry safe. A reverted call leaves the delegation entry untouched, so the stored $$\sigma\_a$$ is forced on the retry; keying the pads to it would repeat $$r\_e$$ and every mask, and a retry that changed the amount would publish the difference in the clear (§2.5). $$\sigma\_a'$$ is constrained only against prover-supplied state, so a retry re-samples it freely. Constraint O14 rejects $$\sigma\_a' = \sigma\_a$$, which would key the pads to the salt the previous transfer already used; it cannot see older salts, so **global non-repetition of $$\sigma\_a'$$ across the delegation's lifetime is a client obligation** ([SDK.md](./SDK.md) §10.4), not an enforced property. **`live_until_ledger`** @@ -734,22 +736,23 @@ The spender transfers from the owner's escrowed allowance to a recipient. | O4 | $$v\_a \in [0, 2^{127})$$, $$v\_{\text{transfer}} \in [0, 2^{127})$$, $$v\_a - v\_{\text{transfer}} \in [0, 2^{127})$$ (range validity, Section 2.6) | | O5 | $$s = \text{ECDH}(r\_e, \text{PVK}\_{\text{recipient}})$$ (recipient ECDH shared scalar, §2.4) | | O6 | $$R\_e = r\_e \cdot H$$ | -| O7 | $$r\_{\text{transfer}} = \text{Poseidon}(\delta\_{\text{transfer\\\_blind}}, s, \sigma\_a)$$ (transfer blinding) | +| O7 | $$r\_{\text{transfer}} = \text{Poseidon}(\delta\_{\text{transfer\\\_blind}}, s, \sigma\_a')$$ (transfer blinding) | | O8 | $$C\_{\text{transfer}} = v\_{\text{transfer}} \cdot G + r\_{\text{transfer}} \cdot H$$ | -| O9 | $$\tilde{v} = v\_{\text{transfer}} + \text{Poseidon}(\delta\_{\text{transfer\\\_amount}}, s, \sigma\_a)$$ (encrypted amount) | +| O9 | $$\tilde{v} = v\_{\text{transfer}} + \text{Poseidon}(\delta\_{\text{transfer\\\_amount}}, s, \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{transfer}}) \cdot G + r\_a' \cdot H$$ (new allowance) | | O12 | $$\tilde{a}' = (v\_a - v\_{\text{transfer}}) + \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$$) | +| 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'$$) | +| O14 | $$\sigma\_a' \neq \sigma\_a$$ (nonce rotation; rejects the one-step pad reuse, §6.2 *Transfer nonce*) | | O\_a1 | $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$ (recipient-auditor ECDH shared scalar, reuses ephemeral scalar) | -| O\_a2 | $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma\_a)$$ (recipient-auditor channel masks) | +| O\_a2 | $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma\_a')$$ (recipient-auditor channel masks) | | O\_a3 | $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}$$ (recipient-auditor encrypted transfer amount) | | O\_a4 | $$\tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + 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} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (owner-auditor ECDH shared scalar, reuses ephemeral scalar) | -| O\_a6 | $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ (owner-auditor channel masks) | +| O\_a6 | $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a')$$ (owner-auditor channel masks) | | O\_a7 | $$\tilde{v}\_{\text{aud,s}} = v\_{\text{transfer}} + m\_{v,s}$$ (owner-auditor encrypted transfer amount) | | O\_a8 | $$\tilde{a}\_{\text{aud,s}} = (v\_a - v\_{\text{transfer}}) + m\_{a,s}$$ (owner-auditor encrypted post-transfer allowance) | -| O\_a9 | $$\tilde{r}\_{\text{aud,s}} = r\_a' + m\_{r,s}$$ (owner-auditor escrow of the NEW allowance blinding, the one O11 commits under, Section 8.5; already a witness by O10, so this is one field addition. The lane-2 pad absorbs the consumed salt while the plaintext derives from the written one; §8.4 says why that is sound) | +| O\_a9 | $$\tilde{r}\_{\text{aud,s}} = r\_a' + m\_{r,s}$$ (owner-auditor escrow of the NEW allowance blinding, the one O11 commits under, Section 8.5; already a witness by O10, so this is one field addition. Pad and plaintext both key off $$\sigma\_a'$$, under distinct domain tags and distinct secrets) | **Public inputs (25 fields):** @@ -764,9 +767,9 @@ The spender transfers from the owner's escrowed allowance to a recipient. **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{transfer}}$$, $$r\_e$$. -**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores `new_allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}}, \tilde{r}\_{\text{aud,s}})$$. +**Post-verification:** The contract checks `ledger.sequence() <= live_until_ledger`, updates `allowance_commitment`, `a_tilde`, stores $$\sigma\_a'$$ as the new `allowance_salt`, and adds $$C\_{\text{transfer}}$$ to the recipient's `receiving_commitment`. 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}}, \tilde{r}\_{\text{aud,s}})$$. -**Ephemeral scalar.** The spender derives $$r\_e = \text{Poseidon}(\delta\_{\text{eph}}, vk\_{\text{op}}, \sigma\_a)$$ (§5.3) from its *own* viewing key rather than the owner's, so that the spender can later disclose it ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7). The circuit does not constrain the derivation; it does not constrain $$vk\_{\text{op}}$$ at all, per *Contract binding* below. One consequence follows for the owner: since the owner does not hold $$vk\_{\text{op}}$$, the owner cannot recompute $$r\_e$$ for a spender transfer and cannot disclose it without the spender's cooperation ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7, *Coverage asymmetry*). +**Ephemeral scalar.** The spender derives $$r\_e = \text{Poseidon}(\delta\_{\text{eph}}, vk\_{\text{op}}, \sigma\_a')$$ (§5.3, §6.2 *Transfer nonce*) from its *own* viewing key rather than the owner's, so that the spender can later disclose it ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7). The circuit does not constrain the derivation; it does not constrain $$vk\_{\text{op}}$$ at all, per *Contract binding* below. One consequence follows for the owner: since the owner does not hold $$vk\_{\text{op}}$$, the owner cannot recompute $$r\_e$$ for a spender transfer and cannot disclose it without the spender's cooperation ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §7, *Coverage asymmetry*). **Recipient uniformity.** The recipient path is identical to the direct-transfer path of §7.6 *Recipient processing*. @@ -838,7 +841,7 @@ The same proof also escrows to the owner's *auditor* (S14), under its own domain ### 7.12 Expiry and Revert Safety -Allowance randomness includes `allowance_salt` to prevent deterministic-randomness reuse after reverted transactions. Delegation storage, expiry, and revocation semantics are specified in §6.2. +A reverted transaction leaves the delegation entry unchanged, so the stored `allowance_salt` recurs on the retry. Every value a retry could vary is therefore keyed to the prover-chosen $$\sigma\_a'$$ (§6.2 *Transfer nonce*). Delegation storage, expiry, and revocation semantics are specified in §6.2. --- diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 1410ba95c..374cfe616 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -39,7 +39,7 @@ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_{\text{ 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{transfer}}, r\_{\text{transfer}})$$. -**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the blinding of a commitment the operation writes, and never a key: the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6), and the new allowance blinding $$r\_a'$$ on spender transfers (O\_a9, Section 8.4). The two blindings belong to different commitment roles, so the uniformity is a naming convention, not an interchangeability claim; each is scoped to the operation that emitted it. The pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a)$$, both fresh per operation. +**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the blinding of a commitment the operation writes, and never a key: the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6), and the new allowance blinding $$r\_a'$$ on spender transfers (O\_a9, Section 8.4). The two blindings belong to different commitment roles, so the uniformity is a naming convention, not an interchangeability claim; each is scoped to the operation that emitted it. The pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a')$$, both fresh per operation. **Recipient-auditor opening capability.** Because the recipient-auditor recovers $$r\_{\text{transfer}}$$ for every inbound transfer, and because deposits add to `receiving_commitment` with $$r = 0$$ (Section 7.3), the recipient-auditor can reconstruct the full Pedersen opening of $$C\_{\text{receive}}$$ between merges: @@ -89,25 +89,25 @@ The auditor contract stores Grumpkin public keys as full affine points $$(x, y)$ When building public inputs for any operation that produces auditor ciphertexts (transfers, withdrawals, set/revoke spender), the contract fetches the relevant auditor keys for the recipient's and/or sender's `auditor_id`. The contract passes the full Grumpkin point as a public input; the circuit constrains the ECDH ciphertexts against that exact point. The contract 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 contract 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 spender) reconstructs the proof against the new $$K\_{\text{aud}}$$ and resubmits. The rejection is benign: the contract'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. +**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 contract 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 spender) reconstructs the proof against the new $$K\_{\text{aud}}$$ and resubmits. The rejection is benign: the contract's spendable balance, receiving balance, and delegation state are unchanged by the reverted call, the operation's salt 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 resolves the version from its own rotation records (with a versioned activation-ledger registry, the auditor instead 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. +**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 resolves the version from its own rotation records (with a versioned activation-ledger registry, the auditor instead 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 Spender Transfer Auditing Each spender transfer produces auditor ciphertexts under two keys (constraints O\_a1--O\_a9), 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)$$ +$$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma\_a')$$ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,r}} - m\_{v,r}, \qquad r\_{\text{transfer}} = \tilde{r}\_{\text{aud,r}} - m\_{r,r}$$ The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the lane-2 blinding-escrow slot -- the *new* allowance blinding $$r\_a'$$, the one O11 commits $$C\_a'$$ under (O\_a9): -$$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a)$$ +$$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a')$$ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}, \qquad r\_a' = \tilde{r}\_{\text{aud,s}} - m\_{r,s}$$ -Note the pad and the plaintext key off different salts: the lane-2 mask absorbs the *consumed* $$\sigma\_a$$ (the whole sponge does, per O\_a6), while $$r\_a' = \text{Poseidon}(\delta\_{\text{allow\\\_r}}, dvk\_i, \sigma\_a')$$ derives from the salt the operation writes. That is sound -- the pad needs only to be fresh and unpredictable to everyone but the auditor, and $$(s\_{a,s}, \sigma\_a)$$ is both, since $$s\_{a,s}$$ is fresh per operation -- and $$(\tilde{a}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$ therefore open the post-transfer $$C\_a'$$, the state left on-chain. +$$(\tilde{a}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$ open the post-transfer $$C\_a'$$, the state left on-chain. -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 spender-transfer inbound flows: $$r\_{\text{transfer}}$$ from spender-transfers contributes to $$r\_r$$ in $$C\_{\text{receive}}$$ identically to owner-transfer inbound flows. +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 spender-transfer inbound flows: $$r\_{\text{transfer}}$$ from spender-transfers contributes to $$r\_r$$ in $$C\_{\text{receive}}$$ identically to owner-transfer inbound flows. ### 8.5 Spender Allowance Auditing @@ -205,7 +205,7 @@ The recovery procedure, the definition of the **checkpoint** it is built around, 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. +**Retry procedure.** On revert, the wallet simply picks a new random salt -- $$\sigma$$, or $$\sigma\_a'$$ for a spender transfer -- and recomputes the proof. No special-case logic is needed. The salt is a public input and emitted in events so the auditor and owner can reconstruct randomness. ### 9.7 Replay Protection @@ -391,7 +391,7 @@ Requires `bn254_fr_{add, sub, mul, inv}` host calls (CAP-80, Section 10.7). **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 contract 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{transfer}}$$ (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). +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{transfer}}$$ (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 the operation's salt, 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 contract 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 contract trusts the fetched value. @@ -450,7 +450,7 @@ This table is authoritative: every entry is exactly the set of prover-supplied p | `set_spender` | $$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}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$, `proof` | | `revoke_spender` | $$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 contract loads it from the `(from, spender)` 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_spender`, by contrast, has no prior delegation entry to load from, so its $$\sigma\_a$$ is prover-supplied and bound by S6. +For `confidential_transfer_from`, the stored allowance salt $$\sigma\_a$$ is **not** carried in `data`: the contract loads it from the `(from, spender)` delegation entry (§7.8 public-input table). Only the prover-chosen replacement $$\sigma\_a'$$ travels in `data`. It is bound as this transfer's channel nonce (O7, O9, O\_a2, O\_a6), as the new allowance state's salt (O10, O12), and against the salt it replaces (O14); it is then written back to the delegation entry as the new `allowance_salt` and emitted in the event (§6.2 *Transfer nonce*). 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_spender`, 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 @@ -485,15 +485,15 @@ Each state-modifying operation emits a structured event. Events carry the data n | `Merge` | `account` | | `Withdraw` | `from`, `to`, `amount`, $$R\_e$$, $$\sigma$$, $$\tilde{b}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | -| `SpenderTransfer` | `spender`, `from`, `to`, $$R\_e$$, $$\tilde{v}$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | -| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\sigma\_a$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$ | +| `SpenderTransfer` | `spender`, `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}}$$, $$\tilde{r}\_{\text{aud,s}}$$ | +| `SetSpender` | `account`, `spender`, `live_until_ledger`, $$R\_e$$, $$\sigma$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$, $$\tilde{r}\_{a,\text{aud,s}}$$ | | `RevokeSpender` | `account`, `spender`, $$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 `SpenderTransfer` events using $$(R\_e, \tilde{v}, \sigma)$$ to derive $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$ (Section 5.3). +- **Recipient wallet**: processes `Transfer` and `SpenderTransfer` events using $$(R\_e, \tilde{v}, \sigma)$$ -- with $$\sigma\_a'$$ in place of $$\sigma$$ for `SpenderTransfer` -- to derive $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$ (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). diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index 8a09f224a..d5eba3b6c 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -396,12 +396,14 @@ The projection MUST still be reconciled against the event, and MUST NOT be treat ### 10.4 Salt freshness -A fresh $$\sigma$$ MUST be sampled for every **attempt**, including retries after a reverted or dropped transaction. +A fresh salt MUST be sampled for every **attempt**, including retries after a reverted or dropped transaction. The salt that must be fresh is the one the operation's pads absorb: $$\sigma$$ for owner-initiated operations, and $$\sigma_a'$$ — the replacement allowance salt — for spender transfers (DESIGN.md §6.2 *Transfer nonce*). -DESIGN_cont.md §9.6 motivates this as unlinkability: a fresh $$\sigma$$ prevents an observer correlating a reverted attempt with its retry. It is equally a confidentiality requirement, because the salt is the sole freshness input to every derived pad in the operation, the ephemeral scalar included (DESIGN.md §2.5, §5.3). Reuse therefore repeats the ephemeral key and every channel mask that depends on it. +DESIGN_cont.md §9.6 motivates this as unlinkability: a fresh salt prevents an observer correlating a reverted attempt with its retry. It is equally a confidentiality requirement, because the salt is the sole freshness input to every derived pad in the operation, the ephemeral scalar included (DESIGN.md §2.5, §5.3). Reuse therefore repeats the ephemeral key and every channel mask that depends on it, and two attempts that differ only in amount publish that difference in the clear. An implementation MUST NOT cache or reuse a salt across attempts, and MUST NOT derive it from anything an observer can predict. +**Freshness is not enforced on-chain.** Constraint O14 rejects only $$\sigma_a' = \sigma_a$$, the adjacent collision; a circuit cannot see a delegation's older salts. An implementation MUST therefore treat non-repetition of $$\sigma_a'$$ over the whole life of a delegation as its own obligation, and MUST NOT cycle salts through a bounded set. + ### 10.5 Deterministic ephemeral scalars An implementation MUST derive the ephemeral scalar of every operation the holder or spender originates: diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 1ab37b748..857d55dac 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -74,7 +74,7 @@ This document reuses the notation, key hierarchy, and commitment scheme from DES - $$sk\_A$$, $$vk\_A$$, $$\text{PVK}\_A$$: an account's spending key, viewing key, and public viewing key (DESIGN.md §4). - $$\text{addr\\\_f}$$: the contract's compressed address Field $$\text{address\\\_to\\\_field}(\text{contract})$$, bound into $$vk$$ derivation (DESIGN.md §2.7, §4.2). Stored once at construction in the contract's instance storage (DESIGN.md §3.5). - $$K\_{\text{aud,s}}$$, $$K\_{\text{aud,r}}$$, $$aud\_{sk}$$: the sender-side and recipient-side auditor Grumpkin public keys, and an auditor's secret key (DESIGN_cont.md §8.1, §8.3). Each account selects an `auditor_id` at registration; the same `auditor_id` may resolve to either role depending on the transfer's direction. -- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a$$ in place of $$\sigma$$, the post-transfer allowance salt $$\sigma\_a'$$ is emitted alongside it, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, with lane 2's $$\tilde{r}\_{\text{aud,s}}$$ carrying the post-transfer allowance blinding $$r\_a'$$ rather than a spendable one (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. +- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a'$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, with lane 2's $$\tilde{r}\_{\text{aud,s}}$$ carrying the post-transfer allowance blinding $$r\_a'$$ rather than a spendable one (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a'$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. - $$H$$: the Grumpkin Pedersen generator used uniformly for key derivation and ECDH (DESIGN.md §2.3, §2.4). ### 2.1 Disclosure Recipient @@ -174,7 +174,7 @@ Given a bundle for $$(P\_R, \nu)$$ that this verifier previously issued, the rec 1. **Resolve the event.** Look up $$\text{ref}\_E$$ via the indexer or via direct RPC of the transaction. The lookup MUST return exactly one event whose contract address equals the deployed confidential-token contract. Extract the event's payload fields verbatim: - For `Transfer`: `from`, `to`, $$R\_e$$, $$\sigma$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). - - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a$$, $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). + - For `SpenderTransfer`: `spender`, `from`, `to`, $$R\_e$$, $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN_cont.md §11.2). Any other event type, or a `circuit_id` whose constraints reference a field the event does not carry, is rejected here. @@ -210,7 +210,7 @@ Either way the request and the proof are public, which is the deliberate privacy ## 6. Circuit D-recipient: Holder Discloses an Inbound Transfer -The account holder is the recipient of an on-chain confidential transfer (either a `Transfer` to them or a `SpenderTransfer` whose `to` is them) and proves to a third party that the transfer was for amount $$v\_{\text{transfer}}$$. The same circuit covers both event families because the recipient-side ECDH constraint has identical shape in either case; only the value of the event nonce $$\sigma\_E$$ differs ($$\sigma$$ for `Transfer`, $$\sigma\_a$$ for `SpenderTransfer`; see DESIGN.md §7.6 T9, §7.8 O9). +The account holder is the recipient of an on-chain confidential transfer (either a `Transfer` to them or a `SpenderTransfer` whose `to` is them) and proves to a third party that the transfer was for amount $$v\_{\text{transfer}}$$. The same circuit covers both event families because the recipient-side ECDH constraint has identical shape in either case; only the value of the event nonce $$\sigma\_E$$ differs ($$\sigma$$ for `Transfer`, $$\sigma\_a'$$ for `SpenderTransfer`; see DESIGN.md §7.6 T9, §7.8 O9). **Public inputs** @@ -218,7 +218,7 @@ The account holder is the recipient of an on-chain confidential transfer (either |:---|:---| | $$\text{addr\\\_f}$$ | compressed contract-address Field, loaded from instance storage (DESIGN.md §2.7, §3.5) | | $$\text{PVK}\_A$$ | disclosing account's stored `viewing_public_key` (DESIGN.md §6.1); $$A$$ is the address listed as the event's `to` | -| $$R\_e, \sigma\_E, \tilde{v}$$ | from the on-chain event being disclosed (DESIGN_cont.md §11.2). $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a$$ for `SpenderTransfer`. | +| $$R\_e, \sigma\_E, \tilde{v}$$ | from the on-chain event being disclosed (DESIGN_cont.md §11.2). $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a'$$ for `SpenderTransfer`. | | $$P\_R$$ | disclosure recipient's Grumpkin pubkey (§2.1) | | $$\nu$$ | recipient-supplied nonce (§2.1) | | $$R\_{\text{disc}}, \tilde{v}\_{\text{disc}}$$ | disclosure ciphertext to recipient (§4) | @@ -255,7 +255,7 @@ In both cases the prover must supply the ephemeral scalar $$r\_e$$ as a witness: $$r\_e = \text{Poseidon2}(\delta\_{\text{eph}}, vk, \sigma\_E)$$ -where $$vk$$ is the originator's viewing key ($$vk\_A$$ for `Transfer`, $$vk\_{\text{op}}$$ for `SpenderTransfer`) and $$\sigma\_E$$ is the event nonce ($$\sigma$$ or $$\sigma\_a$$). The disclosed amount then follows: +where $$vk$$ is the originator's viewing key ($$vk\_A$$ for `Transfer`, $$vk\_{\text{op}}$$ for `SpenderTransfer`) and $$\sigma\_E$$ is the event nonce ($$\sigma$$ or $$\sigma\_a'$$). The disclosed amount then follows: $$v\_{\text{transfer}} = \tilde{v} - \text{Poseidon}(\delta\_{\text{transfer\\\_amount}}, \text{ECDH}(r\_e, \text{PVK}\_B), \sigma\_E) \qquad \text{(DESIGN.md §2.4)}$$ @@ -263,7 +263,7 @@ with $$\text{PVK}\_B$$ read from the event's `to` address. Both quantities come **What this implies for $$vk$$.** Since $$r\_e$$ is recoverable from $$vk$$, so is a full Pedersen opening of every transfer the account originated. DESIGN_cont.md §9.4 states the capability a compromised $$vk$$ therefore carries, and DESIGN_cont.md §8.2 records that per-transfer openings are not exclusive to the recipient's auditor. The operative consequence for this layer is that a counterparty needing outbound visibility is served with D-sender proofs, which are bound to that counterparty and to a nonce (§13.2) — never by handing over $$vk$$ (SDK.md §13). -In the symbols below, $$A$$ denotes the **originating** address — the holder's address for `Transfer` and the spender's address for `SpenderTransfer`. $$sk\_A$$ is the originator's spending key, $$\text{PVK}\_A$$ is the originator's stored public viewing key, and $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a$$ for `SpenderTransfer`. +In the symbols below, $$A$$ denotes the **originating** address — the holder's address for `Transfer` and the spender's address for `SpenderTransfer`. $$sk\_A$$ is the originator's spending key, $$\text{PVK}\_A$$ is the originator's stored public viewing key, and $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a'$$ for `SpenderTransfer`. **Public inputs** @@ -316,7 +316,7 @@ The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{ | Symbol | Source | |:---|:---| | $$K\_{\text{aud}}$$ | auditor's on-chain Grumpkin pubkey for the chosen channel ($$K\_{\text{aud,r}}$$ or $$K\_{\text{aud,s}}$$) (DESIGN_cont.md §8.3) | -| $$R\_e, \sigma\_E, \tilde{v}\_{\text{aud}}$$ | from the on-chain event ($$\tilde{v}\_{\text{aud,r}}$$ for the recipient-side channel, $$\tilde{v}\_{\text{aud,s}}$$ for the sender-side channel). $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a$$ for `SpenderTransfer` (DESIGN.md §7.8). | +| $$R\_e, \sigma\_E, \tilde{v}\_{\text{aud}}$$ | from the on-chain event ($$\tilde{v}\_{\text{aud,r}}$$ for the recipient-side channel, $$\tilde{v}\_{\text{aud,s}}$$ for the sender-side channel). $$\sigma\_E = \sigma$$ for `Transfer`, $$\sigma\_E = \sigma\_a'$$ for `SpenderTransfer` (DESIGN.md §7.8). | | $$P\_R, \nu$$ | disclosure recipient pubkey and nonce | | $$R\_{\text{disc}}, \tilde{v}\_{\text{disc}}$$ | disclosure ciphertext | @@ -401,7 +401,7 @@ For statements of the form "this account received at least $$X$$ from counterpar | Symbol | Source | |:---|:---| | Common: $$\text{addr\\\_f}$$, $$\text{PVK}\_A$$, $$P\_R$$, $$\nu$$, $$R\_{\text{disc}}, \tilde{V}\_{\text{disc}}$$ | as in §6 | -| List: $$(R\_{e,i}, \sigma\_{E,i}, \tilde{v}\_i)$$ for $$i \in [1, n]$$ | from $$n$$ on-chain transfer-family events; $$\sigma\_{E,i} = \sigma$$ if event $$i$$ is a `Transfer`, $$\sigma\_a$$ if `SpenderTransfer`. Each event MUST be identified by a $$\text{ref}\_{E,i}$$ in the proof bundle and resolved per §5.3. | +| List: $$(R\_{e,i}, \sigma\_{E,i}, \tilde{v}\_i)$$ for $$i \in [1, n]$$ | from $$n$$ on-chain transfer-family events; $$\sigma\_{E,i} = \sigma$$ if event $$i$$ is a `Transfer`, $$\sigma\_a'$$ if `SpenderTransfer`. Each event MUST be identified by a $$\text{ref}\_{E,i}$$ in the proof bundle and resolved per §5.3. | | Optional: $$V\_{\text{threshold}}$$ | aggregate threshold | **Private witnesses:** $$sk\_A$$, $$vk\_A$$, $$\\{v\_{\text{transfer},i}\\}\_{i=1}^n$$, $$r\_{\text{disc}}$$. @@ -432,7 +432,7 @@ The confidential-token contract requires no new state-modifying entry points to |:---|:---|:---| | `confidential_balance(account) -> ConfidentialAccount` | Verifier extracts $$\text{PVK}\_A$$ (and $$\text{PVK}\_B$$ for D-sender, $$C\_{\text{spend}}$$ for D-balance) from the returned `ConfidentialAccount` tuple | Already exposed (DESIGN_cont.md §11.3); the struct carries every field this layer reads, so no narrower accessor is required | | Auditor contract's key lookup for `auditor_id` | Verifier looks up $$K\_{\text{aud,r}}$$ or $$K\_{\text{aud,s}}$$ | Already exposed (DESIGN_cont.md §8.3). The auditor contract MAY maintain a sequence of versioned keys per `auditor_id` with activation ledgers; the verifier MUST select the version whose activation ledger is the largest value not exceeding the disclosed event's ledger (DESIGN_cont.md §8.3, *Auditor's off-chain obligation*). | -| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a$$ in place of $$\sigma$$, additionally emits $$\sigma\_a'$$, and uses $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$. | +| Transfer-family events | Verifier reads the per-event fields ($$R\_e$$, $$\sigma$$ or $$\sigma\_a'$$, $$\tilde{v}$$, $$\tilde{b}$$, $$\tilde{v}\_{\text{aud,r}}$$, $$\tilde{r}\_{\text{aud,r}}$$, $$\tilde{v}\_{\text{aud,s}}$$, $$\tilde{b}\_{\text{aud,s}}$$ / $$\tilde{a}\_{\text{aud,s}}$$, $$\tilde{r}\_{\text{aud,s}}$$) | Already emitted (DESIGN_cont.md §11.2). `SpenderTransfer` uses $$\sigma\_a'$$ in place of $$\sigma$$ and $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$. | | Instance storage: $$\text{addr\\\_f}$$ | D-recipient, D-sender, and D-balance bind $$vk$$ derivation to the contract via $$\text{addr\\\_f}$$ | Computed once at construction (DESIGN.md §3.5); the verifier reproduces it from the contract address using the encoding in DESIGN.md §2.7 | These are the only on-chain dependencies. Disclosure proofs are otherwise self-contained off-chain artifacts. diff --git a/packages/tokens/src/confidential/mod.rs b/packages/tokens/src/confidential/mod.rs index 405953ba6..d7825631a 100644 --- a/packages/tokens/src/confidential/mod.rs +++ b/packages/tokens/src/confidential/mod.rs @@ -401,7 +401,7 @@ pub trait ConfidentialToken { /// /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` - /// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, + /// * data - `[r_e_point, v_tilde, sigma_a_new, v_tilde_aud_r, /// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, r_tilde_aud_s]` fn confidential_transfer_from( e: &Env, @@ -448,7 +448,7 @@ pub trait ConfidentialToken { /// # Events /// /// * topics - `["set_spender", account: Address, spender: Address]` - /// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, + /// * data - `[live_until_ledger: u32, r_e_point, sigma, b_tilde, /// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` fn set_spender( e: &Env, @@ -756,7 +756,6 @@ pub struct SpenderTransfer { pub to: Address, pub r_e_point: BytesN<64>, pub v_tilde: BytesN<32>, - pub sigma_a: BytesN<32>, pub sigma_a_new: BytesN<32>, pub v_tilde_aud_r: BytesN<32>, pub r_tilde_aud_r: BytesN<32>, @@ -774,7 +773,6 @@ pub fn emit_spender_transfer( to: &Address, r_e_point: &BytesN<64>, v_tilde: &BytesN<32>, - sigma_a: &BytesN<32>, sigma_a_new: &BytesN<32>, v_tilde_aud_r: &BytesN<32>, r_tilde_aud_r: &BytesN<32>, @@ -788,7 +786,6 @@ pub fn emit_spender_transfer( to: to.clone(), r_e_point: r_e_point.clone(), v_tilde: v_tilde.clone(), - sigma_a: sigma_a.clone(), sigma_a_new: sigma_a_new.clone(), v_tilde_aud_r: v_tilde_aud_r.clone(), r_tilde_aud_r: r_tilde_aud_r.clone(), @@ -810,7 +807,6 @@ pub struct SetSpender { pub live_until_ledger: u32, pub r_e_point: BytesN<64>, pub sigma: BytesN<32>, - pub sigma_a: BytesN<32>, pub b_tilde: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, @@ -827,7 +823,6 @@ pub fn emit_set_spender( live_until_ledger: u32, r_e_point: &BytesN<64>, sigma: &BytesN<32>, - sigma_a: &BytesN<32>, b_tilde: &BytesN<32>, v_tilde_aud_s: &BytesN<32>, b_tilde_aud_s: &BytesN<32>, @@ -840,7 +835,6 @@ pub fn emit_set_spender( live_until_ledger, r_e_point: r_e_point.clone(), sigma: sigma.clone(), - sigma_a: sigma_a.clone(), b_tilde: b_tilde.clone(), v_tilde_aud_s: v_tilde_aud_s.clone(), b_tilde_aud_s: b_tilde_aud_s.clone(), diff --git a/packages/tokens/src/confidential/storage.rs b/packages/tokens/src/confidential/storage.rs index ed48ef644..9c447f346 100644 --- a/packages/tokens/src/confidential/storage.rs +++ b/packages/tokens/src/confidential/storage.rs @@ -115,8 +115,6 @@ pub struct WithdrawPayload { pub r_e_point: Point, pub sigma: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, - /// Sender-auditor secret-escrow slot (sponge lane 2): the blinding of the - /// new spendable commitment, `r' + m_r_s` (constraint W_a5). pub r_tilde_aud_s: BytesN<32>, } @@ -144,8 +142,6 @@ pub struct TransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, - /// Sender-auditor secret-escrow slot (sponge lane 2): the blinding of the - /// sender's new spendable commitment, `r_A' + m_r_s` (constraint T_a9). pub r_tilde_aud_s: BytesN<32>, } @@ -173,8 +169,6 @@ pub struct SpenderTransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, - /// Owner-auditor blinding-escrow slot (sponge lane 2): the blinding of - /// the new allowance commitment, `r_a' + m_r_s` (constraint O_a9). pub r_tilde_aud_s: BytesN<32>, } @@ -201,14 +195,7 @@ pub struct SetSpenderPayload { pub sigma_a: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, - /// Owner-auditor secret-escrow slot (sponge lane 2): the blinding of the - /// new spendable commitment, `r' + m_r_s` (constraint S_a6). pub r_tilde_aud_s: BytesN<32>, - /// Owner-auditor escrow of the allowance blinding, - /// `r_a + Poseidon2(delta_esc_allow_r_aud, s_a_s, op_i)` (constraint - /// S14). Distinct from `r_tilde_aud_s` above, which escrows the *spendable* - /// blinding, and from `escrowed_dvk`, which hands the *spender* the - /// delegation viewing key. pub r_a_tilde_aud_s: BytesN<32>, } @@ -783,8 +770,8 @@ pub fn confidential_transfer( /// /// * topics - `["spender_transfer", spender: Address, from: Address, to: /// Address]` -/// * data - `[r_e_point, v_tilde, sigma_a, sigma_a_new, v_tilde_aud_r, -/// r_tilde_aud_r, v_tilde_aud_s, a_tilde_aud_s, r_tilde_aud_s]` +/// * data - `[r_e_point, v_tilde, sigma_a_new, v_tilde_aud_r, r_tilde_aud_r, +/// v_tilde_aud_s, a_tilde_aud_s, r_tilde_aud_s]` /// /// # Security Warning /// @@ -813,8 +800,6 @@ pub fn confidential_transfer_from( // owner). let k_aud_s = auditor.get_key(&owner.auditor_id); - // Capture it here to emit in the event below. - let sigma_a = delegation.allowance_salt.clone(); // PI order (DESIGN §7.8): // C_a, sigma_a, Y_op, PVK_recipient, K_aud_r, K_aud_s, // C_a', C_transfer, R_e, v_tilde, a_tilde', sigma_a', @@ -858,7 +843,6 @@ pub fn confidential_transfer_from( to, &payload.r_e_point, &payload.v_tilde, - &sigma_a, &payload.sigma_a_new, &payload.v_tilde_aud_r, &payload.r_tilde_aud_r, @@ -899,8 +883,8 @@ pub fn confidential_transfer_from( /// # Events /// /// * topics - `["set_spender", account: Address, spender: Address]` -/// * data - `[live_until_ledger: u32, r_e_point, sigma, sigma_a, b_tilde, -/// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` +/// * data - `[live_until_ledger: u32, r_e_point, sigma, b_tilde, v_tilde_aud_s, +/// b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` /// /// # Security Warning /// @@ -969,7 +953,6 @@ pub fn set_spender( live_until_ledger, &payload.r_e_point, &payload.sigma, - &payload.sigma_a, &payload.b_tilde, &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, diff --git a/packages/tokens/src/confidential/test.rs b/packages/tokens/src/confidential/test.rs index 0216a51ca..e71ffb57a 100644 --- a/packages/tokens/src/confidential/test.rs +++ b/packages/tokens/src/confidential/test.rs @@ -702,7 +702,6 @@ fn set_spender_stores_delegation() { live_until_ledger: 1_000u32, r_e_point: fixture_point(&h.e), sigma: fixture_field(&h.e, 0x23), - sigma_a: fixture_field(&h.e, 0x24), b_tilde: fixture_field(&h.e, 0x21), v_tilde_aud_s: fixture_field(&h.e, 0x25), b_tilde_aud_s: fixture_field(&h.e, 0x26), @@ -808,7 +807,6 @@ fn auditor_key_rotation_rescopes_the_escrowed_allowance_opening() { live_until_ledger: 1_000u32, r_e_point: fixture_point(&h.e), sigma: fixture_field(&h.e, 0x23), - sigma_a: fixture_field(&h.e, 0x24), b_tilde: fixture_field(&h.e, 0x21), v_tilde_aud_s: fixture_field(&h.e, 0x25), b_tilde_aud_s: fixture_field(&h.e, 0x26), @@ -841,7 +839,6 @@ fn auditor_key_rotation_rescopes_the_escrowed_allowance_opening() { to: bob.clone(), r_e_point: fixture_point(&h.e), v_tilde: fixture_field(&h.e, 0x31), - sigma_a: fixture_field(&h.e, 0x24), sigma_a_new: fixture_field(&h.e, 0x33), v_tilde_aud_r: fixture_field(&h.e, 0x34), r_tilde_aud_r: fixture_field(&h.e, 0x35), @@ -908,8 +905,8 @@ fn confidential_transfer_from_updates_delegation_and_recipient() { to: bob.clone(), r_e_point: fixture_point(&h.e), v_tilde: fixture_field(&h.e, 0x31), - // The delegation's pre-transfer salt, read from storage. - sigma_a: fixture_field(&h.e, 0x24), + // The payload's sigma_a', not the stored allowance_salt (0x24) it + // replaces: every pad and the ephemeral scalar are keyed to it. sigma_a_new: fixture_field(&h.e, 0x33), v_tilde_aud_r: fixture_field(&h.e, 0x34), r_tilde_aud_r: fixture_field(&h.e, 0x35), @@ -928,6 +925,50 @@ fn confidential_transfer_from_updates_delegation_and_recipient() { assert_ne!(bob_acc.receiving_commitment.to_array(), [0u8; 64]); } +/// The `SpenderTransfer` event carries the transfer's channel nonce +/// `sigma_a'`, not the stored `allowance_salt` it replaces (DESIGN §6.2 +/// *Transfer nonce*). Emitting the stored salt would hand the recipient and +/// both auditors a nonce none of the pads absorbed, and would repeat across a +/// retry. +#[test] +fn confidential_transfer_from_emits_new_salt_as_channel_nonce() { + let h = setup(); + let alice = Address::generate(&h.e); + let spender = Address::generate(&h.e); + let bob = Address::generate(&h.e); + + h.token.register(&alice, &1u32, ®ister_data(&h.e)); + h.token.register(&spender, &1u32, ®ister_data(&h.e)); + h.token.register(&bob, &1u32, ®ister_data(&h.e)); + h.token.set_spender(&alice, &spender, &1_000u32, &set_spender_data(&h.e)); + let stored_salt = h.token.get_spender_delegation(&alice, &spender).allowance_salt; + assert_eq!(stored_salt, fixture_field(&h.e, 0x24)); + + h.token.confidential_transfer_from(&spender, &alice, &bob, &spender_transfer_data(&h.e)); + + let events = h.e.events().all(); + assert_eq!(events.events().len(), 1); + assert_eq!( + events.events().first().unwrap(), + &SpenderTransfer { + spender: spender.clone(), + from: alice.clone(), + to: bob.clone(), + r_e_point: fixture_point(&h.e), + v_tilde: fixture_field(&h.e, 0x31), + sigma_a_new: fixture_field(&h.e, 0x33), + v_tilde_aud_r: fixture_field(&h.e, 0x34), + r_tilde_aud_r: fixture_field(&h.e, 0x35), + v_tilde_aud_s: fixture_field(&h.e, 0x36), + a_tilde_aud_s: fixture_field(&h.e, 0x37), + r_tilde_aud_s: fixture_field(&h.e, 0x38), + } + .to_xdr(&h.e, &h.token_addr) + ); + let delegation = h.token.get_spender_delegation(&alice, &spender); + assert_eq!(delegation.allowance_salt, fixture_field(&h.e, 0x33)); +} + #[test] #[should_panic(expected = "Error(Contract, #3505)")] fn confidential_transfer_from_expired_panics() { From f7dbf1fd06666677a45e9b8ffc54c7e3595ff19c Mon Sep 17 00:00:00 2001 From: brozorec <9572072+brozorec@users.noreply.github.com> Date: Fri, 28 Aug 2026 16:31:44 +0200 Subject: [PATCH 12/12] docs(confidential): write sponge lanes as lane[i] instead of "lane N" Bare cardinals read as ordinals -- "lane 2" invites "second lane" when it means the third slot, index 2. Every specific-lane reference now uses the zero-based index notation, defined normatively in DESIGN.md 2.5, and the ambiguous [0..1] range in the prefix-property formulas is replaced by an explicit per-index equality. --- .../src/confidential/circuits/CLAUDE.md | 4 +-- .../src/confidential/circuits/lib/src/lib.nr | 26 ++++++++--------- .../confidential/circuits/lib/src/tests.nr | 26 ++++++++--------- .../circuits/set_spender/src/main.nr | 2 +- .../circuits/set_spender/src/tests.nr | 4 +-- .../circuits/spender_transfer/src/tests.nr | 6 ++-- .../circuits/transfer/src/tests.nr | 4 +-- .../circuits/withdraw/src/main.nr | 8 +++--- .../circuits/withdraw/src/tests.nr | 6 ++-- .../src/confidential/docs/COMPLIANCE.md | 4 +-- .../tokens/src/confidential/docs/DESIGN.md | 8 +++--- .../src/confidential/docs/DESIGN_cont.md | 28 +++++++++---------- packages/tokens/src/confidential/docs/SDK.md | 16 +++++------ .../confidential/docs/SELECTIVE_DISCLOSURE.md | 8 +++--- 14 files changed, 75 insertions(+), 75 deletions(-) diff --git a/packages/tokens/src/confidential/circuits/CLAUDE.md b/packages/tokens/src/confidential/circuits/CLAUDE.md index 3add5f8c0..a1363bb9d 100644 --- a/packages/tokens/src/confidential/circuits/CLAUDE.md +++ b/packages/tokens/src/confidential/circuits/CLAUDE.md @@ -38,9 +38,9 @@ Directory `transfer/` is package `circuit_transfer`; `gadgets/commit/` is `gadge Sponge parameters, the canonical lane assignment, and the mode-exclusivity rule that follows from a single-block absorb are normative in `../docs/DESIGN.md` §2.5; the Noir sponge must match it exactly. The obligations that section places on this code: `sponge_squeeze_2(d,s,σ)[0]` must stay equal to `poseidon_with_domain(d,[s,σ])`, and `sponge_squeeze_3(d,s,σ)[0..2]` must stay equal to `sponge_squeeze_2(d,s,σ)` — which is why `sponge_squeeze_2` is defined as the prefix of `sponge_squeeze_3` rather than as a second permutation. A divergence in either silently changes every existing mask. -`AUDITOR_SENDER` is squeezed three-wide by every circuit that escrows lane 2 and two-wide only by RevokeSpender (V_a3); `AUDITOR_RECIPIENT` is always two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. +`AUDITOR_SENDER` is squeezed three-wide by every circuit that escrows `lane[2]` and two-wide only by RevokeSpender (V_a3); `AUDITOR_RECIPIENT` is always two-wide; every other tag goes through `poseidon_with_domain`. Widening or narrowing a channel is a spec change, not a refactor. -Lane 2 carries **the blinding of a commitment the operation writes, never a key** — `r'` on W_a5 / T_a9 / S_a6, `r_a'` on O_a9. Tag 17 (`ESCROWED_ALLOWANCE_BLINDING_AUDITOR`) is the same idea off-sponge: SetSpender's lane 2 is already taken, so S14 escrows `r_a` under a single-output pad. Do not escrow `dvk_i` here: it is permanent per `(owner, spender)` and survives revoke-then-re-delegate, so one leaked ciphertext would open every allowance state for that pair, past and future (`../docs/DESIGN_cont.md` §8.5). +`lane[2]` carries **the blinding of a commitment the operation writes, never a key** — `r'` on W_a5 / T_a9 / S_a6, `r_a'` on O_a9. Tag 17 (`ESCROWED_ALLOWANCE_BLINDING_AUDITOR`) is the same idea off-sponge: SetSpender's `lane[2]` is already taken, so S14 escrows `r_a` under a single-output pad. Do not escrow `dvk_i` here: it is permanent per `(owner, spender)` and survives revoke-then-re-delegate, so one leaked ciphertext would open every allowance state for that pair, past and future (`../docs/DESIGN_cont.md` §8.5). ECDH must absorb both `S.x` and `S.y`; x-only extraction collapses `P` and `-P`. diff --git a/packages/tokens/src/confidential/circuits/lib/src/lib.nr b/packages/tokens/src/confidential/circuits/lib/src/lib.nr index ac52212fa..2b9240534 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/lib.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/lib.nr @@ -133,8 +133,8 @@ pub mod domain { pub global ESCROWED_DELEGATION_VIEWING_KEY: Field = 10; /// Sender or owner-auditor channel tag for Poseidon2 sponge masks /// (Section 2.5, Section 8.1). **Three lanes**, and the only tag squeezed - /// three-wide: lane 0 is the amount mask (where applicable), lane 1 the - /// balance/allowance checkpoint mask, lane 2 the commitment-blinding + /// three-wide: lane[0] is the amount mask (where applicable), lane[1] the + /// balance/allowance checkpoint mask, lane[2] the commitment-blinding /// escrow slot -- the new spendable blinding on checkpoints, the new /// allowance blinding on spender transfers. Constraints W_a3 / T_a6 / /// S_a3 / O_a6 (`delta_aud_s`). @@ -342,8 +342,8 @@ pub fn encrypt_esc_dvk(dvk: Field, s: Field, op_i: Field) -> Field { /// `r_a_tilde_aud_s = r_a + Poseidon2(ESC_ALLOW_R_AUD, s_a_s, op_i)` where /// `s_a_s = ecdh(r_e, K_aud_s)` is the *owner-auditor* shared scalar. /// -/// A single-output Poseidon rather than a third sponge lane, because at -/// `set_spender` lane 2 of the owner-auditor sponge is already taken by S_a6. +/// A single-output Poseidon rather than a further sponge lane, because at +/// `set_spender` lane[2] of the owner-auditor sponge is already taken by S_a6. /// Costs one Poseidon2 permutation and zero scalar multiplications. `op_i` is /// a per-`(owner, spender)` constant that separates pads across delegations, /// not a nonce: the pad's freshness comes solely from `s_a_s`, which is fresh @@ -375,7 +375,7 @@ pub fn encrypt_esc_allow_r_auditor(r_a: Field, s_a_s: Field, op_i: Field) -> Fie /// /// Used by the recipient-auditor channel of Transfer (T_a2) and /// SpenderTransfer (O_a2), and by RevokeSpender's sender-auditor channel -/// (V_a3), which escrows nothing in lane 2. Every other sender-auditor +/// (V_a3), which escrows nothing in lane[2]. Every other sender-auditor /// channel reads [`sponge_squeeze_3`]. /// /// Defined as the two-lane prefix of [`sponge_squeeze_3`] so the two cannot @@ -389,13 +389,13 @@ pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { /// Poseidon2 sponge with three squeezes. Absorbs `(d, s, sigma)` and returns /// the first three rate-elements of the post-permutation state in order. /// -/// Implements `SpongeSqueeze_3(delta, s, sigma)` from Section 2.5. Lane 0 is -/// the amount mask and lane 1 the balance / allowance / randomness mask, as in -/// [`sponge_squeeze_2`]. **Lane 2 is the sender-auditor blinding-escrow -/// slot**: it carries the blinding of a commitment the operation writes, and -/// never a key -- the new spendable blinding on the three checkpoint -/// operations (W_a5 / T_a9 / S_a6), the new allowance blinding on spender -/// transfers (O_a9). +/// Implements `SpongeSqueeze_3(delta, s, sigma)` from Section 2.5. As in +/// [`sponge_squeeze_2`], lane[0] is the amount mask and lane[1] the balance / +/// allowance / randomness mask. **The lane[2] slot is the sender-auditor +/// blinding escrow**: it carries the blinding of a commitment the operation +/// writes, and never a key -- the new spendable blinding on the three +/// checkpoint operations (W_a5 / T_a9 / S_a6), the new allowance blinding on +/// spender transfers (O_a9). /// /// `sponge_squeeze_3(d, s, sigma)[0..2] == sponge_squeeze_2(d, s, sigma)`, by /// construction: the absorb fits one rate-3 block, so all three outputs are @@ -404,7 +404,7 @@ pub fn sponge_squeeze_2(d: Field, s: Field, sigma: Field) -> [Field; 2] { /// value and costs no extra constraints. The capacity lane at `state[3]` stays /// untouched. /// -/// The blindings lane 2 carries still belong to different commitment roles -- +/// The blindings lane[2] carries still belong to different commitment roles -- /// spendable on the checkpoints, allowance on spender transfers -- so the /// uniformity is documentary, not a claim that a recipient can substitute one /// for the other. Each is scoped to the operation that emitted it. diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 96d0cded5..1be685b2c 100644 --- a/packages/tokens/src/confidential/circuits/lib/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/lib/src/tests.nr @@ -236,7 +236,7 @@ fn sponge_squeeze_2_first_matches_poseidon_with_domain() { #[test] fn sponge_squeeze_3_agrees_with_squeeze_2_on_shared_lanes() { // The widening from two lanes to three must be observationally identical - // on lanes 0 and 1 for every domain tag: both squeezes read the same + // on lane[0] and lane[1] for every domain tag: both squeezes read the same // post-permutation state, and the absorb occupies a single rate-3 block. // This is what lets `delta_aud_s` be widened everywhere it appears // without renumbering a single existing constraint. @@ -252,8 +252,8 @@ fn sponge_squeeze_3_agrees_with_squeeze_2_on_shared_lanes() { #[test] fn sponge_squeeze_3_outputs_distinct() { - // Lane 2 comes from a third rate position of the same permutation and - // must differ from both others. A collision would let an observer who + // The lane[2] mask comes from rate position state[2] and must differ + // from both others. A collision would let an observer who // recovers the balance mask also recover the secret-escrow pad -- and // with it the account's spendable blinding, which is exactly what the // slot exists to keep between the sender and its auditor. @@ -501,19 +501,19 @@ fn fixtures_match_testdata() { assert(ss2_r[0] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0); assert(ss2_r[1] == SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1); - // Lane 0 is the single-squeeze funnel: the absorb fits one rate-3 block, - // so a tag used in both modes would collapse one onto the other - // (Section 2.5 mode exclusivity). + // The lane[0] output is the single-squeeze funnel: the absorb fits one + // rate-3 block, so a tag used in both modes would collapse one onto the + // other (Section 2.5 mode exclusivity). assert( poseidon_with_domain(domain::AUDITOR_SENDER, [s_ecdh, sigma]) == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0, ); - // The three-lane squeeze must agree with the two-lane one on lanes 0-1: - // the absorb fits one rate-3 block, so both are served from the same - // permutation. If this diverges, widening the sender-auditor channel - // silently changed every existing mask and every published ciphertext - // became undecryptable. + // The three-lane squeeze must agree with the two-lane one on lane[0] + // and lane[1]: the absorb fits one rate-3 block, so both are served from + // the same permutation. If this diverges, widening the sender-auditor + // channel silently changed every existing mask and every published + // ciphertext became undecryptable. let ss3_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_ecdh, sigma); assert(ss3_s[0] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_0); assert(ss3_s[1] == SPONGE_SQUEEZE_2_AUDITOR_SENDER_1); @@ -542,8 +542,8 @@ global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0: Field = 0x0801e6d7184cea11e6225fcfb4e88a90674cb085da8ebc04918c57cc44b9c25d; global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1: Field = 0x131187e2ac296c1c54c2be2a5cbe614644f01c754108ce25359212d67b36ecc3; -// Lane 2 of the same permutation -- the sender-auditor secret-escrow slot. -// Mirrored in `testdata/sponge_squeeze_3.json`. +// The lane[2] output of the same permutation -- the sender-auditor +// secret-escrow slot. Mirrored in `testdata/sponge_squeeze_3.json`. global SPONGE_SQUEEZE_3_AUDITOR_SENDER_2: Field = 0x1c4e67103733b87be6672e0af91fc5416b6d582bf99d5a0f78ea6aaf5fab20b0; diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr index 71fc7bad4..8ec7148bb 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/main.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/main.nr @@ -77,7 +77,7 @@ mod tests; // A separate single- // output Poseidon // rather than a sponge -// lane, because lane 2 +// lane, because lane[2] // of this channel is // taken by S_a6. Costs // one Poseidon and zero diff --git a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr index f8e2194c0..2596c0919 100644 --- a/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/set_spender/src/tests.nr @@ -84,7 +84,7 @@ global ESCROWED_DVK_CIPHER: Field = 0x1ef89e3bd93ec1afed649d150e9d0ff5df72002759e260cb66b06604a4d7ebe0; global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c4397; -// Lane-2 spendable-blinding escrow (S_a6) and the auditor-side allowance- +// lane[2] spendable-blinding escrow (S_a6) and the auditor-side allowance- // blinding escrow (S14). Both are blindings of commitments this operation // writes -- C_spend' and C_a respectively. global R_TILDE_AUD_S: Field = @@ -1522,7 +1522,7 @@ fn rejects_escrow_of_blinding_for_the_wrong_sigma_a() { #[test(should_fail)] fn rejects_tampered_r_tilde_aud_s() { - // r_tilde_aud_s mutated by +1: S_a6 fails because the lane-2 escrow is + // r_tilde_aud_s mutated by +1: S_a6 fails because the lane[2] escrow is // pinned to r' = derive_spend_r(vk, sigma), which the prover's claim no // longer matches. run_main( diff --git a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr index dde220ca0..efcda52cd 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -71,7 +71,7 @@ global R_TILDE_AUD_R: Field = 0x11504bfdaa7e3dfb4b38918d6ae2ba379abfda0a53ebc00fd1b561e805407c96; global V_TILDE_AUD_S: Field = 0x08ad26221dfe94a593dae37d3efdbe93a503a3cfe35bd10c3ab60061267c20fc; -// Lane-2 blinding-escrow slot (O_a9): the NEW allowance blinding r_a', the +// lane[2] blinding-escrow slot (O_a9): the NEW allowance blinding r_a', the // one O11 commits C_a' under, escrowed to the owner's auditor on every // spender transfer. Pad and plaintext both key off SIGMA_A_NEW. global R_TILDE_AUD_S: Field = @@ -1583,7 +1583,7 @@ fn repeated_r_e_and_sigma_a_new_repeat_every_channel_pad() { fn rejects_escrow_of_the_consumed_allowance_blinding() { // The prover escrows R_A -- the blinding of the allowance this transfer // CONSUMES -- instead of O10's r_a', the blinding of the allowance it - // writes. Correctly padded under the canonical lane-2 mask, so only O_a9 + // writes. Correctly padded under the canonical lane[2] mask, so only O_a9 // can catch it, and it does. // // The distinction matters because the auditor's job is to track live @@ -1631,7 +1631,7 @@ fn rejects_escrow_of_the_consumed_allowance_blinding() { #[test(should_fail)] fn rejects_tampered_r_tilde_aud_s() { - // r_tilde_aud_s mutated by +1: O_a9 fails because the lane-2 escrow is + // r_tilde_aud_s mutated by +1: O_a9 fails because the lane[2] escrow is // pinned to O10's r_a', which the prover's claim no longer matches. run_main( SK_OP, diff --git a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr index 78ec6851d..3003de492 100644 --- a/packages/tokens/src/confidential/circuits/transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/transfer/src/tests.nr @@ -71,7 +71,7 @@ global V_TILDE_AUD_S: Field = 0x0d0a3aa77960977737f4355093d4672ea9f95556fabf63bda0913ebef52c42cf; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; -// Lane-2 secret-escrow slot: r_tilde_aud_s = r_A' + m_r_s, over T10's r_A'. +// lane[2] secret-escrow slot: r_tilde_aud_s = r_A' + m_r_s, over T10's r_A'. global R_TILDE_AUD_S: Field = 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; @@ -1350,7 +1350,7 @@ fn rejects_tampered_b_tilde_aud_s() { #[test(should_fail)] fn rejects_tampered_r_tilde_aud_s() { - // r_tilde_aud_s mutated by +1: T_a9 fails because the lane-2 escrow is + // r_tilde_aud_s mutated by +1: T_a9 fails because the lane[2] escrow is // pinned to r_A' = derive_spend_r(vk, sigma), which the prover's claim no // longer matches. run_main( diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr index 5af565b88..bfcd6b985 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/main.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/main.nr @@ -44,9 +44,9 @@ mod tests; // W_a3 (m_b, m_r) = SpongeSqueeze_3(delta_aud_s, // s_{a,s}, // sigma)[1], [2] Sender-channel sponge, -// lanes 1 and 2. Lane 0 -// (the amount slot) is -// unused -- the +// lane[1] and lane[2]. +// The lane[0] amount +// slot is unused -- the // withdrawal amount is // public -- and skipping // it keeps the checkpoint @@ -179,7 +179,7 @@ fn main( // W_a2 (shared-secret scalar) let s_a_s = ecdh(r_e, k_aud_s); - // W_a3 (sender-channel masks: lane 0 unused, then balance, then the + // W_a3 (sender-channel masks: lane[0] unused, then balance, then the // secret-escrow slot) let m_s = sponge_squeeze_3(domain::AUDITOR_SENDER, s_a_s, sigma); diff --git a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr index fed12f4f3..a9911e7dd 100644 --- a/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/withdraw/src/tests.nr @@ -58,7 +58,7 @@ global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c07c; -// Lane-2 secret-escrow slot: r_tilde_aud_s = r' + m_r, over W5's r'. +// lane[2] secret-escrow slot: r_tilde_aud_s = r' + m_r, over W5's r'. global R_TILDE_AUD_S: Field = 0x12bae1b1e1b12493e37d427d3ee28b481c37f7f456c4ab0c022b8ba6262ca57f; @@ -537,7 +537,7 @@ fn rejects_identity_k_aud_s() { #[test(should_fail)] fn rejects_tampered_b_tilde_aud_s() { - // b_tilde_aud_s mutated by +1: W_a4 fails because the lane-1 pad derives + // b_tilde_aud_s mutated by +1: W_a4 fails because the lane[1] pad derives // a specific scalar that no longer matches the prover's claim. Closes the // tampered-ciphertext criterion in the issue. main( @@ -566,7 +566,7 @@ fn rejects_tampered_b_tilde_aud_s() { #[test(should_fail)] fn rejects_tampered_r_tilde_aud_s() { - // r_tilde_aud_s mutated by +1: W_a5 fails because the lane-2 escrow is + // r_tilde_aud_s mutated by +1: W_a5 fails because the lane[2] escrow is // pinned to r' = derive_spend_r(vk, sigma), which the prover's claim no // longer matches. main( diff --git a/packages/tokens/src/confidential/docs/COMPLIANCE.md b/packages/tokens/src/confidential/docs/COMPLIANCE.md index 4e111b648..4fa533739 100644 --- a/packages/tokens/src/confidential/docs/COMPLIANCE.md +++ b/packages/tokens/src/confidential/docs/COMPLIANCE.md @@ -215,9 +215,9 @@ The clawback proof is a constant-size circuit deployed through the existing Veri **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}}, m\_{r,s}^{\text{old}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{old}}), \sigma^{\text{old}})$$, then $$v\_s = \tilde{b}\_{\text{aud,s}}^{\text{old}} - m\_{b,s}^{\text{old}}$$ and $$r\_s = \tilde{r}\_{\text{aud,s}}^{\text{old}} - m\_{r,s}^{\text{old}}$$, and $$C\_{\text{spend}} = v\_s \cdot G + r\_s \cdot H$$. The lane-2 escrow (`DESIGN.md` W\_a5 / T\_a9 / S\_a6) is what makes this a closed derivation: the circuit recovers the blinding from the auditor channel itself rather than from $$vk\_A$$, which it does not hold. The old checkpoint referenced here must be the account's most recent lane-2 escrow; `revoke_spender` publishes none (V\_a3), so a target frozen immediately after a revocation must be settled against the checkpoint preceding it. +2. **Spendable-balance decryption.** $$(m\_{v,s}^{\text{old}}, m\_{b,s}^{\text{old}}, m\_{r,s}^{\text{old}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{old}}), \sigma^{\text{old}})$$, then $$v\_s = \tilde{b}\_{\text{aud,s}}^{\text{old}} - m\_{b,s}^{\text{old}}$$ and $$r\_s = \tilde{r}\_{\text{aud,s}}^{\text{old}} - m\_{r,s}^{\text{old}}$$, and $$C\_{\text{spend}} = v\_s \cdot G + r\_s \cdot H$$. The `lane[2]` escrow (`DESIGN.md` W\_a5 / T\_a9 / S\_a6) is what makes this a closed derivation: the circuit recovers the blinding from the auditor channel itself rather than from $$vk\_A$$, which it does not hold. The old checkpoint referenced here must be the account's most recent `lane[2]` escrow; `revoke_spender` publishes none (V\_a3), so a target frozen immediately after a revocation must be settled against the checkpoint preceding it. 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}}$$ is **sampled** by the auditor per `DESIGN.md` §2.2, not derived. This is the one operation in the protocol that samples its ephemeral scalar: `DESIGN.md` §5.3 derives $$r\_e$$ from the originator's viewing key, and the originator here is the auditor, which holds no account's viewing key. Then $$R\_e^{\text{new}} = r\_e^{\text{new}} \cdot H$$, $$r\_e^{\text{new}} \neq 0$$, and, with $$(m\_{v,s}^{\text{new}}, m\_{b,s}^{\text{new}}, m\_{r,s}^{\text{new}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{new}}), \sigma^{\text{new}})$$, both $$\tilde{b}\_{\text{aud,s}}^{\text{new}} = (v\_s + v\_r - \alpha) + m\_{b,s}^{\text{new}}$$ and $$\tilde{r}\_{\text{aud,s}}^{\text{new}} = r\_s' + m\_{r,s}^{\text{new}}$$. Re-escrowing lane 2 is what keeps the auditor's standing opening (`DESIGN_cont.md` §8.1) intact across a seizure. +4. **Refreshed checkpoint.** $$r\_e^{\text{new}}$$ is **sampled** by the auditor per `DESIGN.md` §2.2, not derived. This is the one operation in the protocol that samples its ephemeral scalar: `DESIGN.md` §5.3 derives $$r\_e$$ from the originator's viewing key, and the originator here is the auditor, which holds no account's viewing key. Then $$R\_e^{\text{new}} = r\_e^{\text{new}} \cdot H$$, $$r\_e^{\text{new}} \neq 0$$, and, with $$(m\_{v,s}^{\text{new}}, m\_{b,s}^{\text{new}}, m\_{r,s}^{\text{new}}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, \text{ECDH}(k\_{\text{aud,s}}, R\_e^{\text{new}}), \sigma^{\text{new}})$$, both $$\tilde{b}\_{\text{aud,s}}^{\text{new}} = (v\_s + v\_r - \alpha) + m\_{b,s}^{\text{new}}$$ and $$\tilde{r}\_{\text{aud,s}}^{\text{new}} = r\_s' + m\_{r,s}^{\text{new}}$$. Re-escrowing `lane[2]` is what keeps the auditor's standing opening (`DESIGN_cont.md` §8.1) intact across a seizure. 5. **New spendable commitment.** $$C\_{\text{spend}}^{\text{new}} = (v\_s + v\_r - \alpha) \cdot G + r\_s' \cdot H$$. The commitment is a public input rather than a value the contract recomputes, since $$v\_s$$, $$v\_r$$, and $$r\_s'$$ are all private witnesses. **Post-verification.** The contract stores the prover-supplied $$C\_{\text{spend}}^{\text{new}}$$, 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}}, \tilde{r}\_{\text{aud,s}}^{\text{new}}, R\_e^{\text{new}}, \sigma^{\text{new}})$$ so the auditor sees the new checkpoint and its blinding. diff --git a/packages/tokens/src/confidential/docs/DESIGN.md b/packages/tokens/src/confidential/docs/DESIGN.md index a0cac05d0..290d85c04 100644 --- a/packages/tokens/src/confidential/docs/DESIGN.md +++ b/packages/tokens/src/confidential/docs/DESIGN.md @@ -147,11 +147,11 @@ The domain tag is always the first element absorbed, so $$\text{Poseidon2}(\delt $$\text{SpongeSqueeze}\_n(\delta\_{\text{channel}}, s, \sigma) = \bigl(\text{state}[0], \\, \ldots, \\, \text{state}[n-1]\bigr), \qquad \text{state} = \text{permute}\bigl([\delta\_{\text{channel}}, \\, s, \\, \sigma, \\, 3 \cdot 2^{64}]\bigr)$$ -where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is the number of rate lanes read; the capacity lane $$\text{state}[3]$$ is never squeezed. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, squeezed three-wide, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$, squeezed two-wide. No other arity is instantiated. Because the absorb is one block, $$\text{SpongeSqueeze}\_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}\_2(\delta, s, \sigma)$$: widening a channel adds a lane without changing the value of any existing one. +where $$s$$ is the ECDH shared scalar of Section 2.4 and $$n \in \\{2, 3\\}$$ is the number of rate lanes read; the capacity lane $$\text{state}[3]$$ is never squeezed. Two channel tags are used: $$\delta\_{\text{aud\\\_s}}$$ for the sender-auditor channel keyed by $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$, squeezed three-wide, and $$\delta\_{\text{aud\\\_r}}$$ for the recipient-auditor channel keyed by $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}})$$, squeezed two-wide. No other arity is instantiated. Because the absorb is one block, $$\text{SpongeSqueeze}\_3(\delta, s, \sigma)[i] = \text{SpongeSqueeze}\_2(\delta, s, \sigma)[i]$$ for $$i \in \\{0, 1\\}$$: widening a channel adds a lane without changing the value of any existing one. -**Lane assignment.** Squeeze order is canonical. Lane 0 is always an amount mask and lane 1 is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. Lane 2, present only on the sender-auditor channel, is the **blinding-escrow slot**: it always carries the blinding of a commitment the operation writes, never a key. Which commitment per operation is specified in Section 8.1 *The lane-2 slot*. The Withdraw checkpoint (W\_a3, W\_a5) takes lanes 1 and 2 and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. +**Lane assignment.** Squeeze order is canonical. Lanes are named by their zero-based index into the squeeze output: `lane[i]` is $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[i]$$, so a three-wide squeeze yields `lane[0]`, `lane[1]`, and `lane[2]`. `lane[0]` is always an amount mask and `lane[1]` is always a balance, allowance, or randomness mask, fixed per operation by the formulas in Sections 7 and 8. `lane[2]`, present only on the sender-auditor channel, is the **blinding-escrow slot**: it always carries the blinding of a commitment the operation writes, never a key. Which commitment per operation is specified in Section 8.1 *The `lane[2]` slot*. The Withdraw checkpoint (W\_a3, W\_a5) takes `lane[1]` and `lane[2]` and leaves the amount lane unused, so a checkpoint pad can never coincide with an amount pad. -**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two multi-lane tags; every other tag in Section 13 is used only with the single-output form above. Reading one multi-lane tag at two different widths is not a mode violation, by the prefix property above: the sender-auditor tag is squeezed three-wide on every operation that escrows lane 2 and two-wide on `RevokeSpender` (V\_a3), while the recipient-auditor tag is always two-wide. +**Mode exclusivity.** Because the absorb occupies a single block, $$\text{SpongeSqueeze}\_n(\delta, s, \sigma)[0]$$ is the same field element as $$\text{Poseidon2}(\delta, s, \sigma)$$. Distinct domain tags (Section 13) are therefore not sufficient on their own: each tag MUST additionally be used in exactly one sponge mode, or the same $$(\delta, s, \sigma)$$ would yield one mode's mask as the other's output. $$\delta\_{\text{aud\\\_s}}$$ and $$\delta\_{\text{aud\\\_r}}$$ are the two multi-lane tags; every other tag in Section 13 is used only with the single-output form above. Reading one multi-lane tag at two different widths is not a mode violation, by the prefix property above: the sender-auditor tag is squeezed three-wide on every operation that escrows `lane[2]` and two-wide on `RevokeSpender` (V\_a3), while the recipient-auditor tag is always two-wide. The sponge masks are deterministic in $$(s, \sigma)$$, where $$s$$ is the ECDH shared scalar of Section 2.4, so reusing the pair across two operations reuses every pad slot they share, and a slot whose plaintext is known in one operation (e.g. a transfer amount known to its recipient) decrypts the other operation's ciphertext in that slot. The canonical slot assignment above limits the blast radius of such reuse to same-slot pairs, but does not eliminate it; provers and wallets MUST use a fresh $$(r\_e, \sigma)$$ for every proof. Because $$r\_e$$ is derived from the originator's viewing key and the salt rather than drawn independently (§5.3), a fresh salt is the only thing that makes the pair fresh: the salt carries the entire requirement, and Section 9.6's retry rule is what discharges it. The salt that carries it is whichever one the operation's pads absorb -- $$\sigma$$ for owner-initiated operations, the prover-chosen $$\sigma\_a'$$ for spender transfers (§6.2 *Transfer nonce*) -- never a salt loaded from storage, which a revert leaves unchanged. @@ -602,7 +602,7 @@ The owner withdraws a public amount $$a$$ (typed `i128`) from their spendable ba | 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} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ (sender-auditor ECDH shared scalar, §2.4) | -| W\_a3 | $$(\cdot, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel sponge; $$m\_b$$ is lane 1 — the balance slot, matching T\_a6/S\_a3 — and $$m\_r$$ is lane 2, the secret-escrow slot. Lane 0, the amount slot, is unused: the withdrawal amount is public, and skipping the slot keeps the checkpoint pad distinct from every amount pad even under $$(r\_e, \sigma)$$ reuse, Section 2.5) | +| W\_a3 | $$(\cdot, m\_b, m\_r) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ (sender-auditor channel sponge; $$m\_b$$ is `lane[1]` — the balance slot, matching T\_a6/S\_a3 — and $$m\_r$$ is `lane[2]`, the secret-escrow slot. `lane[0]`, the amount slot, is unused: the withdrawal amount is public, and skipping the slot keeps the checkpoint pad distinct from every amount pad even under $$(r\_e, \sigma)$$ reuse, Section 2.5) | | W\_a4 | $$\tilde{b}\_{\text{aud,s}} = (v - a) + m\_b$$ (sender-auditor encrypted balance checkpoint) | | W\_a5 | $$\tilde{r}\_{\text{aud,s}} = r' + m\_r$$ (sender-auditor escrow of the new spendable blinding, over W5's $$r'$$; with W\_a4 this hands the sender's auditor the opening of $$C\_{\text{spend}}'$$, Section 8.1) | diff --git a/packages/tokens/src/confidential/docs/DESIGN_cont.md b/packages/tokens/src/confidential/docs/DESIGN_cont.md index 374cfe616..9319a4102 100644 --- a/packages/tokens/src/confidential/docs/DESIGN_cont.md +++ b/packages/tokens/src/confidential/docs/DESIGN_cont.md @@ -23,7 +23,7 @@ $$s\_{a,r} = \text{ECDH}(r\_e, K\_{\text{aud,r}}) \qquad \text{(DESIGN §2.4)}$$ $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma)$$ $$\tilde{v}\_{\text{aud,r}} = v\_{\text{transfer}} + m\_{v,r}, \qquad \tilde{r}\_{\text{aud,r}} = r\_{\text{transfer}} + m\_{r,r}$$ -**Sender's auditor** ($$K\_{\text{aud,s}}$$, from the sender's `auditor_id`) receives the transfer amount, the sender's post-transfer balance, and -- in the lane-2 blinding-escrow slot (DESIGN §2.5 *Lane assignment*) -- the sender's post-transfer spendable blinding $$r\_A'$$: +**Sender's auditor** ($$K\_{\text{aud,s}}$$, from the sender's `auditor_id`) receives the transfer amount, the sender's post-transfer balance, and -- in the `lane[2]` blinding-escrow slot (DESIGN §2.5 *Lane assignment*) -- the sender's post-transfer spendable blinding $$r\_A'$$: $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}}) \qquad \text{(DESIGN §2.4)}$$ $$(m\_{v,s}, m\_{b,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma)$$ @@ -39,7 +39,7 @@ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_{\text{ 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{transfer}}, r\_{\text{transfer}})$$. -**The lane-2 slot.** Lane 2 of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the blinding of a commitment the operation writes, and never a key: the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6), and the new allowance blinding $$r\_a'$$ on spender transfers (O\_a9, Section 8.4). The two blindings belong to different commitment roles, so the uniformity is a naming convention, not an interchangeability claim; each is scoped to the operation that emitted it. The pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a')$$, both fresh per operation. +**The `lane[2]` slot.** The `lane[2]` output of the sender-auditor channel (DESIGN §2.5 *Lane assignment*) carries the blinding of a commitment the operation writes, and never a key: the new spendable blinding on the three checkpoint operations (W\_a5, T\_a9, S\_a6), and the new allowance blinding $$r\_a'$$ on spender transfers (O\_a9, Section 8.4). The two blindings belong to different commitment roles, so the uniformity is a naming convention, not an interchangeability claim; each is scoped to the operation that emitted it. The pad is fixed by $$(s\_{a,s}, \sigma)$$ or $$(s\_{a,s}, \sigma\_a')$$, both fresh per operation. **Recipient-auditor opening capability.** Because the recipient-auditor recovers $$r\_{\text{transfer}}$$ for every inbound transfer, and because deposits add to `receiving_commitment` with $$r = 0$$ (Section 7.3), the recipient-auditor can reconstruct the full Pedersen opening of $$C\_{\text{receive}}$$ between merges: @@ -50,13 +50,13 @@ where $$i$$ ranges over inbound transfers and spender-transfers since the last m The capability is bounded in three ways: - **Forward-only.** Only events emitted while the auditor key was active are decryptable. -- **Receiving-side only.** The reconstruction above covers `receiving_commitment`. It does not extend to $$C\_{\text{spend}}$$: the recipient-auditor cannot derive the spend-side blinding $$r\_s = \text{Poseidon}(\delta\_{\text{spend\\\_r}}, vk\_A, \sigma)$$, which depends on $$vk\_A$$. It 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. The spend-side opening reaches the *sender*-auditor by a different route -- the lane-2 escrow, bounded separately below -- not by this reconstruction. +- **Receiving-side only.** The reconstruction above covers `receiving_commitment`. It does not extend to $$C\_{\text{spend}}$$: the recipient-auditor cannot derive the spend-side blinding $$r\_s = \text{Poseidon}(\delta\_{\text{spend\\\_r}}, vk\_A, \sigma)$$, which depends on $$vk\_A$$. It 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. The spend-side opening reaches the *sender*-auditor by a different route -- the `lane[2]` escrow, bounded separately below -- not by this reconstruction. - **Reset by merge.** 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 reconstruction above restarts from the next inbound flow. -**Sender-auditor opening capability.** The lane-2 escrow hands the sender-auditor the *blinding* of the account's post-operation spendable balance directly, without $$vk\_A$$: together with the value in $$\tilde{b}\_{\text{aud,s}}$$ it is a full Pedersen opening of $$C\_{\text{spend}}'$$. It is available at exactly the three checkpoint operations that escrow lane 2 -- withdrawal (W\_a5), outgoing transfer (T\_a9), and `set_spender` (S\_a6) -- and is likewise bounded: +**Sender-auditor opening capability.** The `lane[2]` escrow hands the sender-auditor the *blinding* of the account's post-operation spendable balance directly, without $$vk\_A$$: together with the value in $$\tilde{b}\_{\text{aud,s}}$$ it is a full Pedersen opening of $$C\_{\text{spend}}'$$. It is available at exactly the three checkpoint operations that escrow `lane[2]` -- withdrawal (W\_a5), outgoing transfer (T\_a9), and `set_spender` (S\_a6) -- and is likewise bounded: - **Forward-only**, on the same grounds as the recipient side. -- **Maintained across merges.** An account binds a single `auditor_id` (Section 6.1), so the key that decrypts the lane-2 escrow is the same key that decrypts the recipient channel of every inbound flow to that account. Merge adds both the values and the blindings ($$v\_{\text{spend}}' = v\_s + v\_r$$, $$r\_{\text{spend}}' = r\_s + r\_r$$, Section 7.4), and the auditor holds each addend: $$(v\_{\text{transfer},i}, r\_{\text{transfer},i})$$ from the recipient-channel reconstruction above, and $$(a\_j, 0)$$ from the public deposits. It therefore carries the escrowed opening forward through every merge by the same addition the contract performs, rather than losing it at one. +- **Maintained across merges.** An account binds a single `auditor_id` (Section 6.1), so the key that decrypts the `lane[2]` escrow is the same key that decrypts the recipient channel of every inbound flow to that account. Merge adds both the values and the blindings ($$v\_{\text{spend}}' = v\_s + v\_r$$, $$r\_{\text{spend}}' = r\_s + r\_r$$, Section 7.4), and the auditor holds each addend: $$(v\_{\text{transfer},i}, r\_{\text{transfer},i})$$ from the recipient-channel reconstruction above, and $$(a\_j, 0)$$ from the public deposits. It therefore carries the escrowed opening forward through every merge by the same addition the contract performs, rather than losing it at one. - **Not renewed by `revoke_spender`.** V\_a3 stays two-lane (DESIGN §7.9), so a revoke rewrites $$C\_{\text{spend}}$$ under a blinding the auditor never receives and leaves it with the post-reclaim *value* alone. The opening is re-acquired at the next checkpoint (W\_a5, T\_a9, S\_a6). - **Rotation-scoped.** A newly activated key cannot decrypt escrows published under the previous one (Section 8.3). An auditor that carries its accumulated opening across the rotation keeps it; one that bootstraps from the new key alone re-acquires it at the next checkpoint. @@ -100,7 +100,7 @@ Each spender transfer produces auditor ciphertexts under two keys (constraints O $$(m\_{v,r}, m\_{r,r}) = \text{SpongeSqueeze}\_2(\delta\_{\text{aud\\\_r}}, s\_{a,r}, \sigma\_a')$$ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,r}} - m\_{v,r}, \qquad r\_{\text{transfer}} = \tilde{r}\_{\text{aud,r}} - m\_{r,r}$$ -The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the lane-2 blinding-escrow slot -- the *new* allowance blinding $$r\_a'$$, the one O11 commits $$C\_a'$$ under (O\_a9): +The owner's auditor decrypts the transfer amount, the post-transfer allowance, and -- in the `lane[2]` blinding-escrow slot -- the *new* allowance blinding $$r\_a'$$, the one O11 commits $$C\_a'$$ under (O\_a9): $$(m\_{v,s}, m\_{a,s}, m\_{r,s}) = \text{SpongeSqueeze}\_3(\delta\_{\text{aud\\\_s}}, s\_{a,s}, \sigma\_a')$$ $$v\_{\text{transfer}} = \tilde{v}\_{\text{aud,s}} - m\_{v,s}, \qquad v\_a' = \tilde{a}\_{\text{aud,s}} - m\_{a,s}, \qquad r\_a' = \tilde{r}\_{\text{aud,s}} - m\_{r,s}$$ @@ -113,7 +113,7 @@ where $$s\_{a,r}$$, $$s\_{a,s}$$, and $$\sigma\_a'$$ are recovered from the even The auditor tracks each allowance's current value through the per-event ciphertexts produced at every state-changing operation: `set_spender` 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_spender` reveals the reclaimed amount (Section 8.2). -**Allowance opening.** The owner's auditor also receives the *blinding* of the allowance commitment each event writes: $$r\_a$$ under $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ at `set_spender` (S14, below) and $$r\_a'$$ in lane 2 at every spender transfer (O\_a9, §8.4). Paired with the value that event already publishes -- $$\tilde{v}\_{\text{aud,s}}$$ on `SetSpender`, $$\tilde{a}\_{\text{aud,s}}$$ on `SpenderTransfer` -- that is the full Pedersen opening of the $$C\_a$$ left on-chain, reconstructed from the event alone with no storage read. Unlike the spendable side there is no merge to fold in, since a delegation's only state transitions are the events themselves. +**Allowance opening.** The owner's auditor also receives the *blinding* of the allowance commitment each event writes: $$r\_a$$ under $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ at `set_spender` (S14, below) and $$r\_a'$$ in `lane[2]` at every spender transfer (O\_a9, §8.4). Paired with the value that event already publishes -- $$\tilde{v}\_{\text{aud,s}}$$ on `SetSpender`, $$\tilde{a}\_{\text{aud,s}}$$ on `SpenderTransfer` -- that is the full Pedersen opening of the $$C\_a$$ left on-chain, reconstructed from the event alone with no storage read. Unlike the spendable side there is no merge to fold in, since a delegation's only state transitions are the events themselves. What is escrowed is one state's blinding, not the generator of every state. $$dvk\_i = \text{Poseidon}(\delta\_{\text{dvk}}, vk, \text{op}\_i)$$ is deterministic and permanent for a $$(owner, spender)$$ pair -- neither the salt nor any generation counter enters it, so `revoke_spender` followed by re-delegation to the same address regenerates it exactly. A single leaked $$dvk\_i$$ ciphertext would therefore hand its holder every allowance value and blinding for that pair, past and future; a leaked $$r\_a$$ ciphertext costs one state. @@ -123,7 +123,7 @@ $$\tilde{r}\_{a,\text{aud,s}} = r\_a + \text{Poseidon}(\delta\_{\text{esc\\\_all reusing the S\_a2 shared scalar $$s\_{a,s} = \text{ECDH}(r\_e, K\_{\text{aud,s}})$$ rather than opening a new ECDH channel, so it costs one Poseidon and no scalar multiplication (DESIGN S14). The auditor recovers $$s\_{a,s}$$ from $$k\_{\text{aud,s}}$$ and the event's $$R\_e$$, and $$\text{op}\_i$$ from the event's `spender` topic, then subtracts. Because the escrowed value is pinned to S6, the opening the auditor holds necessarily matches the $$C\_a$$ the same proof wrote. -A single-output pad rather than a sponge lane, because lane 2 of this channel is already taken by the spendable blinding (S\_a6). What separates it from that channel is tag distinctness: the tag is the first absorbed element, so no pair of remaining inputs makes $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ and $$\delta\_{\text{aud\\\_s}}$$ agree. DESIGN §2.5 *Mode exclusivity* is a separate per-tag obligation that each satisfies independently -- 17 is only ever single-output, 11 only ever multi-lane. $$\text{op}\_i$$ is a per-$$(owner, spender)$$ constant that separates pads across delegations, not a nonce: the pad's freshness rests entirely on $$s\_{a,s}$$, hence on $$r\_e$$, hence on the salt (DESIGN §5.3, §9.6). Re-delegating to the same spender under a reused salt would republish a byte-identical ciphertext. +A single-output pad rather than a sponge lane, because `lane[2]` of this channel is already taken by the spendable blinding (S\_a6). What separates it from that channel is tag distinctness: the tag is the first absorbed element, so no pair of remaining inputs makes $$\delta\_{\text{esc\\\_allow\\\_r\\\_aud}}$$ and $$\delta\_{\text{aud\\\_s}}$$ agree. DESIGN §2.5 *Mode exclusivity* is a separate per-tag obligation that each satisfies independently -- 17 is only ever single-output, 11 only ever multi-lane. $$\text{op}\_i$$ is a per-$$(owner, spender)$$ constant that separates pads across delegations, not a nonce: the pad's freshness rests entirely on $$s\_{a,s}$$, hence on $$r\_e$$, hence on the salt (DESIGN §5.3, §9.6). Re-delegating to the same spender under a reused salt would republish a byte-identical ciphertext. **Key rotation.** Decryption is scoped to events, and only to events. A key can decrypt an allowance ciphertext exactly when it was the active key at the moment that ciphertext was produced. Two consequences, and neither is stronger than that: @@ -241,10 +241,10 @@ pub enum CircuitType { | Circuit | What it proves | |:---|:---| | `Register` | Spending key well-formedness; contract-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 ciphertexts (balance checkpoint + lane-2 escrow of the new spendable blinding); 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 + lane-2 escrow of the new spendable blinding); deterministic randomness for new sender balance; encrypted balance scalar; sender key ownership; range validity (balance $$\in [0, 2^{127})$$, amount $$\in [0, 2^{127})$$) | -| `SpenderTransfer` | 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 + lane-2 escrow of the new allowance blinding); deterministic randomness for new allowance; encrypted allowance scalar; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | -| `SetSpender` | Balance split; $$dvk\_i$$ derivation; ECDH escrow of $$dvk\_i$$ to the spender and of the allowance blinding to the owner's auditor; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint + lane-2 escrow of the new spendable blinding); owner key ownership; contract-bound via $$vk$$ derivation | +| `Withdraw` | Balance sufficiency; new spendable commitment with deterministic randomness; encrypted balance scalar; sender-auditor ECDH ciphertexts (balance checkpoint + `lane[2]` escrow of the new spendable blinding); 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 + `lane[2]` escrow of the new spendable blinding); deterministic randomness for new sender balance; encrypted balance scalar; sender key ownership; range validity (balance $$\in [0, 2^{127})$$, amount $$\in [0, 2^{127})$$) | +| `SpenderTransfer` | 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 + `lane[2]` escrow of the new allowance blinding); deterministic randomness for new allowance; encrypted allowance scalar; spender key ownership; contract-bound indirectly via $$C\_a$$ chain (Section 7.8) | +| `SetSpender` | Balance split; $$dvk\_i$$ derivation; ECDH escrow of $$dvk\_i$$ to the spender and of the allowance blinding to the owner's auditor; allowance commitment with deterministic randomness; encrypted balance and allowance scalars; owner-auditor ECDH ciphertexts (escrow amount + balance checkpoint + `lane[2]` escrow of the new spendable blinding); owner key ownership; contract-bound via $$vk$$ derivation | | `RevokeSpender` | 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; contract-bound via $$vk$$ derivation | ### 10.3 Circuit Cost Analysis @@ -262,7 +262,7 @@ The dominant cost in Noir circuits is elliptic curve scalar multiplication. With | `SetSpender` | 7 | $$Y$$ (S1), $$C\_{\text{spend}}$$ opening (S3), $$C\_a$$ (S7), $$C\_{\text{spend}}'$$ (S10), $$R\_e$$ (S\_a1), $$dvk\_i$$ escrow ECDH (S12, §7.11), owner-auditor ECDH (S\_a2) | | `RevokeSpender` | 6 | $$Y$$ (V1), $$C\_a$$ opening (V4), $$C\_{\text{spend}}$$ opening (V5), $$C\_{\text{spend}}'$$ (V7), $$R\_e$$ (V\_a1), owner-auditor ECDH (V\_a2) | -`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The auditor-side escrow of the allowance blinding $$r\_a$$ (S14) reuses the S\_a2 shared scalar and adds a Poseidon evaluation, not a call. The lane-2 escrows (W\_a5, T\_a9, S\_a6, O\_a9) read a third lane of a permutation each circuit already computes and cost one field addition apiece. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 95, `RevokeSpender` 123, `Transfer` 134, `SetSpender` 135, `SpenderTransfer` 136. +`SetSpender` is the one circuit with a third ECDH beyond the auditor channel: the $$dvk\_i$$ handoff of §7.11 reuses $$r\_e$$ but multiplies it against $$Y\_{\text{op}}$$, so it is a separate call, not a reuse of the S\_a2 shared secret. The auditor-side escrow of the allowance blinding $$r\_a$$ (S14) reuses the S\_a2 shared scalar and adds a Poseidon evaluation, not a call. The `lane[2]` escrows (W\_a5, T\_a9, S\_a6, O\_a9) read `lane[2]` of a permutation each circuit already computes and cost one field addition apiece. The ordering these totals imply is consistent with the committed ACIR opcode counts in `circuits/constraints.baseline`: `Register` 33, `Withdraw` 95, `RevokeSpender` 123, `Transfer` 134, `SetSpender` 135, `SpenderTransfer` 136. 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). @@ -549,6 +549,6 @@ Each $$\delta$$ is a small positive integer in $$\mathbb{F}\_r$$, fixed for the This table assigns all seventeen values; no other document assigns them. Tags 14–16 are never absorbed inside a core circuit — 14 is derived off-circuit (DESIGN.md §5.3 makes its derivation normative for every operation whose originator holds a viewing key), and 15–16 belong to the off-chain selective-disclosure layer ([SELECTIVE_DISCLOSURE.md](./SELECTIVE_DISCLOSURE.md) §2.2) — so they are not part of the on-chain wire contract; `circuits/lib/src/lib.nr` accordingly implements 1–13 and 17. All seventeen values MUST still be distinct and each MUST be confined to a single sponge mode, so a deployment treats them as one namespace. Tag 17 is assigned out of sequence with its neighbours because it was added after 14–16. It takes its own tag rather than reusing $$\delta\_{\text{esc\\\_dvk}}$$ because the two escrows key off different shared scalars ($$Y\_{\text{op}}$$ vs $$K\_{\text{aud,s}}$$), which is the distinct-domain leg of §5.3's *Why reusing $$r\_e$$ is safe*. It shares its shared scalar with $$\delta\_{\text{aud\\\_s}}$$ instead, and what separates the two is simply that they are different tags: the tag is the first absorbed element, so no pair of remaining inputs makes tag 17 and tag 11 agree. Mode exclusivity is a separate per-tag rule that each satisfies on its own — 17 is only ever single-output, 11 only ever multi-lane (§8.5). -**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share their first lane with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag ever squeezed three-wide, and the widths it is read at agree on their shared lanes (§2.5 *Mode exclusivity*). 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. +**Provenance.** Sequential small integers are the simplest assignment that satisfies the requirement of *distinctness* across all Poseidon2 invocations in this protocol -- §3.2 models Poseidon2 as a pseudorandom function, so evaluations whose leading input differs are computationally independent. Distinctness alone is not sufficient: each tag must also be confined to a single sponge mode, since the multi-lane forms of §2.5 share `lane[0]` with the single-output form on the same inputs; $$\delta\_{\text{aud\\\_s}}$$ is the one tag ever squeezed three-wide, and the widths it is read at agree on their shared lanes (§2.5 *Mode exclusivity*). 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. **Cross-protocol collision.** Future protocols that share Grumpkin / BN254 / Poseidon2 with this protocol -- 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/v1:X"}))$$, but this is a deployment-time choice that must be applied uniformly and disclosed in the deployment's circuit-binding documentation. diff --git a/packages/tokens/src/confidential/docs/SDK.md b/packages/tokens/src/confidential/docs/SDK.md index d5eba3b6c..2ee6fb501 100644 --- a/packages/tokens/src/confidential/docs/SDK.md +++ b/packages/tokens/src/confidential/docs/SDK.md @@ -95,13 +95,13 @@ A value is a **canonical** $$\mathbb{F}_r$$ representative iff it is a 32-byte b The sponge construction, its width and rate, the IV placement, the padding rule, and the two- and three-lane forms $$\text{SpongeSqueeze}_2$$ and $$\text{SpongeSqueeze}_3$$ are specified normatively in DESIGN.md §2.5. What follows is what that construction additionally requires of a client. -**Three self-checks are available before any proof is generated.** The absorbed length in both squeeze forms is always 3, so the IV is fixed at $$3 \cdot 2^{64}$$; the first lane is identical to $$\text{poseidon\\\_with\\\_domain}(\delta, [s, \sigma])$$ on the same inputs; and $$\text{SpongeSqueeze}_3(\delta, s, \sigma)[0..1] = \text{SpongeSqueeze}_2(\delta, s, \sigma)$$, since the absorb fits one rate-3 block and both forms read the same permutation. An implementation that reproduces all three has the block layout and the IV lane right. The third is pinned by `circuits/lib/testdata/sponge_squeeze_3.json`. +**Three self-checks are available before any proof is generated.** The absorbed length in both squeeze forms is always 3, so the IV is fixed at $$3 \cdot 2^{64}$$; `lane[0]` is identical to $$\text{poseidon\\\_with\\\_domain}(\delta, [s, \sigma])$$ on the same inputs; and $$\text{SpongeSqueeze}_3(\delta, s, \sigma)[i] = \text{SpongeSqueeze}_2(\delta, s, \sigma)[i]$$ for $$i \in \\{0, 1\\}$$, since the absorb fits one rate-3 block and both forms read the same permutation. An implementation that reproduces all three has the block layout and the IV lane right. The third is pinned by `circuits/lib/testdata/sponge_squeeze_3.json`. **The domain-tagged funnel.** Every Poseidon2 invocation in the protocol routes through one entry point that places the domain tag as the **first absorbed element**: $$\text{poseidon\\\_with\\\_domain}(\delta, [x_1, \ldots, x_n]) = \text{sponge}([\delta, x_1, \ldots, x_n])$$ -Squeeze-slot assignment is canonical and MUST be followed: lane 0 is always an amount mask, lane 1 is always a balance, allowance, or per-transfer-randomness mask, and lane 2 is always the sender-auditor blinding-escrow slot — the new spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`, the new allowance blinding $$r_a'$$ on `SpenderTransfer` (DESIGN.md §2.5). Lane 2 never carries a key. Only the sender-auditor channel ($$\delta_{\text{aud\\\_s}}$$) is squeezed three-wide; the recipient channel ($$\delta_{\text{aud\\\_r}}$$) stays at two lanes. `Withdraw`, whose amount is public, takes lanes **1** and **2** and leaves lane 0 unused (DESIGN.md W_a3–W_a5), so a checkpoint pad can never coincide with an amount pad. +Squeeze-slot assignment is canonical and MUST be followed: `lane[0]` is always an amount mask, `lane[1]` is always a balance, allowance, or per-transfer-randomness mask, and `lane[2]` is always the sender-auditor blinding-escrow slot — the new spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`, the new allowance blinding $$r_a'$$ on `SpenderTransfer` (DESIGN.md §2.5). `lane[2]` never carries a key. Only the sender-auditor channel ($$\delta_{\text{aud\\\_s}}$$) is squeezed three-wide; the recipient channel ($$\delta_{\text{aud\\\_r}}$$) stays at two lanes. `Withdraw`, whose amount is public, takes `lane[1]` and `lane[2]` and leaves `lane[0]` unused (DESIGN.md W_a3–W_a5), so a checkpoint pad can never coincide with an amount pad. ### 4.4 Generators and commitments @@ -161,7 +161,7 @@ Secret scalars — $$\sigma$$, $$\sigma_a$$ — MUST be produced by the rejectio DESIGN_cont.md §13 assigns all seventeen values and is their only source; the right-hand column is this document's addition. $$\delta_{\text{disc\\\_bind}}$$ and $$\delta_{\text{disc}}$$ belong to the off-chain disclosure layer (SELECTIVE_DISCLOSURE.md §2.2). Tag 1 is absorbed by the contract rather than by a circuit — the contract derives $$\text{addr\\\_f}$$ and $$\text{op}_i$$ on-chain and the circuits receive them as opaque public inputs (DESIGN.md §2.7 *Usage sites*) — so it is part of the on-chain wire contract all the same. None of 14–16 is absorbed either in a circuit or on-chain, so none is part of the on-chain wire contract, but all three are part of the cross-client contract because two wallets serving the same account must agree on them (§6.3). -All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the multi-lane tags — 11 read three-wide wherever lane 2 is escrowed and two-wide on `RevokeSpender`, 12 always two-wide; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows the allowance blinding $$r_a$$ to the owner's auditor under a single-output pad rather than over lane 2, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_allow_r_auditor.json`. +All seventeen values MUST be distinct, and each MUST be used in exactly one sponge mode, per DESIGN.md §2.5 *Mode exclusivity*. Tags 11 and 12 are the multi-lane tags — 11 read three-wide wherever `lane[2]` is escrowed and two-wide on `RevokeSpender`, 12 always two-wide; the remaining fifteen, including 1, 14–16, and 17, are single-output tags. Tag 17 is absorbed only by the `SetSpender` circuit (DESIGN.md S14), which escrows the allowance blinding $$r_a$$ to the owner's auditor under a single-output pad rather than over `lane[2]`, that lane being taken by the spendable blinding; its fixture is `circuits/lib/testdata/encrypt_esc_allow_r_auditor.json`. ### 4.9 Address compression @@ -463,22 +463,22 @@ A spender MUST NOT be able to reach the owner's spendable balance through any in ## 11. Auditor Client -An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. The allowance opening comes straight out of the event: the blinding of the $$C_a$$ that operation writes is escrowed in the event itself -- tag 17 on `SetSpender`, lane 2 on `SpenderTransfer` -- and the matching value is in the sender-channel ciphertext (DESIGN_cont.md §8.5). An auditor that did not observe the event holds no opening for that state and cannot derive one; there is no key from which the openings follow. For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. +An auditor decrypts from the public event and its own secret $$k$$ alone, with no viewing key, holder cooperation, or extra on-chain read. The allowance opening comes straight out of the event: the blinding of the $$C_a$$ that operation writes is escrowed in the event itself -- tag 17 on `SetSpender`, `lane[2]` on `SpenderTransfer` -- and the matching value is in the sender-channel ciphertext (DESIGN_cont.md §8.5). An auditor that did not observe the event holds no opening for that state and cannot derive one; there is no key from which the openings follow. For each channel it computes the shared scalar against the event's ephemeral point, derives that channel's lane masks (§4.3) — three on the sender / owner channel, two on the recipient channel — and subtracts. The two channels differ in what they yield (DESIGN_cont.md §8.1): -| Channel | Lane 0 | Lane 1 | Lane 2 | +| Channel | `lane[0]` | `lane[1]` | `lane[2]` | |:--|:--|:--|:--| | Sender / owner ($$\delta_{\text{aud\\\_s}}$$) | Transfer amount, or the escrowed amount for `SetSpender` and the reclaimed amount for `RevokeSpender` | Sender's post-operation balance, or post-operation allowance for a spender transfer | Post-operation spendable blinding on `Withdraw`, `Transfer`, and `SetSpender`; post-transfer allowance blinding $$r_a'$$ on `SpenderTransfer`; nothing on `RevokeSpender`, which stays two-lane | | Recipient ($$\delta_{\text{aud\\\_r}}$$) | Transfer amount | Per-transfer Pedersen randomness $$r_{\text{transfer}}$$ | — (channel is two-lane) | -`Withdraw`, `SetSpender`, and `RevokeSpender` carry a sender-channel balance checkpoint whose pad is lane **1**. Only `Withdraw` leaves lane 0 unused, its amount being public (DESIGN.md W_a3, §4.3); `SetSpender` and `RevokeSpender` read lane 0 as well, for the escrowed and reclaimed amounts respectively (DESIGN.md S_a4, V_a4). +`Withdraw`, `SetSpender`, and `RevokeSpender` carry a sender-channel balance checkpoint whose pad is `lane[1]`. Only `Withdraw` leaves `lane[0]` unused, its amount being public (DESIGN.md W_a3, §4.3); `SetSpender` and `RevokeSpender` read `lane[0]` as well, for the escrowed and reclaimed amounts respectively (DESIGN.md S_a4, V_a4). -An implementation MUST squeeze the sender / owner channel three-wide and MUST NOT widen the recipient channel. Reading lane 2 on `RevokeSpender` yields a pad over no ciphertext (V_a3 is two-lane, DESIGN.md §7.9); an implementation MUST treat a `RevokeSpender` event as carrying no escrowed blinding rather than substituting a stale one. Because the first two lanes of $$\text{SpongeSqueeze}_3$$ coincide with $$\text{SpongeSqueeze}_2$$ (§4.3), a client that already reads lanes 0 and 1 keeps every value it decrypted before. +An implementation MUST squeeze the sender / owner channel three-wide and MUST NOT widen the recipient channel. Reading `lane[2]` on `RevokeSpender` yields a pad over no ciphertext (V_a3 is two-lane, DESIGN.md §7.9); an implementation MUST treat a `RevokeSpender` event as carrying no escrowed blinding rather than substituting a stale one. Because the first two lanes of $$\text{SpongeSqueeze}_3$$ coincide with $$\text{SpongeSqueeze}_2$$ (§4.3), a client that already reads `lane[0]` and `lane[1]` keeps every value it decrypted before. **Cross-channel agreement.** Where an auditor holds the key for both parties, the amount decrypts independently on each channel and the circuit constrains both to the same value, so the two MUST agree. An implementation SHOULD perform this comparison and treat disagreement as evidence that $$k$$ is not the auditor key for both parties of that event. -**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). The lane-2 opening of the sender channel is forward-only and **standing**: it opens the spendable commitment as of the checkpoint that escrowed it and stays valid through merges, because one account key serves both channels (DESIGN_cont.md §8.1 *Sender-auditor opening capability*). Maintaining it is the client's job, not the protocol's: an implementation MUST add the lane-0 amount and lane-1 $$r_{\text{transfer}}$$ of every inbound `Transfer` and `SpenderTransfer` to its stored $$(v, r)$$, and MUST treat each `Deposit` as $$(\text{amount}, 0)$$ (DESIGN.md §7.3). It MUST invalidate the stored opening on a `RevokeSpender` event, which rewrites the spendable commitment under a blinding no channel carries, until the next lane-2 escrow. Across a key rotation an implementation MUST either carry the accumulated opening forward from the prior key or treat the account as unopened until the next checkpoint under the new key; the value side needs no replay either way, since the next owner-initiated proof publishes a fresh balance checkpoint. +**Scope MUST be represented, not implied.** The recipient-channel capability is forward-only, receiving-side only, and reset by merge (DESIGN_cont.md §8.1). The `lane[2]` opening of the sender channel is forward-only and **standing**: it opens the spendable commitment as of the checkpoint that escrowed it and stays valid through merges, because one account key serves both channels (DESIGN_cont.md §8.1 *Sender-auditor opening capability*). Maintaining it is the client's job, not the protocol's: an implementation MUST add the `lane[0]` amount and `lane[1]` $$r_{\text{transfer}}$$ of every inbound `Transfer` and `SpenderTransfer` to its stored $$(v, r)$$, and MUST treat each `Deposit` as $$(\text{amount}, 0)$$ (DESIGN.md §7.3). It MUST invalidate the stored opening on a `RevokeSpender` event, which rewrites the spendable commitment under a blinding no channel carries, until the next `lane[2]` escrow. Across a key rotation an implementation MUST either carry the accumulated opening forward from the prior key or treat the account as unopened until the next checkpoint under the new key; the value side needs no replay either way, since the next owner-initiated proof publishes a fresh balance checkpoint. An auditor facade MUST NOT be able to construct a spending witness. It *can*, however, open the spendable balance past a merge -- the inbound $$r_{\text{transfer}}$$ reaches the same key on the recipient channel -- so a facade that exposes openings exposes them for the account's whole history under the active key, not for isolated events. diff --git a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md index 857d55dac..6936258ca 100644 --- a/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md +++ b/packages/tokens/src/confidential/docs/SELECTIVE_DISCLOSURE.md @@ -74,7 +74,7 @@ This document reuses the notation, key hierarchy, and commitment scheme from DES - $$sk\_A$$, $$vk\_A$$, $$\text{PVK}\_A$$: an account's spending key, viewing key, and public viewing key (DESIGN.md §4). - $$\text{addr\\\_f}$$: the contract's compressed address Field $$\text{address\\\_to\\\_field}(\text{contract})$$, bound into $$vk$$ derivation (DESIGN.md §2.7, §4.2). Stored once at construction in the contract's instance storage (DESIGN.md §3.5). - $$K\_{\text{aud,s}}$$, $$K\_{\text{aud,r}}$$, $$aud\_{sk}$$: the sender-side and recipient-side auditor Grumpkin public keys, and an auditor's secret key (DESIGN_cont.md §8.1, §8.3). Each account selects an `auditor_id` at registration; the same `auditor_id` may resolve to either role depending on the transfer's direction. -- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a'$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, with lane 2's $$\tilde{r}\_{\text{aud,s}}$$ carrying the post-transfer allowance blinding $$r\_a'$$ rather than a spendable one (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a'$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. +- $$(R\_e, \sigma, \tilde{v}, \tilde{b}, \tilde{v}\_{\text{aud,r}}, \tilde{r}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}, \tilde{b}\_{\text{aud,s}}, \tilde{r}\_{\text{aud,s}})$$: per-transfer event fields (DESIGN.md §7.6, §11.2). For `SpenderTransfer` events the recipient/auditor ECDH nonce is $$\sigma\_a'$$ in place of $$\sigma$$, and the sender-auditor channel emits $$\tilde{a}\_{\text{aud,s}}$$ in place of $$\tilde{b}\_{\text{aud,s}}$$, with `lane[2]`'s $$\tilde{r}\_{\text{aud,s}}$$ carrying the post-transfer allowance blinding $$r\_a'$$ rather than a spendable one (DESIGN.md §7.8, §11.2). Throughout this document, the symbol $$\sigma\_E$$ refers to the **event ECDH nonce**, equal to $$\sigma$$ for `Transfer` events and to $$\sigma\_a'$$ for `SpenderTransfer` events; one circuit handles both families, parameterized by which nonce the disclosing event emitted. - $$H$$: the Grumpkin Pedersen generator used uniformly for key derivation and ECDH (DESIGN.md §2.3, §2.4). ### 2.1 Disclosure Recipient @@ -97,7 +97,7 @@ The disclosure layer inherits the protocol's threat model (DESIGN.md §3.2) and **Holder is the prover for D-recipient and D-sender variants.** The holder is trusted only to produce *correct* proofs about events they choose to disclose. The holder is *not* trusted to be complete: they may withhold events. Recipients that require completeness must obtain it from the auditor (DESIGN_cont.md §8) or from out-of-band evidence. -**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen opening of every $$C\_a$$ (DESIGN_cont.md §8.5) and, at each checkpoint event that escrows lane 2, of the account's $$C\_{\text{spend}}$$ as of that event (DESIGN_cont.md §8.1 *Sender-auditor opening capability*, §8.2). The latter is standing rather than event-scoped: the account's single auditor key also decrypts every inbound blinding, so the opening carries across merges, and only `revoke_spender` -- which escrows no blinding -- suspends it until the next checkpoint. The D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. +**Auditor is the prover for D-auditor variants.** The auditor is trusted to disclose accurately when asked. The auditor's existing trust scope (DESIGN.md §3.3) is not enlarged. That scope already includes the full Pedersen opening of every $$C\_a$$ (DESIGN_cont.md §8.5) and, at each checkpoint event that escrows `lane[2]`, of the account's $$C\_{\text{spend}}$$ as of that event (DESIGN_cont.md §8.1 *Sender-auditor opening capability*, §8.2). The latter is standing rather than event-scoped: the account's single auditor key also decrypts every inbound blinding, so the opening carries across merges, and only `revoke_spender` -- which escrows no blinding -- suspends it until the next checkpoint. The D-auditor variants expose to a disclosure recipient only what the chosen variant states, never the openings themselves. **Disclosure recipient is honest-but-curious.** The recipient correctly verifies proofs and decrypts ciphertexts addressed to their key. The recipient may attempt to replay or rebroadcast proofs; nonce binding prevents reuse against other parties. @@ -309,7 +309,7 @@ The auditor proves to a third party that an on-chain event corresponds to a tran **Which auditor.** Every transfer carries ciphertexts under *two* auditor keys (DESIGN_cont.md §8.1): the recipient-side key $$K\_{\text{aud,r}}$$ (channel $$\delta\_{\text{aud\\\_r}}$$, two lanes yielding masks for $$v\_{\text{transfer}}$$ and $$r\_{\text{transfer}}$$) and the sender-side key $$K\_{\text{aud,s}}$$ (channel $$\delta\_{\text{aud\\\_s}}$$, three lanes yielding masks for $$v\_{\text{transfer}}$$, the sender's post-transfer balance, and the blinding-escrow slot of DESIGN.md §2.5). Whichever auditor is disclosing reuses the same shared-secret derivation they perform to read events natively; the circuit additionally encrypts the result to the disclosure recipient. -The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{\delta\_{\text{aud\\\_r}}, \delta\_{\text{aud\\\_s}}\\}$$ and the corresponding event ciphertext as $$\tilde{v}\_{\text{aud}} \in \\{\tilde{v}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}\\}$$. In each case the amount mask is lane 0 of the channel's sponge; the remaining lanes ($$m\_{r,r}$$ on the recipient channel; $$m\_{b,s}$$ and $$m\_{r,s}$$ on the sender channel) are computed and discarded for an amount disclosure, or used in place of lane 0 for the balance/randomness variants noted below. A3 is written with $$\text{SpongeSqueeze}\_2$$ because its first two lanes coincide with those of $$\text{SpongeSqueeze}\_3$$ on either channel (DESIGN.md §2.5); only a variant reading lane 2 squeezes three-wide. +The constraints below parameterize the channel as $$\delta\_{\text{aud}} \in \\{\delta\_{\text{aud\\\_r}}, \delta\_{\text{aud\\\_s}}\\}$$ and the corresponding event ciphertext as $$\tilde{v}\_{\text{aud}} \in \\{\tilde{v}\_{\text{aud,r}}, \tilde{v}\_{\text{aud,s}}\\}$$. In each case the amount mask is `lane[0]` of the channel's sponge; the remaining lanes ($$m\_{r,r}$$ on the recipient channel; $$m\_{b,s}$$ and $$m\_{r,s}$$ on the sender channel) are computed and discarded for an amount disclosure, or used in place of `lane[0]` for the balance/randomness variants noted below. A3 is written with $$\text{SpongeSqueeze}\_2$$ because its first two lanes coincide with those of $$\text{SpongeSqueeze}\_3$$ on either channel (DESIGN.md §2.5); only a variant reading `lane[2]` squeezes three-wide. **Public inputs** @@ -337,7 +337,7 @@ D-auditor does not bind to an account record; the auditor key already binds the **Verifier flow.** Follow §5.3 with `circuit_id = D-auditor` (or the chosen balance / randomness variant). Step 2 is skipped — no $$\text{PVK}\_A$$ lookup is needed. Step 3 resolves $$K\_{\text{aud}}$$ at the event's ledger: $$K\_{\text{aud,r}}$$ from the `auditor_id` on the event's `to` account when disclosing the recipient-side channel, or $$K\_{\text{aud,s}}$$ from the `auditor_id` on the `from` account when disclosing the sender-side channel. `from` is the funds' owner in both `Transfer` and `SpenderTransfer`, since the sender-auditor channel always tracks the owner (DESIGN.md §7.8). -**Balance / randomness variants.** Lane 1 of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on lane 2 of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries the post-transfer allowance blinding $$r\_a'$$ (DESIGN.md §7.8 O\_a9), so the same variant discloses the opening of $$C\_a'$$ rather than of $$C\_{\text{spend}}'$$ (DESIGN_cont.md §8.5). These variants are not separately tabulated. +**Balance / randomness variants.** `lane[1]` of each channel carries a distinct datum: $$m\_{b,s}$$ (sender's post-transfer balance checkpoint, channel $$\delta\_{\text{aud\\\_s}}$$, recovered from $$\tilde{b}\_{\text{aud,s}}$$) or $$m\_{r,r}$$ (per-transfer Pedersen randomness, channel $$\delta\_{\text{aud\\\_r}}$$, recovered from $$\tilde{r}\_{\text{aud,r}}$$). A circuit that discloses either of these substitutes the corresponding event ciphertext for $$\tilde{v}\_{\text{aud}}$$ in A4 and reads $$m\_2$$ rather than $$m\_v$$ from the sponge output. Range constraint D5 applies unchanged to a balance disclosure; for a randomness disclosure D5 is dropped since $$r\_{\text{transfer}} \in \mathbb{F}\_r$$ is not range-bounded. The balance variant has a blinding sibling on `lane[2]` of the sender channel: $$m\_{r,s}$$ recovers the sender's post-transfer spendable blinding from $$\tilde{r}\_{\text{aud,s}}$$ (DESIGN.md §7.6 T\_a9), so an auditor can disclose the full opening of the sender's $$C\_{\text{spend}}$$ *as of that transfer* rather than its value alone (DESIGN_cont.md §8.1); A3 then squeezes three-wide and D5 is dropped as for the randomness variant. On a `SpenderTransfer` the same lane carries the post-transfer allowance blinding $$r\_a'$$ (DESIGN.md §7.8 O\_a9), so the same variant discloses the opening of $$C\_a'$$ rather than of $$C\_{\text{spend}}'$$ (DESIGN_cont.md §8.5). These variants are not separately tabulated. ---