diff --git a/packages/tokens/src/confidential/CLAUDE.md b/packages/tokens/src/confidential/CLAUDE.md index f31e0d6d9..5b3b3db38 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,17 +59,17 @@ 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 | +| 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 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 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 - **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 74060ec76..a1363bb9d 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 @@ -34,9 +34,13 @@ 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 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, 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. + +`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`. @@ -56,7 +60,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 +80,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..2b9240534 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_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. 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 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 + /// 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,24 @@ 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 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` + /// 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 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 ################## @@ -304,6 +338,29 @@ 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 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 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 +/// 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`: `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 ################## /// Poseidon2 sponge with two squeezes. Absorbs `(d, s, sigma)` -- where @@ -316,27 +373,43 @@ pub fn encrypt_esc_dvk(dvk: Field, 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]] } -/// 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. 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). /// -/// `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). -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] +/// `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. +/// +/// 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); + [state[0], state[1], state[2]] } diff --git a/packages/tokens/src/confidential/circuits/lib/src/tests.nr b/packages/tokens/src/confidential/circuits/lib/src/tests.nr index 91f12440a..1be685b2c 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, + dvk_from_vk_op, ecdh, encrypt_allowance, encrypt_amount, + 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; use std::hash::{derive_generators, pedersen_commitment, poseidon2_permutation}; @@ -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] @@ -239,6 +233,55 @@ 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 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. + 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() { + // 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. + 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_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(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_ALLOWANCE_BLINDING_AUDITOR, [s, op_i]); + assert(encrypt_esc_allow_r_auditor(x, s, op_i) - mask == x); +} + #[test] fn sponge_squeeze_2_outputs_distinct() { // The two squeezed masks come from different rate positions of the same @@ -346,10 +389,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]; @@ -364,6 +403,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_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]); println(f"poseidon_with_domain_VK_2 = {pwd}"); @@ -449,21 +501,32 @@ 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, - ); + // 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 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); + 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_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. assert(poseidon_with_domain(domain::VIEWING_KEY, [sk, addr_f]) == vk); } @@ -479,6 +542,10 @@ global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_0: Field = 0x0801e6d7184cea11e6225fcfb4e88a90674cb085da8ebc04918c57cc44b9c25d; global SPONGE_SQUEEZE_2_AUDITOR_RECIPIENT_1: Field = 0x131187e2ac296c1c54c2be2a5cbe614644f01c754108ce25359212d67b36ecc3; +// 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; #[test] fn derive_helpers_deterministic_and_distinct() { 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/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/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/lib/testdata/sponge_squeeze_3.json b/packages/tokens/src/confidential/circuits/lib/testdata/sponge_squeeze_3.json new file mode 100644 index 000000000..1b8c773dc --- /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 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" }, + "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..8ec7148bb 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_allow_r_auditor, + encrypt_esc_dvk, scalar_mul, sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -59,6 +59,34 @@ mod tests; // + Poseidon2(delta_esc_dvk, s_esc, op_i) // Masked dvk_i for the // spender. +// S14 r_a_tilde_aud_s = r_a +// + Poseidon2(delta_esc_allow_r_aud, s_{a,s}, op_i) +// Auditor-side escrow +// of the allowance +// blinding S6 derived +// and S7 committed to. +// With v_tilde_aud_s +// (S_a4) this hands the +// owner's auditor the +// 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 +// lane, because lane[2] +// of this channel is +// taken by S_a6. Costs +// one Poseidon and zero +// scalar muls; r_a is +// already a witness +// (S6). +// 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 +101,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 +129,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 +173,18 @@ 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 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 // ----------------- @@ -180,6 +225,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, + 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}, @@ -263,12 +310,23 @@ 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 blinding-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. 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 082a71041..2596c0919 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_allow_r_auditor, encrypt_esc_dvk, H, scalar_mul, + sponge_squeeze_3, vk_from_sk, }; use std::embedded_curve_ops::EmbeddedCurvePoint; @@ -83,6 +84,13 @@ 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- +// 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 R_A_TILDE_AUD_S: Field = + 0x28991b7df68413693ffe3fd0d47b0793e65f3c6edb6907a7a152fe80d158927f; global B_TILDE_AUD_S: Field = 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c07c; @@ -109,9 +117,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 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; @@ -138,6 +148,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"R_A_TILDE_AUD_S = {r_a_tilde_aud_s}"); } #[test] @@ -172,13 +184,15 @@ 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); 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,9 +201,11 @@ 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_allow_r_auditor(r_a, s_a_s, OP_I) == R_A_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 @@ -224,6 +240,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, + r_a_tilde_aud_s_in: Field, ) { main( sk_in, @@ -255,6 +273,8 @@ fn run_main( r_e_y_in, v_tilde_aud_s_in, b_tilde_aud_s_in, + r_tilde_aud_s_in, + r_a_tilde_aud_s_in, ); } @@ -289,6 +309,8 @@ fn run_fixture() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -316,7 +338,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 +369,8 @@ fn full_allowance_escrow() { R_E_Y, V + m_s[0], 0 + m_s[1], + r_new + m_s[2], + encrypt_esc_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -369,7 +393,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 +424,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_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -421,7 +447,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 +478,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_allow_r_auditor(derive_allow_r(dvk_from_vk_op(vk, OP_I), SIGMA_A), s_a_s, OP_I), ); } @@ -478,7 +506,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 +537,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_allow_r_auditor(r_a, s_a_s, OP_I), ); } @@ -561,6 +591,8 @@ fn rejects_wrong_sk() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -578,7 +610,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 +641,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_allow_r_auditor(derive_allow_r(dvk_from_vk_op(vk, OP_I), SIGMA_A), s_a_s, OP_I), ); } @@ -659,6 +693,8 @@ fn rejects_wrong_addr_f() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -700,6 +736,8 @@ fn rejects_wrong_op_i() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -736,6 +774,8 @@ fn rejects_tampered_c_a() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -773,6 +813,8 @@ fn rejects_tampered_a_tilde() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -809,6 +851,8 @@ fn rejects_tampered_c_spend_new() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -845,6 +889,8 @@ fn rejects_tampered_b_tilde() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -884,6 +930,8 @@ fn rejects_tampered_escrowed_dvk_r_x() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -921,6 +969,8 @@ fn rejects_tampered_escrowed_dvk_cipher() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -962,6 +1012,8 @@ fn rejects_wrong_y_op() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -983,7 +1035,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 +1067,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_allow_r_auditor(derive_allow_r(dvk, SIGMA_A), s_a_s_zero, OP_I), ); } @@ -1035,7 +1090,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 +1122,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_allow_r_auditor(derive_allow_r(dvk, SIGMA_A), s_a_s_bad, OP_I), ); } @@ -1103,6 +1161,8 @@ fn rejects_off_curve_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -1140,6 +1200,8 @@ fn rejects_identity_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -1177,6 +1239,8 @@ fn rejects_wrong_k_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -1213,6 +1277,8 @@ fn rejects_tampered_v_tilde_aud_s() { R_E_Y, V_TILDE_AUD_S + 1, B_TILDE_AUD_S, + R_TILDE_AUD_S, + R_A_TILDE_AUD_S, ); } @@ -1249,5 +1315,247 @@ fn rejects_tampered_b_tilde_aud_s() { R_E_Y, V_TILDE_AUD_S, B_TILDE_AUD_S + 1, + R_TILDE_AUD_S, + 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_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, + 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, + encrypt_esc_allow_r_auditor(derive_allow_r(dvk, SIGMA_A + 1), s_a_s, OP_I), + ); +} + +#[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, + 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 d419818a9..a5cb8187a 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; @@ -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. @@ -86,16 +93,49 @@ mod tests; // OWNER, not the // spender (Section // 8.4). -// O_a6 (m_v_s, m_a_s) -// = SpongeSqueeze_2(delta_aud_s, -// s_{a,s}, sigma_a) Owner-channel -// sponge: two masks. +// O_a6 (m_v_s, m_a_s, m_r_s) +// = SpongeSqueeze_3(delta_aud_s, +// 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. // O_a8 a_tilde_aud_s = (v_a - v_transfer) + m_a_s Owner-auditor // encrypted // post-transfer // allowance. +// 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. 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 +// 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) // --------------------------------------------- @@ -108,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) // ---------------------------------------- @@ -132,7 +176,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 // --- ----- ------ ------------------------------- @@ -165,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; @@ -177,6 +222,9 @@ mod tests; // 23 a_tilde_aud_s a_tilde_aud_s Prover-supplied owner-auditor // encrypted post-transfer // allowance; 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 // ----------------- @@ -220,6 +268,7 @@ fn main( r_tilde_aud_r: pub Field, v_tilde_aud_s: pub Field, a_tilde_aud_s: 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. @@ -261,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); @@ -269,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 @@ -297,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); @@ -308,12 +357,19 @@ 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 + // blinding-escrow slot) + 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); // O_a8 assert(v_a_new + m_s[1] == a_tilde_aud_s); + + // 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 6337933ab..efcda52cd 100644 --- a/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr +++ b/packages/tokens/src/confidential/circuits/spender_transfer/src/tests.nr @@ -1,17 +1,18 @@ 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; // 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,21 +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. Pad and plaintext both key off SIGMA_A_NEW. +global R_TILDE_AUD_S: Field = + 0x19b2c06c2ec9cacaa9f36876d6b988d186ad82b2f47cff17dbe6e4632d79b4d7; global A_TILDE_AUD_S: Field = - 0x288ff48898f218dbba2a5c9546d0986af3257a4a539e23e4f35c404d6720c144; + 0x29499e6f15cad25108f6b3931d8f3cd70cd0c0000ca03921509c5466feb773bb; #[test] fn print_fixtures() { @@ -89,23 +95,24 @@ 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_2(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]; let yopx = y_op.x; let yopy = y_op.y; @@ -146,6 +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"R_TILDE_AUD_S = {r_tilde_aud_s}"); } #[test] @@ -155,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); @@ -184,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_2(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); @@ -200,6 +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(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. @@ -238,6 +247,7 @@ fn run_main( r_tilde_aud_r_in: Field, v_tilde_aud_s_in: Field, a_tilde_aud_s_in: Field, + r_tilde_aud_s_in: Field, ) { main( sk_op_in, @@ -270,6 +280,7 @@ fn run_main( r_tilde_aud_r_in, v_tilde_aud_s_in, a_tilde_aud_s_in, + r_tilde_aud_s_in, ); } @@ -305,6 +316,7 @@ fn run_fixture() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -316,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; @@ -332,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_2(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, @@ -375,6 +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], + r_a_new_2 + m_s[2], ); } @@ -390,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_2(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, @@ -432,6 +445,7 @@ fn full_allowance_transfer() { r_transfer + m_r[1], V_A + m_s[0], 0 + m_s[1], + r_a_new + m_s[2], ); } @@ -469,6 +483,7 @@ fn rejects_wrong_sk_op() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -542,6 +557,7 @@ fn rejects_wrong_dvk_against_contract() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -557,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_2(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, @@ -590,6 +606,7 @@ fn rejects_wrong_allowance_opening() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_bad + m_s[1], + r_a_new + m_s[2], ); } @@ -606,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_2(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, @@ -648,6 +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], + r_a_new + m_s[2], ); } @@ -669,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_2(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, @@ -702,6 +720,7 @@ fn rejects_v_a_out_of_range() { R_TILDE_AUD_R, V_TILDE_AUD_S, v_a_new_huge + m_s[1], + r_a_new + m_s[2], ); } @@ -718,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_2(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, @@ -760,6 +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], + r_a_new + m_s[2], ); } @@ -800,6 +820,7 @@ fn rejects_wrong_recipient_pvk() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -808,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( @@ -847,6 +868,7 @@ fn rejects_poisoned_c_transfer() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -884,6 +906,7 @@ fn rejects_tampered_v_tilde() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -929,6 +952,7 @@ fn rejects_tampered_a_tilde_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -966,22 +990,36 @@ fn rejects_tampered_c_a_new() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } #[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, @@ -1002,17 +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, + 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], ); } @@ -1022,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 @@ -1033,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_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_NEW); run_main( SK_OP, @@ -1072,6 +1111,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], + derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s_zero[2], ); } @@ -1089,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_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_NEW); run_main( SK_OP, @@ -1128,6 +1168,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], + derive_allow_r(DVK_I, SIGMA_A_NEW) + m_s_bad[2], ); } @@ -1166,6 +1207,7 @@ fn rejects_off_curve_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1204,6 +1246,7 @@ fn rejects_identity_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1240,6 +1283,7 @@ fn rejects_off_curve_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1276,6 +1320,7 @@ fn rejects_identity_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1313,6 +1358,7 @@ fn rejects_tampered_v_tilde_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1350,6 +1396,7 @@ fn rejects_tampered_r_tilde_aud_r() { R_TILDE_AUD_R + 1, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1387,6 +1434,7 @@ fn rejects_tampered_v_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S + 1, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1424,6 +1472,199 @@ fn rejects_tampered_a_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S + 1, + 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; 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); + + 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); + let c_transfer_2 = commit(v_transfer_2, r_transfer_2); + 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); + 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); + + 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_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 = + 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_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') + // publish v_tilde values whose difference is the amount delta. The circuit + // 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); +} + +#[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_NEW); + 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_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, + 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_TILDE_AUD_S + 1, ); } @@ -1462,6 +1703,7 @@ fn rejects_wrong_k_aud_r() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -1500,5 +1742,6 @@ fn rejects_wrong_k_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, A_TILDE_AUD_S, + R_TILDE_AUD_S, ); } 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..3003de492 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,9 +193,10 @@ 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. +// `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 @@ -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,45 @@ fn rejects_tampered_b_tilde_aud_s() { R_TILDE_AUD_R, V_TILDE_AUD_S, B_TILDE_AUD_S + 1, + 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/vks/set_spender.vk.json b/packages/tokens/src/confidential/circuits/vks/set_spender.vk.json index ad7189e2e..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","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","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 4f047cf19..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","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","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/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..bfcd6b985 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 +// sigma)[1], [2] Sender-channel sponge, +// lane[1] and lane[2]. +// The lane[0] 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..a9911e7dd 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_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() { @@ -72,7 +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 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; @@ -87,6 +92,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] @@ -112,12 +118,17 @@ 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 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); 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 +153,7 @@ fn matches_fixture() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -156,7 +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 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, @@ -177,6 +191,7 @@ fn full_withdrawal() { R_E_X, R_E_Y, b_tilde_aud_s, + r_tilde_aud_s, ); } @@ -190,7 +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 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, @@ -211,6 +228,7 @@ fn rejects_under_funded_withdrawal() { R_E_X, R_E_Y, b_tilde_aud_s_invalid, + r_tilde_aud_s_invalid, ); } @@ -243,6 +261,7 @@ fn rejects_v_out_of_range() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -270,6 +289,7 @@ fn rejects_a_out_of_range() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -295,6 +315,7 @@ fn rejects_wrong_sk() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -320,6 +341,7 @@ fn rejects_wrong_balance_opening() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -345,6 +367,7 @@ fn rejects_wrong_addr_f() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -370,6 +393,7 @@ fn rejects_tampered_b_tilde() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -395,6 +419,7 @@ fn rejects_tampered_c_spend_new() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -422,6 +447,7 @@ fn rejects_r_e_zero() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -449,6 +475,7 @@ fn rejects_wrong_r_e() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -476,6 +503,7 @@ fn rejects_off_curve_k_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } @@ -503,14 +531,14 @@ fn rejects_identity_k_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S, + R_TILDE_AUD_S, ); } #[test(should_fail)] fn rejects_tampered_b_tilde_aud_s() { - // b_tilde_aud_s mutated by +1: W_a3+W_a4 fail because - // encrypt_auditor_sender_balance derives a specific scalar that no - // longer matches the prover's claim. Closes the + // 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, @@ -532,6 +560,36 @@ fn rejects_tampered_b_tilde_aud_s() { R_E_X, R_E_Y, B_TILDE_AUD_S + 1, + R_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, ); } @@ -560,5 +618,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..40070b7dd 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), + r_tilde_aud_s: 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), + r_a_tilde_aud_s: fr(e), } } diff --git a/packages/tokens/src/confidential/docs/COMPLIANCE.md b/packages/tokens/src/confidential/docs/COMPLIANCE.md index cc24f991f..4fa533739 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 2bbbd5dcb..290d85c04 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,17 +143,17 @@ 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)[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. -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. +**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}\_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}}$$ 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. @@ -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 | |:---|:---| @@ -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 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`** @@ -600,10 +602,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 +615,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 +651,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 +666,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 +698,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 | $$\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) = \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 +716,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}}$$, $$\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}})$$. +**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 @@ -730,23 +736,25 @@ 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}) = \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 | $$\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 (24 fields):** +**Public inputs (25 fields):** | Input | Notes | |:---|:---| @@ -755,13 +763,13 @@ 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}}$$, $$\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}})$$. +**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*. @@ -829,9 +837,11 @@ 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 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 -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 afaaeb1a1..9319a4102 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: @@ -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]` 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}) = \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.** 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: $$v\_r = \sum\_i v\_{\text{transfer},i} + \sum\_j a\_j, \qquad r\_r = \sum\_i r\_{\text{transfer},i}$$ @@ -48,10 +50,19 @@ 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. +- **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. + +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}}$$. ### 8.2 Auditor Visibility Properties @@ -59,12 +70,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. @@ -78,29 +89,48 @@ 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\_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)$$ +$$(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]` blinding-escrow slot -- the *new* allowance blinding $$r\_a'$$, the one O11 commits $$C\_a'$$ under (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 r\_a' = \tilde{r}\_{\text{aud,s}} - 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. +$$(\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. ### 8.5 Spender Allowance Auditing 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). -**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. +**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. + +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: + +- 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. + +**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. --- @@ -155,7 +185,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 @@ -173,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 @@ -209,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 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 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 @@ -230,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 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 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). @@ -260,7 +292,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*. @@ -358,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. @@ -411,13 +444,13 @@ 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}}$$, $$\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. +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 @@ -450,17 +483,17 @@ 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'$$, $$\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). @@ -512,9 +545,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\\\_allow\\\_r\\\_aud}}$$ | 17 | Allowance-blinding escrow to the owner's auditor (S14, §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}}$$ 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 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 `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/INDEXER.md b/packages/tokens/src/confidential/docs/INDEXER.md index ab0d7222d..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). @@ -114,7 +115,16 @@ 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. + +### 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 diff --git a/packages/tokens/src/confidential/docs/OVERVIEW.md b/packages/tokens/src/confidential/docs/OVERVIEW.md index 1bbe76f89..693350a75 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. | --- @@ -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. | @@ -129,8 +129,14 @@ 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 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 | +| 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. 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 +146,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. 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 33cc1d81e..2ee6fb501 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}$$; `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. 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 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 @@ -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\\\_allow\\\_r\\\_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*. 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 @@ -395,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: @@ -460,22 +463,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. 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 | -|:--|:--|:--| -| 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`; 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 `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). 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, 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 *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. --- @@ -513,7 +518,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..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}})$$: 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}}$$, 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. +**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. @@ -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}}$$, $$\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** @@ -307,16 +307,16 @@ 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 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 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** | 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 | @@ -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 $$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. --- @@ -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}}$$) | 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}}$$) | 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 8af6269dc..d7825631a 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_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, spender: Address, @@ -448,7 +449,7 @@ pub trait ConfidentialToken { /// /// * 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]` + /// v_tilde_aud_s, b_tilde_aud_s, r_tilde_aud_s, r_a_tilde_aud_s]` fn set_spender( e: &Env, account: Address, @@ -663,6 +664,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 +678,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 +688,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 +709,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 +726,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 +739,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); } @@ -749,11 +756,12 @@ 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>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Emits an `SpenderTransfer` event. @@ -765,11 +773,12 @@ 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>, v_tilde_aud_s: &BytesN<32>, a_tilde_aud_s: &BytesN<32>, + r_tilde_aud_s: &BytesN<32>, ) { SpenderTransfer { spender: spender.clone(), @@ -777,11 +786,12 @@ 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(), v_tilde_aud_s: v_tilde_aud_s.clone(), a_tilde_aud_s: a_tilde_aud_s.clone(), + r_tilde_aud_s: r_tilde_aud_s.clone(), } .publish(e); } @@ -800,6 +810,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 r_a_tilde_aud_s: BytesN<32>, } /// Emits a `SetSpender` event. @@ -814,6 +826,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>, + r_a_tilde_aud_s: &BytesN<32>, ) { SetSpender { account: account.clone(), @@ -824,6 +838,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(), + 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 42cd61ae4..9c447f346 100644 --- a/packages/tokens/src/confidential/storage.rs +++ b/packages/tokens/src/confidential/storage.rs @@ -115,6 +115,7 @@ pub struct WithdrawPayload { pub r_e_point: Point, pub sigma: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -141,6 +142,7 @@ pub struct TransferPayload { 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>, } /// Envelope decoded from the `data: Bytes` argument of @@ -167,6 +169,7 @@ pub struct SpenderTransferPayload { pub r_tilde_aud_r: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub a_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -192,6 +195,8 @@ pub struct SetSpenderPayload { pub sigma_a: BytesN<32>, pub v_tilde_aud_s: BytesN<32>, pub b_tilde_aud_s: BytesN<32>, + pub r_tilde_aud_s: BytesN<32>, + pub r_a_tilde_aud_s: BytesN<32>, } /// Envelope decoded from the `data: Bytes` argument of @@ -574,7 +579,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 +615,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 +627,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 +645,7 @@ pub fn withdraw( &payload.sigma, &payload.b_tilde, &payload.b_tilde_aud_s, + &payload.r_tilde_aud_s, ); } @@ -667,7 +674,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 +697,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 +716,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 +735,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, ); } @@ -760,8 +770,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]` +/// * 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 /// @@ -790,12 +800,11 @@ 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', - // 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, + // r_tilde_aud_s let mut pi = Bytes::new(e); append_point(&mut pi, &delegation.allowance_commitment); append_field(&mut pi, &delegation.allowance_salt); @@ -813,6 +822,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.r_tilde_aud_s); verify(e, CircuitType::SpenderTransfer, &pi, proof); @@ -833,11 +843,12 @@ 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, &payload.v_tilde_aud_s, &payload.a_tilde_aud_s, + &payload.r_tilde_aud_s, ); } @@ -873,7 +884,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, r_a_tilde_aud_s]` /// /// # Security Warning /// @@ -897,7 +908,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, 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); @@ -915,6 +927,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.r_a_tilde_aud_s); verify(e, CircuitType::SetSpender, &pi, proof); @@ -942,6 +956,8 @@ pub fn set_spender( &payload.b_tilde, &payload.v_tilde_aud_s, &payload.b_tilde_aud_s, + &payload.r_tilde_aud_s, + &payload.r_a_tilde_aud_s, ); } diff --git a/packages/tokens/src/confidential/test.rs b/packages/tokens/src/confidential/test.rs index e0923bce6..e71ffb57a 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,13 +13,26 @@ 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, 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 @@ -37,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, @@ -143,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; @@ -166,6 +212,7 @@ struct Harness<'a> { token_admin: Address, sac: StellarAssetClient<'a>, sac_addr: Address, + auditor_addr: Address, } fn setup<'a>() -> Harness<'a> { @@ -200,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 { @@ -219,6 +266,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 +286,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 +306,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), + r_a_tilde_aud_s: fixture_field(e, 0x28), }, proof: Bytes::new(e), } @@ -276,6 +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), + r_tilde_aud_s: fixture_field(e, 0x38), }, proof: Bytes::new(e), } @@ -384,6 +436,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), } @@ -536,7 +589,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); @@ -584,7 +652,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); @@ -606,7 +692,24 @@ 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), + 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) + ); let delegation = h.token.get_spender_delegation(&alice, &spender); assert_eq!(delegation.live_until_ledger, 1_000); @@ -661,6 +764,122 @@ 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), + 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_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] @@ -676,7 +895,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 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), + 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) + ); // Delegation allowance commitment was rotated. let delegation = h.token.get_spender_delegation(&alice, &spender); @@ -686,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() {