diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 3c931270d..6ee2f9f27 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -4,6 +4,7 @@ members = [ "register", "withdraw", "transfer", + "set_operator", "gadgets/assert_on_curve", "gadgets/commit", "gadgets/ecdh", diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index c27213b7c..8363906f9 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -22,6 +22,10 @@ | circuit_register | directive_invert | N/A | N/A | 9 | | circuit_register | lte_hint | N/A | N/A | 33 | | circuit_register | main | Bounded { width: 4 } | 33 | 72 | +| circuit_set_operator | decompose_hint | N/A | N/A | 30 | +| circuit_set_operator | directive_invert | N/A | N/A | 9 | +| circuit_set_operator | lte_hint | N/A | N/A | 33 | +| circuit_set_operator | main | Bounded { width: 4 } | 128 | 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 | diff --git a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh index 0d511b318..04931d64b 100755 --- a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh +++ b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh @@ -25,6 +25,7 @@ CIRCUITS=( "register" "withdraw" "transfer" + "set_operator" ) OUT_DIR="vks" diff --git a/packages/tokens/src/confidential/circuits/set_operator/Nargo.toml b/packages/tokens/src/confidential/circuits/set_operator/Nargo.toml new file mode 100644 index 000000000..9907d429a --- /dev/null +++ b/packages/tokens/src/confidential/circuits/set_operator/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "circuit_set_operator" +type = "bin" +authors = ["OpenZeppelin"] +compiler_version = ">=0.30.0" + +[dependencies] +stellar_confidential_lib = { path = "../lib" } diff --git a/packages/tokens/src/confidential/circuits/set_operator/src/main.nr b/packages/tokens/src/confidential/circuits/set_operator/src/main.nr new file mode 100644 index 000000000..dcfd7fc50 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/set_operator/src/main.nr @@ -0,0 +1,274 @@ +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, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +mod tests; + +// SetOperator circuit -- design doc Section 7.7. +// +// Constraints +// ----------- +// Owner balance split (ownership + new spendable balance) +// S1 Y = sk * H Owner key +// ownership. +// S2 vk = Poseidon2(delta_vk, sk, wrap) Wrapper-bound +// viewing key. +// S3 C_spend = v * G + r * H Opening of current +// spendable balance. +// S4 v, v_a, v - v_a in [0, 2^127) Range validity +// (Section 2.6); the +// [0, 2^127) bound is +// the SEP-41 i128 +// non-negative range +// (Section 3.4). +// S9 r' = Poseidon2(delta_spend_r, vk, sigma) Deterministic +// randomness for the +// new spendable +// balance. +// S10 C_spend' = (v - v_a) * G + r' * H New spendable +// commitment. +// S11 b_tilde = (v - v_a) +// + Poseidon2(delta_enc_bal, vk, sigma) +// Encrypted balance +// scalar (emitted). +// Delegation + allowance (per-operator escrow) +// S5 dvk_i = Poseidon2(delta_dvk, vk, op_i) Delegation viewing +// key; wrapper-bound +// via vk. +// S6 r_a = Poseidon2(delta_allow_r, dvk_i, +// sigma_a) Deterministic +// allowance blinding. +// S7 C_a = v_a * G + r_a * H Allowance commitment. +// S8 a_tilde = v_a +// + Poseidon2(delta_enc_allow, dvk_i, sigma_a) +// Encrypted allowance +// scalar (emitted). +// S12 Escrowed dvk_i correctly encrypts under Y_op via ECDH. +// Expanded per Section 7.11 into three sub-constraints over the +// prover-supplied escrowed_dvk = (R_x, dvk_cipher): +// (a) R_x = R_e.x Same r_e as the +// auditor block +// (S_a1), forced +// equal here. +// (b) s_esc = (r_e * Y_op).x Operator-handoff +// ECDH shared secret. +// (c) dvk_cipher = dvk_i +// + Poseidon2(delta_esc_dvk, s_esc, op_i) +// Masked dvk_i for the +// operator. +// S13 r_e != 0 Rules out R_e = O, +// S_{a,s} = O, and a +// trivial escrow +// shared secret +// (same r_e is reused +// for the dvk escrow +// ECDH; Section 7.11). +// Auditor block (owner-auditor visibility, Section 8.1) +// S_a1 R_e = r_e * H Ephemeral key for +// auditor ECDH (also +// the escrow ephemeral +// key; see S12(a)). +// S_a2 S_{a,s} = r_e * K_aud_s Owner-auditor ECDH +// shared secret. +// S_a3 (m_v, m_b) = SpongeSqueeze_2(delta_aud_s, +// S_{a,s}.x, sigma) Owner-channel +// sponge: two 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. +// +// Point-validation doctrine (Section 10.8) +// ---------------------------------------- +// Y, C_spend, C_spend', C_a, R_e, and S_{a,s} are bound to in-circuit +// multi_scalar_mul outputs (S1, S3, S10, S7, S_a1, S_a2) and are therefore +// on-curve by construction -- no explicit check needed. Y_op was constrained +// on-curve when the operator registered (R1, Section 7.2) and is loaded by +// the wrapper from trusted storage, so it is path (2) of Section 10.8 and the +// wrapper trusts it on read. K_aud_s is a public-input key consumed by an +// ECDH multiplication (path (3)); the verifier doesn't check curve membership +// and an off-curve K_aud_s would break the soundness of S_a2, so this file +// explicitly validates it on-curve AND non-identity before the auditor block +// runs. +// +// Public inputs (24 fields, in design-doc canonical order) +// -------------------------------------------------------- +// Idx Param Symbol Source / Note +// --- ----- ------ ----------------------------- +// 0 c_spend_x C_spend.x Loaded from +// 1 c_spend_y C_spend.y `from.spendable_balance`. +// 2 y_x Y.x Loaded from +// 3 y_y Y.y `from.spending_key`. +// 4 y_op_x Y_op.x Loaded from operator +// 5 y_op_y Y_op.y account's `spending_key`. +// Operator must be registered. +// 6 op_i op_i address_to_field(`operator`), +// computed off-circuit by the +// wrapper (Section 2.7). +// 7 wrap wrap `env.current_contract_address()`. +// 8 k_aud_s_x K_aud_s.x Fetched from the auditor +// 9 k_aud_s_y K_aud_s.y contract by `from.auditor_id`. +// 10 c_spend_new_x C_spend'.x Prover-supplied; written to +// 11 c_spend_new_y C_spend'.y `from.spendable_balance`. +// 12 c_a_x C_a.x Prover-supplied; written to +// 13 c_a_y C_a.y `(from, op).allowance`. +// 14 escrowed_dvk_r_x R_x Prover-supplied; the first +// 32-byte limb of the on-chain +// escrowed_dvk encoding +// (Section 7.11); forced equal +// to R_e.x by S12(a). +// 15 escrowed_dvk_cipher dvk_cipher Prover-supplied; second limb +// of escrowed_dvk; stored. +// 16 b_tilde b_tilde Prover-supplied encrypted +// balance scalar; emitted. +// 17 a_tilde a_tilde Prover-supplied encrypted +// allowance scalar; emitted. +// 18 sigma sigma Prover-supplied owner-channel +// salt; emitted. +// 19 sigma_a sigma_a Prover-supplied allowance +// salt; stored. +// 20 r_e_x R_e.x Prover-supplied ephemeral key +// 21 r_e_y R_e.y for auditor ECDH; emitted. +// 22 v_tilde_aud_s v_tilde_aud_s Prover-supplied owner-auditor +// encrypted escrow amount; +// emitted. +// 23 b_tilde_aud_s b_tilde_aud_s Prover-supplied owner-auditor +// encrypted balance checkpoint; +// emitted. +// +// Private witnesses +// ----------------- +// sk Owner spending secret scalar. +// v Plaintext spendable-balance value. +// r Plaintext blinding factor for C_spend. +// v_a Plaintext per-operator allowance. +// r_e Ephemeral scalar for the auditor + escrow ECDH; must satisfy +// r_e != 0 (S13). + +fn main( + sk: Field, + v: Field, + r: Field, + v_a: Field, + r_e: Field, + c_spend_x: pub Field, + c_spend_y: pub Field, + y_x: pub Field, + y_y: pub Field, + y_op_x: pub Field, + y_op_y: pub Field, + op_i: pub Field, + wrap: pub Field, + k_aud_s_x: pub Field, + k_aud_s_y: pub Field, + c_spend_new_x: pub Field, + c_spend_new_y: pub Field, + c_a_x: pub Field, + c_a_y: pub Field, + escrowed_dvk_r_x: pub Field, + escrowed_dvk_cipher: pub Field, + b_tilde: pub Field, + a_tilde: pub Field, + sigma: pub Field, + sigma_a: pub Field, + r_e_x: pub Field, + r_e_y: pub Field, + v_tilde_aud_s: pub Field, + b_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}, + // and the escrow shared secret all collapse together). + assert(r_e != 0); + + // S1 + let y_derived = scalar_mul(sk, H); + assert(y_derived.x == y_x); + assert(y_derived.y == y_y); + + // S2 + let vk = vk_from_sk(sk, wrap); + + // S3 + let c_spend_derived = commit(v, r); + assert(c_spend_derived.x == c_spend_x); + assert(c_spend_derived.y == c_spend_y); + + // S4 -- Section 2.6 spells out the 127-bit decomposition / recomposition + // pattern; `assert_max_bit_size` is the Noir stdlib primitive that + // implements it directly. + v.assert_max_bit_size::<127>(); + v_a.assert_max_bit_size::<127>(); + let v_new = v - v_a; + v_new.assert_max_bit_size::<127>(); + + // S5 + let dvk = dvk_from_vk_op(vk, op_i); + + // S6 + let r_a = derive_allow_r(dvk, sigma_a); + + // S7 + let c_a_derived = commit(v_a, r_a); + assert(c_a_derived.x == c_a_x); + assert(c_a_derived.y == c_a_y); + + // S8 + let a_tilde_derived = encrypt_allowance(v_a, dvk, sigma_a); + assert(a_tilde_derived == a_tilde); + + // S9 + let r_new = derive_spend_r(vk, sigma); + + // S10 + let c_spend_new_derived = commit(v_new, r_new); + assert(c_spend_new_derived.x == c_spend_new_x); + assert(c_spend_new_derived.y == c_spend_new_y); + + // S11 + let b_tilde_derived = encrypt_balance(v_new, vk, sigma); + assert(b_tilde_derived == b_tilde); + + // S_a1 + let r_e_derived = scalar_mul(r_e, H); + assert(r_e_derived.x == r_e_x); + assert(r_e_derived.y == r_e_y); + + // S12(a) -- escrowed_dvk's R_x limb is forced equal to R_e.x, which is + // the auditor-block ephemeral. Section 7.11: "the escrow's R_x and the + // auditor channel's R_e.x are forced equal" -- this constraint is what + // forces it. + assert(escrowed_dvk_r_x == r_e_x); + + // Y_op is path (2) of Section 10.8: bound to scalar_mul(sk_op, H) when + // the operator registered (R1), trusted on read from the wrapper. + let y_op = EmbeddedCurvePoint { x: y_op_x, y: y_op_y, is_infinite: false }; + + // S12(b) + let s_esc = ecdh(r_e, y_op); + + // S12(c) + let escrowed_dvk_cipher_derived = encrypt_esc_dvk(dvk, s_esc, op_i); + assert(escrowed_dvk_cipher_derived == escrowed_dvk_cipher); + + // K_aud_s point validation (Section 10.8: public-input key). + let k_aud_s = EmbeddedCurvePoint { x: k_aud_s_x, y: k_aud_s_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_s); + + // S_a2 (owner-auditor shared secret x-coordinate) + let s_a_s_x = 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_x, 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); +} diff --git a/packages/tokens/src/confidential/circuits/set_operator/src/tests.nr b/packages/tokens/src/confidential/circuits/set_operator/src/tests.nr new file mode 100644 index 000000000..897ee522b --- /dev/null +++ b/packages/tokens/src/confidential/circuits/set_operator/src/tests.nr @@ -0,0 +1,1252 @@ +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, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +// Canonical fixture inputs. +// +// SK / WRAP / SIGMA / V / R / R_E reuse the lib's pinned vectors (lib/testdata) +// so that Y, C_spend, vk, r_new, b_tilde, R_e, and the owner-channel masks +// are all derivable from -- and pinned by -- the lib's `fixtures_match_testdata` +// test. V_A = 300 keeps the happy path on a non-trivial v - v_a while still +// satisfying S4 (matches the withdraw fixture's A so the owner-block balance +// math is identical across the two owner-initiated circuits). SIGMA_A is a +// distinct salt so the allowance channel is independent of the owner channel. +// SK_OP / K_AUD_S_SCALAR are placeholder secret scalars used to materialize +// on-curve Y_op / K_aud_s; any non-zero scalar would do, these are pinned by +// `set_operator_fixtures_match_lib`. K_AUD_S_SCALAR matches the withdraw +// fixture's auditor scalar so the two circuits share the same owner-auditor +// key in cross-circuit tooling. OP_I is a placeholder address-as-field; the +// wrapper computes it off-circuit via `address_to_field` (Section 2.7). +global SK: Field = 0xdead; +global WRAP: Field = 0xbeef; +global SIGMA: Field = 0x01; +global SIGMA_A: Field = 0x02; +global V: Field = 1000; +global R: Field = 42; +global V_A: Field = 300; +global V_NEW: Field = 700; +global R_E: Field = 0xfeedface; +global OP_I: Field = 0xfee1; + +// Placeholder operator secret scalar; Y_op = SK_OP * H. +global SK_OP: Field = 0xbabe; +// Placeholder owner-auditor secret scalar; K_aud_s = scalar * H. Matches the +// withdraw fixture so cross-circuit tooling sees the same K_aud_s. +global K_AUD_S_SCALAR: Field = 0xc0ffee; + +// Y = sk*H (= lib `scalar_mul_Y` fixture). +global Y_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; +global Y_Y: Field = 0x29116280600c10ead1fdbd9ab4b571896030679bf554d7f1ebf681e5147de21b; + +// C_spend = commit(1000, 42) (= lib `commit` fixture). +global C_SPEND_X: Field = + 0x195a5d8ecd032fe1696054b28f0852b5f03a613681991ace823245ab2f97ed05; +global C_SPEND_Y: Field = + 0x0e67489ecfee0e581dce7db22a383c635886cfb616c3b3bb033bff0c46e9789e; + +// K_aud_s = K_AUD_S_SCALAR * H. R_e = R_E * H. Both pinned by the withdraw +// fixture (`withdraw_auditor_fixtures_match_lib`) since they reuse the same +// scalars. +global K_AUD_S_X: Field = + 0x22502c7b20b64aeaaaa4d4fa5f2f8600f9734e828f7284a8d1431da36b33803a; +global K_AUD_S_Y: Field = + 0x0419089353d24334f03f4a1c9c9b19282e32f6879c5c69b9cdffba6a13a7c5ed; +global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; +global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; + +// C_spend' and b_tilde reuse the withdraw fixture: V_A = A = 300 here, and +// the owner-block randomness (vk, sigma) is identical, so both spendable- +// balance ciphertexts collapse to the withdraw values. Pinned by +// `withdraw_fixtures_match_lib` and re-asserted by +// `set_operator_fixtures_match_lib`. +global C_SPEND_NEW_X: Field = + 0x22f167015ee0e4b33dcb278cdc824c336c919dc4cd06f391850305d5083336e6; +global C_SPEND_NEW_Y: Field = + 0x0be3d0fda4f8f70b04e2d609db4e29b30345da9288bf208c7560572b2bef172d; +global B_TILDE: Field = + 0x04d1659db899a50a94dcfc54a18b8adaf6e9e8e3046bd11893fbd4a86a7c5670; + +// New values pinned by `set_operator_fixtures_match_lib` and +// `set_operator_auditor_fixtures_match_lib` (re-derived from the lib +// primitives -- if either side drifts the test fails immediately). Emitted by +// `print_fixtures`. +global Y_OP_X: Field = 0x08a500f1d05ebcba6cdbe9c246475e406f28ee2285fdfa4ed9781013ebb1571e; +global Y_OP_Y: Field = 0x2264cba150d089d7678ee412307b118826efb9e4c0f1b4ae10a851d63e83c632; +global C_A_X: Field = 0x1c673e9370f2dcf333e4c63d6faf3957e5a667017406540713b248583c5ae7d7; +global C_A_Y: Field = 0x1f9f86c84bed1f091c282300a063e4ff38082a63aeec17aef054d5ece0c3ebcc; +global A_TILDE: Field = + 0x0c4647bbcd2819afdcd6facf2037861da37e68008f3a40ec2f8db6487e1355c1; +global ESCROWED_DVK_CIPHER: Field = + 0x0d9a3beebbda646b71859a11420a74f8b3fe645d9f235f862733554e1d928405; +global V_TILDE_AUD_S: Field = + 0x0a607feb31e56e89ed94a3e1dc3811d3d23aaf10b2f27874ce670878b9f6eee1; +global B_TILDE_AUD_S: Field = + 0x0b586defafdf4583d0f338855fdf8391102adaac4634a3b80594aea6f788e103; + +#[test] +fn print_fixtures() { + // One-shot harness: prints every derived public input for the chosen + // fixture tuple. Run with + // `nargo test --package circuit_set_operator print_fixtures --show-output` + // and paste the values into the globals above. + let vk = vk_from_sk(SK, WRAP); + 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); + + let r_new = derive_spend_r(vk, SIGMA); + let c_spend_new = commit(V_NEW, r_new); + let b_tilde = encrypt_balance(V_NEW, vk, SIGMA); + + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + let v_tilde_aud_s = V_A + m_s[0]; + let b_tilde_aud_s = V_NEW + m_s[1]; + + let yopx = y_op.x; + let yopy = y_op.y; + let cax = c_a.x; + let cay = c_a.y; + let csnx = c_spend_new.x; + let csny = c_spend_new.y; + let kx = k_aud_s.x; + let ky = k_aud_s.y; + let rex = r_e_pt.x; + let rey = r_e_pt.y; + println(f"Y_OP_X = {yopx}"); + println(f"Y_OP_Y = {yopy}"); + println(f"C_A_X = {cax}"); + println(f"C_A_Y = {cay}"); + println(f"A_TILDE = {a_tilde}"); + println(f"C_SPEND_NEW_X = {csnx}"); + println(f"C_SPEND_NEW_Y = {csny}"); + println(f"B_TILDE = {b_tilde}"); + println(f"ESCROWED_DVK_CIPHER = {escrowed_dvk_cipher}"); + println(f"K_AUD_S_X = {kx}"); + println(f"K_AUD_S_Y = {ky}"); + println(f"R_E_X = {rex}"); + 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}"); +} + +#[test] +fn set_operator_fixtures_match_lib() { + let vk = vk_from_sk(SK, WRAP); + 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); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(V_NEW, r_new); + let b_tilde = encrypt_balance(V_NEW, vk, SIGMA); + let y = scalar_mul(SK, H); + let c_spend = commit(V, R); + let y_op = scalar_mul(SK_OP, H); + + assert(y.x == Y_X); + assert(y.y == Y_Y); + assert(c_spend.x == C_SPEND_X); + assert(c_spend.y == C_SPEND_Y); + assert(y_op.x == Y_OP_X); + assert(y_op.y == Y_OP_Y); + assert(c_a.x == C_A_X); + assert(c_a.y == C_A_Y); + assert(a_tilde == A_TILDE); + assert(c_new.x == C_SPEND_NEW_X); + assert(c_new.y == C_SPEND_NEW_Y); + assert(b_tilde == B_TILDE); +} + +#[test] +fn set_operator_auditor_fixtures_match_lib() { + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + + assert(k_aud_s.x == K_AUD_S_X); + assert(k_aud_s.y == K_AUD_S_Y); + assert(r_e_pt.x == R_E_X); + assert(r_e_pt.y == R_E_Y); + assert(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); +} + +// `run_main` accepts all 24 public inputs as parameters -- nothing baked in. +// Tests that target a specific constraint construct the public tuple so only +// that constraint can fail: tamper the witness or public input under test +// AND recompute every downstream public that depends on it, so no cascaded +// mismatch coincidentally lights up the same `should_fail`. +fn run_main( + sk_in: Field, + v_in: Field, + r_in: Field, + v_a_in: Field, + r_e_in: Field, + c_spend_x_in: Field, + c_spend_y_in: Field, + y_x_in: Field, + y_y_in: Field, + y_op_x_in: Field, + y_op_y_in: Field, + op_i_in: Field, + wrap_in: Field, + k_aud_s_x_in: Field, + k_aud_s_y_in: Field, + c_spend_new_x_in: Field, + c_spend_new_y_in: Field, + c_a_x_in: Field, + c_a_y_in: Field, + escrowed_dvk_r_x_in: Field, + escrowed_dvk_cipher_in: Field, + b_tilde_in: Field, + a_tilde_in: Field, + sigma_in: Field, + sigma_a_in: Field, + r_e_x_in: Field, + r_e_y_in: Field, + v_tilde_aud_s_in: Field, + b_tilde_aud_s_in: Field, +) { + main( + sk_in, + v_in, + r_in, + v_a_in, + r_e_in, + c_spend_x_in, + c_spend_y_in, + y_x_in, + y_y_in, + y_op_x_in, + y_op_y_in, + op_i_in, + wrap_in, + k_aud_s_x_in, + k_aud_s_y_in, + c_spend_new_x_in, + c_spend_new_y_in, + c_a_x_in, + c_a_y_in, + escrowed_dvk_r_x_in, + escrowed_dvk_cipher_in, + b_tilde_in, + a_tilde_in, + sigma_in, + sigma_a_in, + r_e_x_in, + r_e_y_in, + v_tilde_aud_s_in, + b_tilde_aud_s_in, + ); +} + +fn run_fixture() { + 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, + WRAP, + 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, + ); +} + +#[test] +fn matches_fixture() { + run_fixture(); +} + +#[test] +fn full_allowance_escrow() { + // Boundary: v_a = v, so v_new = 0. C_spend' commits to (0, r_new) which + // is a valid commitment to zero. b_tilde and b_tilde_aud_s both bind to 0 + // (plus mask) so the owner's auditor sees the post-escrow spendable + // balance reach zero. C_a binds to the full v. + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); + let c_a = commit(V, r_a); + let a_tilde = encrypt_allowance(V, dvk, SIGMA_A); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(0, r_new); + let b_tilde = encrypt_balance(0, vk, SIGMA); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let s_esc = ecdh(R_E, y_op); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + run_main( + SK, + V, + R, + V, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new.x, + c_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 + m_s[0], + 0 + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_under_funded_escrow() { + // v_a > v: v - v_a underflows in the field to a value >= 2^128. S4 fires. + // Every downstream public is recomputed against v_a_too_large so the + // single firing constraint is S4's v_new range check. + let v_a_too_large: Field = V + 1; + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); + let c_a_invalid = commit(v_a_too_large, r_a); + let a_tilde_invalid = encrypt_allowance(v_a_too_large, dvk, SIGMA_A); + let r_new = derive_spend_r(vk, SIGMA); + let c_new_invalid = commit(V - v_a_too_large, r_new); + let b_tilde_invalid = encrypt_balance(V - v_a_too_large, vk, SIGMA); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let s_esc = ecdh(R_E, y_op); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + run_main( + SK, + V, + R, + v_a_too_large, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new_invalid.x, + c_new_invalid.y, + c_a_invalid.x, + c_a_invalid.y, + R_E_X, + escrowed_dvk_cipher, + b_tilde_invalid, + a_tilde_invalid, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + v_a_too_large + m_s[0], + (V - v_a_too_large) + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_out_of_range() { + // 2^127 is exactly the boundary S4 rules out (Section 2.6 / 3.4 -- SEP-41 + // i128 non-negative range is [0, 2^127), so 2^127 is the smallest value + // that must fail). With v_huge - V_A = 2^127 - 300 still in [0, 2^127), + // the v_new range check passes; only the `v.assert_max_bit_size<127>()` + // sub-check of S4 fires. C_spend, c_spend_new, b_tilde, and b_tilde_aud_s + // are recomputed against v_huge / v_new_huge so S3 / S10 / S11 / S_a5 + // cannot coincidentally fire. + let v_huge: Field = 0x80000000000000000000000000000000; + let v_new_huge: Field = v_huge - V_A; + let vk = vk_from_sk(SK, WRAP); + let c_spend = commit(v_huge, R); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(v_new_huge, r_new); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + run_main( + SK, + v_huge, + R, + V_A, + R_E, + c_spend.x, + c_spend.y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new.x, + c_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, + v_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_a_out_of_range() { + // v_a = 2^127: S4's range check on v_a fires at the exact SEP-41 + // boundary. v_new = V - 2^127 field-underflows so the v_new range check + // would also fire, but both belong to S4. Every downstream public that + // depends on v_a (C_a, a_tilde, v_tilde_aud_s) and on v_new (c_spend_new, + // b_tilde, b_tilde_aud_s) is recomputed so the only constraints that fail + // are S4's two range sub-checks. + let v_a_huge: Field = 0x80000000000000000000000000000000; + let v_new_huge: Field = V - v_a_huge; + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + let r_a = derive_allow_r(dvk, SIGMA_A); + let c_a = commit(v_a_huge, r_a); + let a_tilde = encrypt_allowance(v_a_huge, dvk, SIGMA_A); + let r_new = derive_spend_r(vk, SIGMA); + let c_new = commit(v_new_huge, r_new); + let b_tilde = encrypt_balance(v_new_huge, vk, SIGMA); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let s_esc = ecdh(R_E, y_op); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + run_main( + SK, + V, + R, + v_a_huge, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new.x, + c_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_a_huge + m_s[0], + v_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_sk() { + // sk + 1 != sk: S1 fails because scalar_mul(sk+1, H) no longer matches Y. + // vk is rebound to (sk+1, wrap) and every vk-dependent public + // (c_spend_new, b_tilde, c_a, a_tilde, escrowed_dvk_cipher) is recomputed + // under the new vk so S10 / S11 / S7 / S8 / S12(c) cannot fire alongside + // S1. The auditor block does not depend on vk so its canonical values + // still satisfy S_a4 / S_a5. + let vk_bad = vk_from_sk(SK + 1, WRAP); + let dvk_bad = dvk_from_vk_op(vk_bad, OP_I); + let r_a_bad = derive_allow_r(dvk_bad, SIGMA_A); + let c_a_bad = commit(V_A, r_a_bad); + let a_tilde_bad = encrypt_allowance(V_A, dvk_bad, SIGMA_A); + let r_new_bad = derive_spend_r(vk_bad, SIGMA); + let c_new_bad = commit(V_NEW, r_new_bad); + let b_tilde_bad = encrypt_balance(V_NEW, vk_bad, SIGMA); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let s_esc = ecdh(R_E, y_op); + let escrowed_dvk_cipher_bad = encrypt_esc_dvk(dvk_bad, s_esc, OP_I); + run_main( + SK + 1, + V, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new_bad.x, + c_new_bad.y, + c_a_bad.x, + c_a_bad.y, + R_E_X, + escrowed_dvk_cipher_bad, + b_tilde_bad, + a_tilde_bad, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_balance_opening() { + // S3 fires: commit(V+1, R) != C_spend. v_new = (V+1) - V_A cascades into + // S10 / S11 / S_a5; v_a is unchanged so S7 / S8 / S_a4 stay canonical. + // Recompute c_spend_new, b_tilde, and b_tilde_aud_s against v_new = 701 + // so only S3 fires. + let v_bad: Field = V + 1; + let v_new_bad: Field = v_bad - V_A; + let vk = vk_from_sk(SK, WRAP); + let r_new = derive_spend_r(vk, SIGMA); + let c_new_bad = commit(v_new_bad, r_new); + 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_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA); + run_main( + SK, + v_bad, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + c_new_bad.x, + c_new_bad.y, + C_A_X, + C_A_Y, + R_E_X, + ESCROWED_DVK_CIPHER, + b_tilde_bad, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + v_new_bad + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_wrap() { + // The circuit binds vk to wrap via S2 but doesn't assert wrap directly; + // the binding is visible only through vk-derived publics (S10, S11, S7, + // S8, S12(c)). With wrap_bad, recompute c_spend_new and c_a from the new + // vk_bad so S10 / S7 pass -- leaving b_tilde canonical, where the + // wrap-binding catches the mismatch via S11. (Recomputing everything, + // as in `rejects_wrong_sk`, would make the proof verify: S1 is the + // load-bearing wrap-independent check there, and no such constraint + // exists here.) + let wrap_bad: Field = 0xcafe; + let vk_bad = vk_from_sk(SK, wrap_bad); + let dvk_bad = dvk_from_vk_op(vk_bad, OP_I); + let r_a_bad = derive_allow_r(dvk_bad, SIGMA_A); + let c_a_bad = commit(V_A, r_a_bad); + let r_new_bad = derive_spend_r(vk_bad, SIGMA); + let c_new_bad = commit(V_NEW, r_new_bad); + 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, + wrap_bad, + K_AUD_S_X, + K_AUD_S_Y, + c_new_bad.x, + c_new_bad.y, + c_a_bad.x, + c_a_bad.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, + ); +} + +#[test(should_fail)] +fn rejects_wrong_op_i() { + // op_i changes the derived dvk (S5), which cascades into r_a (S6), C_a + // (S7), a_tilde (S8), and the escrow cipher (S12(c)). Keeping the + // canonical C_A / A_TILDE / ESCROWED_DVK_CIPHER public inputs forces all + // three of S7 / S8 / S12(c) to mismatch -- any one is sufficient to + // catch the wrong op_i. + let op_i_bad: Field = OP_I + 1; + 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_bad, + WRAP, + 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, + ); +} + +#[test(should_fail)] +fn rejects_tampered_c_a() { + // S7 fires: C_a + 1 no longer matches commit(v_a, r_a). + 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, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_A_X + 1, + 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, + ); +} + +#[test(should_fail)] +fn rejects_tampered_a_tilde() { + // S8 fires: a_tilde + 1 no longer matches + // v_a + Poseidon2(ENC_ALLOW, dvk, sigma_a). + 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, + WRAP, + 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 + 1, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_c_spend_new() { + // S10 fires. + 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, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X + 1, + 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, + ); +} + +#[test(should_fail)] +fn rejects_tampered_b_tilde() { + // S11 fires. + 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, + WRAP, + 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 + 1, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_escrowed_dvk_r_x() { + // S12(a) fires: escrowed_dvk_r_x must equal R_e.x. Tamper the R_x limb + // by +1 while keeping the canonical R_e (r_e_x / r_e_y from S_a1 + // unchanged) so the auditor block proceeds normally; only S12(a) catches + // the mismatch. + 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, + WRAP, + 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 + 1, + ESCROWED_DVK_CIPHER, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_escrowed_dvk_cipher() { + // S12(c) fires: cipher + 1 no longer matches dvk + Poseidon2(ESC_DVK, + // s_esc, op_i). + 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, + WRAP, + 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 + 1, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_TILDE_AUD_S, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_y_op() { + // Y_op replaced by a different on-curve key (Y_X, Y_Y -- the owner's + // own Y, which is valid on-curve). s_esc = r_e * Y_op changes, so + // escrowed_dvk_cipher under the canonical dvk no longer matches and + // S12(c) fails. (Y_op is path (2) of Section 10.8: trusted on read from + // the wrapper, so the in-circuit consequence of swapping it is purely + // through the escrow-cipher mask.) + run_main( + SK, + V, + R, + V_A, + R_E, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_X, + Y_Y, + OP_I, + WRAP, + 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, + ); +} + +#[test(should_fail)] +fn rejects_r_e_zero() { + // S13 fires: r_e = 0 would force R_e = O (identity), collapse S_{a,s} to + // O, and collapse the escrow shared secret s_esc to 0 -- making every + // mask a knowable constant function of sigma / op_i. Every r_e-dependent + // public (R_e = (0, 0) identity-encoding, escrowed_dvk_cipher derived + // against s_esc = 0, and the auditor channel ciphertexts derived against + // s_a_s_x = 0) is recomputed so the `assert(r_e != 0)` check is the only + // constraint that can catch this -- removing S13 would let the proof + // verify. + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + 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_x_zero = ecdh(0, k_aud_s); + let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_zero, SIGMA); + run_main( + SK, + V, + R, + V_A, + 0, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + K_AUD_S_X, + K_AUD_S_Y, + C_SPEND_NEW_X, + C_SPEND_NEW_Y, + C_A_X, + C_A_Y, + 0, + escrowed_dvk_cipher_zero, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + 0, + 0, + V_A + m_s_zero[0], + V_NEW + m_s_zero[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_r_e() { + // r_e mismatches the public R_e: S_a1 fails because scalar_mul(r_e+1, H) + // no longer equals the canonical (R_E_X, R_E_Y). R_e itself is left + // canonical (so S_a1 catches the mismatch) but every other r_e-dependent + // public (escrowed_dvk_cipher, both auditor channel ciphertexts) is + // recomputed against r_e+1 so S12(c) / S_a4 / S_a5 cannot coincidentally + // fire. escrowed_dvk_r_x is also left canonical at R_E_X so S12(a) keeps + // matching the public R_e -- the only catchable mismatch is S_a1's + // derived point. + let r_e_bad: Field = R_E + 1; + let vk = vk_from_sk(SK, WRAP); + let dvk = dvk_from_vk_op(vk, OP_I); + let y_op = EmbeddedCurvePoint { x: Y_OP_X, y: Y_OP_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + 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_x_bad = ecdh(r_e_bad, k_aud_s); + let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_bad, SIGMA); + run_main( + SK, + V, + R, + V_A, + r_e_bad, + C_SPEND_X, + C_SPEND_Y, + Y_X, + Y_Y, + Y_OP_X, + Y_OP_Y, + OP_I, + WRAP, + 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_bad, + B_TILDE, + A_TILDE, + SIGMA, + SIGMA_A, + R_E_X, + R_E_Y, + V_A + m_s_bad[0], + V_NEW + m_s_bad[1], + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_s() { + // (1, 2) is off-curve. The on-curve check on K_aud_s fires before S_a2 + // ever consumes it. + 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, + WRAP, + 1, + 2, + 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, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_s() { + // K_aud_s = identity collapses ECDH; the on-curve check rejects identity + // for keys. + 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, + WRAP, + 0, + 0, + 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, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_s() { + // K_aud_s replaced by a valid but different on-curve key (Y_X, Y_Y). + // Shared secret changes, owner-channel masks change, S_a4 fails. + 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, + WRAP, + Y_X, + Y_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, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde_aud_s() { + // S_a4 fires. + 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, + WRAP, + 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 + 1, + B_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_b_tilde_aud_s() { + // S_a5 fires. + 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, + WRAP, + 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 + 1, + ); +} diff --git a/packages/tokens/src/confidential/circuits/vks/set_operator.vk.json b/packages/tokens/src/confidential/circuits/vks/set_operator.vk.json new file mode 100644 index 000000000..486d96cd8 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/vks/set_operator.vk.json @@ -0,0 +1 @@ +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x000000000000000000000000000000af741bbc6a67e952c6d36e7f155f2c5d62","0x0000000000000000000000000000000000282f11933bfa855033758665cfcf2e","0x000000000000000000000000000000bee0f10a961a22f16bc2647af867a8fea7","0x000000000000000000000000000000000008ece09d1c43e4a181f781db4a2ef0","0x00000000000000000000000000000030365887618dc69bfb76ce0e6875317282","0x0000000000000000000000000000000000211e193ab33aa08af64bc18a5af1ff","0x000000000000000000000000000000d5d927e5c40e3ceeaeefa34be1728c2af4","0x00000000000000000000000000000000002014f55fbdd42c8648d23d20a2992a","0x000000000000000000000000000000bed1fb17366cf057671bfa3a3163cbc8ee","0x00000000000000000000000000000000000d28b5f777452b4771f33127f5141b","0x0000000000000000000000000000009aedcf0f24f6bee77974be0725e93dbb0b","0x000000000000000000000000000000000001fb7969ea25b130416a5be29dfb4e","0x00000000000000000000000000000065e649f7147528599123d87191d266645a","0x00000000000000000000000000000000002b328eb1b18b4ad48a653c7fd6ae58","0x000000000000000000000000000000382f9d91347bd46d20cc4548c2d520d3c9","0x000000000000000000000000000000000003ecc7f63f645d9c7745589b2e49ba","0x000000000000000000000000000000f06d0426ed1ccc60aa55ea8fae49810141","0x000000000000000000000000000000000017310115dc8e996e3b81db7adb2bfb","0x000000000000000000000000000000de50b2d5d86b66b110fe464f719baf0659","0x0000000000000000000000000000000000021479f83761f88133f85450a49b7d","0x00000000000000000000000000000025734b7bf5acc189e2d4de6dd99bfd3324","0x00000000000000000000000000000000002fbd888b08a201e65f32c88b306f47","0x000000000000000000000000000000c7f7fd7c1416bb1f9d1fa7ab3c14772112","0x00000000000000000000000000000000001d9130fe22f8c15d5959ecd73ee46b","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000000f00793f87b5c1d5b242345ab296149c78","0x000000000000000000000000000000000011796e2890542c47dd2878d5064348","0x000000000000000000000000000000967cc2b3b71056e18a6b2a0a5f475416aa","0x00000000000000000000000000000000002b786c6572fa473179e1a0d0859119","0x0000000000000000000000000000004bbfac405f6fba3e9203daabba8c8d50dc","0x00000000000000000000000000000000000a2b0d71e6501bf80141d4d54ecfa8","0x00000000000000000000000000000043aa328e5b93ef1b6a49708e17f70e57d9","0x00000000000000000000000000000000000905b7025feb236c1391dfb7f1d3d6","0x0000000000000000000000000000007467fa5cd63eb877038357784611bcbf96","0x0000000000000000000000000000000000062cd5671d9ff5b762533a875aef7e","0x000000000000000000000000000000e5051ead1beb62b390cb9452f60648228f","0x00000000000000000000000000000000001f5476797c313c0c0e04004a18b0c2","0x000000000000000000000000000000494c73f4bd803d7c7cd5e33743c803b5e1","0x000000000000000000000000000000000024e9c0b88577e352fed15dc5ebc12f","0x000000000000000000000000000000826ecc75e25e193a958064ca4a6c911416","0x00000000000000000000000000000000001920a77c2b6c370c8a8f6f3dc431c0","0x000000000000000000000000000000a5abcf419507b69df9f52bd4afd94cd31c","0x00000000000000000000000000000000001d944b8998e2814fb148cb55ef2e45","0x0000000000000000000000000000004763c759ea33aa289f20ccb853c322c035","0x0000000000000000000000000000000000202cb5630d6984a4e0736056e2d566","0x00000000000000000000000000000037b43ac733272d38959d94e86b2899b9c8","0x00000000000000000000000000000000002e31746e1b51b34140f88bf5f52b45","0x00000000000000000000000000000084e882ef445d12237d96fec58951bdfd71","0x00000000000000000000000000000000002293b99fe6f2c5c7de5a5b0e125759","0x0000000000000000000000000000005c7dbfbc3e9ef02d417577f506c63f852f","0x00000000000000000000000000000000001257dc481e3d07b30718d024add5da","0x000000000000000000000000000000bcfdee6187db5c810aa4033f866fd249d0","0x000000000000000000000000000000000022d298c62c1ee88e83bd5fa17a55f5","0x000000000000000000000000000000484d841b7afa15bec83e58632aa9efebf7","0x00000000000000000000000000000000001fe93a498410be5a6394a6d4fd13fd","0x00000000000000000000000000000064bf260c4879d3a2d37acb41dac7f61903","0x000000000000000000000000000000000002af639b1b55bc406fa80e09cc199c","0x000000000000000000000000000000598f821e2351f22e3bd2cade51593fe02b","0x00000000000000000000000000000000001297e789101a8fd168f6e3f3a27ebc","0x00000000000000000000000000000055001992954d5bba254ff615c285147159","0x00000000000000000000000000000000002d9bbb9e8f14f94a86c6487521a031","0x000000000000000000000000000000ce22e88f1905ec5d0af3ae3b673a203686","0x00000000000000000000000000000000002c0efd2faeccf8b4369bcda4cc4c92","0x0000000000000000000000000000000f68fa5f9023d05175db668737c7224b69","0x00000000000000000000000000000000000018f9559d1a4ad4cf03d0c0a60763","0x0000000000000000000000000000003978108d8eaa0a95383e51bf5ee7f67fe0","0x00000000000000000000000000000000000e7063b1c5f2b64f65704ccb7a403d","0x00000000000000000000000000000028f169d2343e35af2e9d55086d9aeed28e","0x00000000000000000000000000000000002d2cc50f61fb11f6ae63d82b0f0d4a","0x000000000000000000000000000000947ba048d9f8128a4a46f71add6a08a377","0x00000000000000000000000000000000002747cd2e8300fd25c57d3f12506bf4","0x0000000000000000000000000000000b0107b76dd648cf7641a25935f9491500","0x0000000000000000000000000000000000151691fcb29cf546758bc4437a3428","0x000000000000000000000000000000bcc729a93d82a1a0e5fc20e84b2ed99ed2","0x000000000000000000000000000000000029d7dbfee0f5bc7b5d262890d95ebb","0x000000000000000000000000000000fc957c8baaa583984b20fbe37988dede18","0x0000000000000000000000000000000000255ba5659322161023c9b1645fcbb7","0x0000000000000000000000000000000749ae19418d7b1784271b8c136fda8e70","0x00000000000000000000000000000000001828f167b539d23a2772f108369249","0x0000000000000000000000000000005eec000a5b56a2200314d5d7c0f0e792f9","0x00000000000000000000000000000000002ff5ffea5f5f8ed3e43a47c414d3d8","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x000000000000000000000000000000316c235e48b672f047686b9dccdb3488bf","0x000000000000000000000000000000000011d1ddaa314c817931850bf4cd1719","0x000000000000000000000000000000840079fa058847628841ff923fc99e3182","0x00000000000000000000000000000000002b4e33687739e87be590d132c113f9"] \ No newline at end of file