diff --git a/packages/tokens/src/confidential/circuits/Nargo.toml b/packages/tokens/src/confidential/circuits/Nargo.toml index 6ee2f9f27..50fd2768a 100644 --- a/packages/tokens/src/confidential/circuits/Nargo.toml +++ b/packages/tokens/src/confidential/circuits/Nargo.toml @@ -5,6 +5,7 @@ members = [ "withdraw", "transfer", "set_operator", + "operator_transfer", "gadgets/assert_on_curve", "gadgets/commit", "gadgets/ecdh", diff --git a/packages/tokens/src/confidential/circuits/constraints.baseline b/packages/tokens/src/confidential/circuits/constraints.baseline index 8363906f9..664236fc3 100644 --- a/packages/tokens/src/confidential/circuits/constraints.baseline +++ b/packages/tokens/src/confidential/circuits/constraints.baseline @@ -18,6 +18,10 @@ # Toolchain: nargo 1.0.0-beta.11 # | Package | Function | Expression Width | ACIR Opcodes | Brillig Opcodes | +| circuit_operator_transfer | decompose_hint | N/A | N/A | 30 | +| circuit_operator_transfer | directive_invert | N/A | N/A | 9 | +| circuit_operator_transfer | lte_hint | N/A | N/A | 33 | +| circuit_operator_transfer | main | Bounded { width: 4 } | 131 | 72 | | circuit_register | decompose_hint | N/A | N/A | 30 | | circuit_register | directive_invert | N/A | N/A | 9 | | circuit_register | lte_hint | N/A | N/A | 33 | diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml b/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml new file mode 100644 index 000000000..dcff4c8e1 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/Nargo.toml @@ -0,0 +1,8 @@ +[package] +name = "circuit_operator_transfer" +type = "bin" +authors = ["OpenZeppelin"] +compiler_version = ">=0.30.0" + +[dependencies] +stellar_confidential_lib = { path = "../lib" } diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr new file mode 100644 index 000000000..9d22ec5cb --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/main.nr @@ -0,0 +1,318 @@ +use stellar_confidential_lib::{ + H, assert_on_curve_non_identity, commit, derive_allow_r, derive_tx_blind, domain, ecdh, + encrypt_allowance, encrypt_amount, scalar_mul, sponge_squeeze_2, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +mod tests; + +// OperatorTransfer circuit -- design doc Section 7.8. +// +// Constraints +// ----------- +// Operator key ownership +// O1 Y_op = sk_op * H Operator key +// ownership. +// Allowance state (current) +// O2 C_a = v_a * G + r_a * H Opening of the +// stored allowance +// commitment. +// O3 r_a = Poseidon2(delta_allow_r, dvk_i, +// sigma_a) Allowance +// randomness matches +// stored state (the +// wrapper-binding +// anchor, see below). +// O4 v_a, v_tx, v_a - v_tx in [0, 2^127) Range validity +// (Section 2.6); the +// [0, 2^127) bound is +// the SEP-41 i128 +// non-negative range +// (Section 3.4). +// Recipient ECDH + transfer commitment (Section 5.3, 5.4) +// O5 S = r_e * PVK_recipient Recipient ECDH +// shared secret. +// O6 R_e = r_e * H Ephemeral public +// key. +// O7 r_tx = Poseidon2(delta_tx_blind, S.x, +// sigma_a) Anti-poisoning +// binding for the +// transfer blinding. +// O8 C_tx = v_tx * G + r_tx * H Transfer +// commitment. +// O9 v_tilde = v_tx +// + Poseidon2(delta_tx_amount, S.x, +// sigma_a) Encrypted amount +// (emitted). +// Allowance state (new) +// -- sigma_a' != sigma_a Salt rotation +// (implementation +// hardening; not in +// Section 7.8). +// O10 r_a' = Poseidon2(delta_allow_r, dvk_i, +// sigma_a') New allowance +// randomness. +// O11 C_a' = (v_a - v_tx) * G + r_a' * H New allowance +// commitment. +// O12 a_tilde' = (v_a - v_tx) +// + Poseidon2(delta_enc_allow, dvk_i, +// sigma_a') Encrypted new +// allowance scalar +// (emitted). +// Nonzero ephemeral +// O13 r_e != 0 Rules out R_e = O +// and S, S_{a,r}, +// S_{a,s} = O. +// Auditor block (dual-channel visibility, Section 8.1 + Section 8.4) +// O_a1 S_{a,r} = r_e * K_aud_r Recipient-auditor +// ECDH shared secret +// (reuses r_e). +// O_a2 (m_v_r, m_r_r) +// = SpongeSqueeze_2(delta_aud_r, +// S_{a,r}.x, sigma_a) Recipient-channel +// sponge: two masks. +// O_a3 v_tilde_aud_r = v_tx + m_v_r Recipient-auditor +// encrypted amount. +// O_a4 r_tilde_aud_r = r_tx + m_r_r Recipient-auditor +// encrypted transfer +// randomness. +// O_a5 S_{a,s} = r_e * K_aud_s Owner-auditor ECDH +// shared secret +// (reuses r_e); the +// sender-channel +// visibility points +// at the funds' +// OWNER, not the +// operator (Section +// 8.4). +// O_a6 (m_v_s, m_a_s) +// = SpongeSqueeze_2(delta_aud_s, +// S_{a,s}.x, sigma_a) Owner-channel +// sponge: two masks. +// O_a7 v_tilde_aud_s = v_tx + m_v_s Owner-auditor +// encrypted amount. +// O_a8 a_tilde_aud_s = (v_a - v_tx) + m_a_s Owner-auditor +// encrypted +// post-transfer +// allowance. +// +// Wrapper binding (Section 7.8 final paragraph) +// --------------------------------------------- +// Unlike owner-initiated circuits, OperatorTransfer does NOT constrain the vk +// derivation -- the operator has no access to the owner's sk. Wrapper binding +// is inherited indirectly through the allowance commitment chain: SetOperator +// derived `dvk_i` from the wrapper-specific `vk` (S2, S5), which determined +// `r_a` (S6) and thus `C_a` (S7). Here O3 verifies the operator's claimed +// `dvk_i` against the on-chain `C_a` via `sigma_a`. Because `C_a` is a public +// input and was constructed with wrapper-specific randomness, a proof +// generated against one wrapper's `C_a` cannot verify against another's. +// +// Channel-nonce reuse (Section 6.2 *Dual role*) +// --------------------------------------------- +// `sigma_a` serves as the freshness nonce for THREE distinct uses in this +// circuit: the allowance-randomness Poseidon (O3), the recipient ECDH chain +// (O7 / O9), and both auditor-channel sponges (O_a2 / O_a6). Soundness derives +// from ECDH shared-secret unpredictability (S.x is unknown to anyone but the +// recipient and the prover); `sigma_a` itself is public and need not be +// secret. Only the new-allowance constraints O10 and O12 use the fresh +// `sigma_a'`. +// +// Point-validation doctrine (Section 10.8) +// ---------------------------------------- +// Y_op, C_a, C_a', C_tx, R_e, S, S_{a,r}, and S_{a,s} are bound to in-circuit +// multi_scalar_mul outputs (O1, O2, O11, O8, O6, O5, O_a1, O_a5) and are +// therefore on-curve by construction -- no explicit check needed. +// PVK_recipient was constrained on-curve when the recipient registered (R3, +// Section 7.2) and is loaded by the wrapper from trusted storage, so it is +// path (2) of Section 10.8 and the circuit consumes it without re-checking. +// K_aud_r and K_aud_s are public-input keys with no proof constraint at +// insertion (path (3)) -- the verifier doesn't check curve membership and +// an off-curve key would break the soundness of O_a1 / O_a5. This file +// explicitly validates both on-curve AND non-identity before the +// corresponding ECDH consumes them. +// +// Public inputs (24 fields, in design-doc canonical order) +// -------------------------------------------------------- +// Idx Param Symbol Source / Note +// --- ----- ------ ------------------------------- +// 0 c_a_x C_a.x Loaded from the +// 1 c_a_y C_a.y `(from, operator)` delegation +// entry's allowance_commitment. +// 2 sigma_a sigma_a Loaded from the delegation +// entry's allowance_salt. +// 3 y_op_x Y_op.x Loaded from +// 4 y_op_y Y_op.y `operator.spending_key`; +// matches the auth principal. +// 5 pvk_recipient_x PVK_recipient.x Loaded from +// 6 pvk_recipient_y PVK_recipient.y `to.viewing_public_key`. +// Recipient must be registered. +// 7 k_aud_r_x K_aud_r.x Fetched from the auditor +// 8 k_aud_r_y K_aud_r.y contract by `to.auditor_id`. +// 9 k_aud_s_x K_aud_s.x Fetched from the auditor +// 10 k_aud_s_y K_aud_s.y contract by the OWNER's +// `auditor_id`, not the +// operator's (Section 7.8). +// 11 c_a_new_x C_a'.x Prover-supplied; written to +// 12 c_a_new_y C_a'.y the delegation entry's new +// allowance_commitment. +// 13 c_tx_x C_tx.x Prover-supplied; added to +// 14 c_tx_y C_tx.y `to.receiving_balance`. +// 15 r_e_x R_e.x Prover-supplied ephemeral key; +// 16 r_e_y R_e.y emitted. +// 17 v_tilde v_tilde Prover-supplied encrypted +// transfer amount; emitted. +// 18 a_tilde_new a_tilde' Prover-supplied encrypted new +// allowance scalar; stored. +// 19 sigma_a_new sigma_a' Prover-supplied fresh salt; +// written to allowance_salt. +// 20 v_tilde_aud_r v_tilde_aud_r Prover-supplied recipient- +// auditor encrypted amount; +// emitted. +// 21 r_tilde_aud_r r_tilde_aud_r Prover-supplied recipient- +// auditor encrypted transfer +// randomness; emitted. +// 22 v_tilde_aud_s v_tilde_aud_s Prover-supplied owner-auditor +// encrypted amount; emitted. +// 23 a_tilde_aud_s a_tilde_aud_s Prover-supplied owner-auditor +// encrypted post-transfer +// allowance; emitted. +// +// Private witnesses +// ----------------- +// sk_op Operator's spending secret scalar. +// dvk_i Delegation viewing key for this (owner, operator) pair, pinned by +// O3 to Poseidon2(delta_allow_r, dvk_i, sigma_a) = r_a. +// v_a Plaintext current allowance value. +// r_a Plaintext blinding factor for C_a (single-limb F_r; pinned by O3). +// v_tx Plaintext transfer amount. +// r_e Ephemeral scalar for recipient + auditor ECDH; must satisfy +// r_e != 0 (O13). + +fn main( + sk_op: Field, + dvk_i: Field, + v_a: Field, + r_a: Field, + v_tx: Field, + r_e: Field, + c_a_x: pub Field, + c_a_y: pub Field, + sigma_a: pub Field, + y_op_x: pub Field, + y_op_y: pub Field, + pvk_recipient_x: pub Field, + pvk_recipient_y: pub Field, + k_aud_r_x: pub Field, + k_aud_r_y: pub Field, + k_aud_s_x: pub Field, + k_aud_s_y: pub Field, + c_a_new_x: pub Field, + c_a_new_y: pub Field, + c_tx_x: pub Field, + c_tx_y: pub Field, + r_e_x: pub Field, + r_e_y: pub Field, + v_tilde: pub Field, + a_tilde_new: pub Field, + sigma_a_new: pub Field, + v_tilde_aud_r: pub Field, + r_tilde_aud_r: pub Field, + v_tilde_aud_s: pub Field, + a_tilde_aud_s: pub Field, +) { + // O13 -- runs first so the r_e = 0 attack is rejected before any + // scalar mul against it could quietly produce the identity. + assert(r_e != 0); + + // O1 + let y_op_derived = scalar_mul(sk_op, H); + assert(y_op_derived.x == y_op_x); + assert(y_op_derived.y == y_op_y); + + // O3 -- runs before O2 so the wrapper-binding check (r_a is pinned to + // dvk_i, sigma_a) is in scope before O2 uses r_a as the blinding under + // the Pedersen commitment. + let r_a_derived = derive_allow_r(dvk_i, sigma_a); + assert(r_a_derived == r_a); + + // O2 + let c_a_derived = commit(v_a, r_a); + assert(c_a_derived.x == c_a_x); + assert(c_a_derived.y == c_a_y); + + // O4 -- Section 2.6 spells out the 127-bit decomposition / recomposition + // pattern; `assert_max_bit_size` is the Noir stdlib primitive that + // implements it directly. + v_a.assert_max_bit_size::<127>(); + v_tx.assert_max_bit_size::<127>(); + let v_a_new = v_a - v_tx; + v_a_new.assert_max_bit_size::<127>(); + + // O6 + let r_e_derived = scalar_mul(r_e, H); + assert(r_e_derived.x == r_e_x); + assert(r_e_derived.y == r_e_y); + + // O5 -- PVK_recipient is path (2) of Section 10.8 (proof-constrained at + // the recipient's registration via R3, trusted on read). + let pvk_recipient = + EmbeddedCurvePoint { x: pvk_recipient_x, y: pvk_recipient_y, is_infinite: false }; + let s_x = ecdh(r_e, pvk_recipient); + + // O7 (anti-poisoning, Section 5.4) + let r_tx = derive_tx_blind(s_x, sigma_a); + + // O8 + let c_tx_derived = commit(v_tx, r_tx); + assert(c_tx_derived.x == c_tx_x); + assert(c_tx_derived.y == c_tx_y); + + // O9 + let v_tilde_derived = encrypt_amount(v_tx, s_x, sigma_a); + assert(v_tilde_derived == v_tilde); + + // Salt rotation (implementation hardening). + assert(sigma_a_new != sigma_a); + + // O10 + let r_a_new = derive_allow_r(dvk_i, sigma_a_new); + + // O11 + let c_a_new_derived = commit(v_a_new, r_a_new); + assert(c_a_new_derived.x == c_a_new_x); + assert(c_a_new_derived.y == c_a_new_y); + + // O12 + let a_tilde_new_derived = encrypt_allowance(v_a_new, dvk_i, sigma_a_new); + assert(a_tilde_new_derived == a_tilde_new); + + // K_aud_r / K_aud_s point validation (Section 10.8: public-input keys). + let k_aud_r = EmbeddedCurvePoint { x: k_aud_r_x, y: k_aud_r_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_r); + let k_aud_s = EmbeddedCurvePoint { x: k_aud_s_x, y: k_aud_s_y, is_infinite: false }; + assert_on_curve_non_identity(k_aud_s); + + // O_a1 (recipient-auditor shared secret x-coordinate) + let s_a_r_x = ecdh(r_e, k_aud_r); + + // O_a2 (recipient-channel masks: amount, then r_tx) + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, sigma_a); + + // O_a3 + assert(v_tx + m_r[0] == v_tilde_aud_r); + + // O_a4 + assert(r_tx + m_r[1] == r_tilde_aud_r); + + // O_a5 (owner-auditor shared secret x-coordinate) + let s_a_s_x = ecdh(r_e, k_aud_s); + + // O_a6 (owner-channel masks: amount, then post-transfer allowance) + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, sigma_a); + + // O_a7 + assert(v_tx + m_s[0] == v_tilde_aud_s); + + // O_a8 + assert(v_a_new + m_s[1] == a_tilde_aud_s); +} diff --git a/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr new file mode 100644 index 000000000..b31b5d887 --- /dev/null +++ b/packages/tokens/src/confidential/circuits/operator_transfer/src/tests.nr @@ -0,0 +1,1502 @@ +use crate::main; +use stellar_confidential_lib::{ + commit, derive_allow_r, derive_tx_blind, domain, ecdh, encrypt_allowance, encrypt_amount, H, + scalar_mul, sponge_squeeze_2, +}; +use std::embedded_curve_ops::EmbeddedCurvePoint; + +// Canonical fixture inputs. +// +// SIGMA_A is the channel nonce reused across the allowance Poseidon (O3), +// the recipient ECDH chain (O7 / O9), and both auditor sponges (O_a2 / O_a6) +// -- see main.nr "Channel-nonce reuse". SIGMA_A_NEW is the fresh salt for the +// post-transfer allowance derivations only (O10, O12). V_A / V_TX / V_A_NEW +// give a non-trivial `v_a - v_tx` so O4 is exercised on a real subtraction. +// +// Y_OP_X / Y_OP_Y and the auditor / recipient keys share fixture values with +// withdraw/transfer (sk_op = 0xdead, K_aud_s/r and pvk scalars identical to +// transfer) so the three circuits share an ecosystem of pinned curve points +// for cross-circuit tooling. DVK_I is a placeholder field; in production it +// comes from S5 of SetOperator and the operator decrypts it from +// `escrowed_dvk` (Section 7.11). +global SK_OP: Field = 0xdead; +global DVK_I: Field = 0x44767669; // ASCII "Dvki" +global SIGMA_A: Field = 0x01; +global SIGMA_A_NEW: Field = 0x02; +global V_A: Field = 1000; +global V_TX: Field = 100; +global V_A_NEW: Field = 900; +global R_E: Field = 0xfeedface; + +// Placeholder recipient viewing-key scalar; PVK_RECIPIENT = VK_RECIPIENT * H. +global VK_RECIPIENT: Field = 0xfeed; +// Placeholder auditor secret scalars; K_aud_{r,s} = scalar * H. Distinct so +// the dual-channel separation is exercised by the happy path; both shared +// with the transfer fixtures for cross-circuit tooling consistency. +global K_AUD_R_SCALAR: Field = 0xc0ffee01; +global K_AUD_S_SCALAR: Field = 0xc0ffee; + +// Y_op = sk_op * H (= lib `scalar_mul_Y` fixture for sk = 0xdead). +global Y_OP_X: Field = 0x1b46b003b88a6c34549dc74115f088f4b231a151397526bc10cbf1d15b457646; +global Y_OP_Y: Field = 0x29116280600c10ead1fdbd9ab4b571896030679bf554d7f1ebf681e5147de21b; + +// All values below are pinned by `operator_transfer_fixtures_match_lib` and +// `operator_transfer_auditor_fixtures_match_lib`, which re-derive them from +// the lib primitives and assert the values declared here -- if either side +// drifts the test fails immediately. They are emitted by `print_fixtures`. +global R_A: Field = 0x1528d8006892ad6b484d6f18381c687304d6b4cb9a215992918dd601ad4acf9d; +global C_A_X: Field = 0x1d8739d710d3cc5188faa1e64058cb4986513f46edbbe17c33e2a25da2bb657f; +global C_A_Y: Field = 0x142b6a3fa6772624471dd3e938bbff4a58a17f8bee976a5fcc949c8f487ec209; +global PVK_RECIPIENT_X: Field = + 0x176667da789ef2e73e193b32ff0198687cadb0e5d1843fa8ca3140cbf352aaf0; +global PVK_RECIPIENT_Y: Field = + 0x017ad8f680161d399bcb7034881f3cd6288f99138d51bddf3331a92257698b85; +global K_AUD_R_X: Field = 0x07782f9cf649fcc3690b758cd826426732ae8dad766ec62860ca0330d8b0e3fe; +global K_AUD_R_Y: Field = 0x26a06cc59b1da39f1c50a1deae9bb5289b36c0350a9942ac4bf5a6079717dbd8; +global K_AUD_S_X: Field = 0x22502c7b20b64aeaaaa4d4fa5f2f8600f9734e828f7284a8d1431da36b33803a; +global K_AUD_S_Y: Field = 0x0419089353d24334f03f4a1c9c9b19282e32f6879c5c69b9cdffba6a13a7c5ed; +global C_A_NEW_X: Field = 0x2d19aa5933ba41ebadb28d4c784ca4e405a1a6efcb5cf15e2c8d5e9a0552f178; +global C_A_NEW_Y: Field = 0x0214719083236d5ecfc551fbc2b6d04fa8c95b8986fcfc7bbb8c0ff3cecd3dd1; +global C_TX_X: Field = 0x1679ef51dbb01793592348ed9dcc453f82aba5d4777d786ea09979e06b965dfc; +global C_TX_Y: Field = 0x1ec2eb1cc4b0cfd172637f34166074f3e15e6620d814cc22bae87eeb2a40270d; +global R_E_X: Field = 0x114ed4fcf2c57014eb678c577aa02f30ef590b713d7a6a5e87702d1c7f71957f; +global R_E_Y: Field = 0x07a70cf826350d4f438c7a3c5e8761b0ae6cb63de757f0c96815f4057b9205f4; +global V_TILDE: Field = 0x2861fc81fda926afd33517cb5509df867f324abf81c82ab9d7eca9a94a16f07b; +global A_TILDE_NEW: Field = + 0x1d2e286a8d510a7c4164d0142ceea3202a957d49248426435f311a348f681147; +global V_TILDE_AUD_R: Field = + 0x2d80e4f8a46472bb837956b13028b7d95979a8f5e0f657c50a7d6721f9a19f66; +global R_TILDE_AUD_R: Field = + 0x0b9fb0b33e05f9c93da3802075248e25b59f1b2416c997ffbedd6cd71e1c1385; +global V_TILDE_AUD_S: Field = + 0x0a607feb31e56e89ed94a3e1dc3811d3d23aaf10b2f27874ce670878b9f6ee19; +global A_TILDE_AUD_S: Field = + 0x0b586defafdf4583d0f338855fdf8391102adaac4634a3b80594aea6f788e1cb; + +#[test] +fn print_fixtures() { + // One-shot harness: prints every derived public input for the chosen + // fixture tuple. Run with + // `nargo test --package circuit_operator_transfer print_fixtures --show-output` + // and paste the values into the globals above. + let y_op = scalar_mul(SK_OP, H); + let r_a = derive_allow_r(DVK_I, SIGMA_A); + let c_a = commit(V_A, r_a); + + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let k_aud_r = scalar_mul(K_AUD_R_SCALAR, H); + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_TX, r_tx); + let v_tilde = encrypt_amount(V_TX, s_x, SIGMA_A); + + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A_NEW); + + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let v_tilde_aud_r = V_TX + m_r[0]; + let r_tilde_aud_r = r_tx + m_r[1]; + + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + let v_tilde_aud_s = V_TX + m_s[0]; + let a_tilde_aud_s = V_A_NEW + m_s[1]; + + let yopx = y_op.x; + let yopy = y_op.y; + let cax = c_a.x; + let cay = c_a.y; + let pvkx = pvk_recipient.x; + let pvky = pvk_recipient.y; + let kr_x = k_aud_r.x; + let kr_y = k_aud_r.y; + let ks_x = k_aud_s.x; + let ks_y = k_aud_s.y; + let canx = c_a_new.x; + let cany = c_a_new.y; + let ctxx = c_tx.x; + let ctxy = c_tx.y; + let rex = r_e_pt.x; + let rey = r_e_pt.y; + println(f"Y_OP_X = {yopx}"); + println(f"Y_OP_Y = {yopy}"); + println(f"R_A = {r_a}"); + println(f"C_A_X = {cax}"); + println(f"C_A_Y = {cay}"); + println(f"PVK_RECIPIENT_X = {pvkx}"); + println(f"PVK_RECIPIENT_Y = {pvky}"); + println(f"K_AUD_R_X = {kr_x}"); + println(f"K_AUD_R_Y = {kr_y}"); + println(f"K_AUD_S_X = {ks_x}"); + println(f"K_AUD_S_Y = {ks_y}"); + println(f"C_A_NEW_X = {canx}"); + println(f"C_A_NEW_Y = {cany}"); + println(f"C_TX_X = {ctxx}"); + println(f"C_TX_Y = {ctxy}"); + println(f"R_E_X = {rex}"); + println(f"R_E_Y = {rey}"); + println(f"V_TILDE = {v_tilde}"); + println(f"A_TILDE_NEW = {a_tilde_new}"); + println(f"V_TILDE_AUD_R = {v_tilde_aud_r}"); + println(f"R_TILDE_AUD_R = {r_tilde_aud_r}"); + println(f"V_TILDE_AUD_S = {v_tilde_aud_s}"); + println(f"A_TILDE_AUD_S = {a_tilde_aud_s}"); +} + +#[test] +fn operator_transfer_fixtures_match_lib() { + let y_op = scalar_mul(SK_OP, H); + let r_a = derive_allow_r(DVK_I, SIGMA_A); + let c_a = commit(V_A, r_a); + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_TX, r_tx); + let v_tilde = encrypt_amount(V_TX, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A_NEW); + + assert(y_op.x == Y_OP_X); + assert(y_op.y == Y_OP_Y); + assert(r_a == R_A); + assert(c_a.x == C_A_X); + assert(c_a.y == C_A_Y); + assert(pvk_recipient.x == PVK_RECIPIENT_X); + assert(pvk_recipient.y == PVK_RECIPIENT_Y); + assert(c_tx.x == C_TX_X); + assert(c_tx.y == C_TX_Y); + assert(v_tilde == V_TILDE); + assert(c_a_new.x == C_A_NEW_X); + assert(c_a_new.y == C_A_NEW_Y); + assert(a_tilde_new == A_TILDE_NEW); +} + +#[test] +fn operator_transfer_auditor_fixtures_match_lib() { + let k_aud_r = scalar_mul(K_AUD_R_SCALAR, H); + let k_aud_s = scalar_mul(K_AUD_S_SCALAR, H); + let r_e_pt = scalar_mul(R_E, H); + let pvk_recipient = scalar_mul(VK_RECIPIENT, H); + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + assert(k_aud_r.x == K_AUD_R_X); + assert(k_aud_r.y == K_AUD_R_Y); + assert(k_aud_s.x == K_AUD_S_X); + assert(k_aud_s.y == K_AUD_S_Y); + assert(r_e_pt.x == R_E_X); + assert(r_e_pt.y == R_E_Y); + assert(V_TX + m_r[0] == V_TILDE_AUD_R); + assert(r_tx + m_r[1] == R_TILDE_AUD_R); + assert(V_TX + m_s[0] == V_TILDE_AUD_S); + assert(V_A_NEW + m_s[1] == A_TILDE_AUD_S); +} + +// `run_main` accepts every public input as a parameter -- nothing baked in. +// Tests that target a specific constraint construct the public tuple so only +// that constraint can fail: tamper the witness or public input under test +// AND recompute every downstream public that depends on it, so no cascaded +// mismatch coincidentally lights up the same `should_fail`. +fn run_main( + sk_op_in: Field, + dvk_i_in: Field, + v_a_in: Field, + r_a_in: Field, + v_tx_in: Field, + r_e_in: Field, + c_a_x_in: Field, + c_a_y_in: Field, + sigma_a_in: Field, + y_op_x_in: Field, + y_op_y_in: Field, + pvk_recipient_x_in: Field, + pvk_recipient_y_in: Field, + k_aud_r_x_in: Field, + k_aud_r_y_in: Field, + k_aud_s_x_in: Field, + k_aud_s_y_in: Field, + c_a_new_x_in: Field, + c_a_new_y_in: Field, + c_tx_x_in: Field, + c_tx_y_in: Field, + r_e_x_in: Field, + r_e_y_in: Field, + v_tilde_in: Field, + a_tilde_new_in: Field, + sigma_a_new_in: Field, + v_tilde_aud_r_in: Field, + r_tilde_aud_r_in: Field, + v_tilde_aud_s_in: Field, + a_tilde_aud_s_in: Field, +) { + main( + sk_op_in, + dvk_i_in, + v_a_in, + r_a_in, + v_tx_in, + r_e_in, + c_a_x_in, + c_a_y_in, + sigma_a_in, + y_op_x_in, + y_op_y_in, + pvk_recipient_x_in, + pvk_recipient_y_in, + k_aud_r_x_in, + k_aud_r_y_in, + k_aud_s_x_in, + k_aud_s_y_in, + c_a_new_x_in, + c_a_new_y_in, + c_tx_x_in, + c_tx_y_in, + r_e_x_in, + r_e_y_in, + v_tilde_in, + a_tilde_new_in, + sigma_a_new_in, + v_tilde_aud_r_in, + r_tilde_aud_r_in, + v_tilde_aud_s_in, + a_tilde_aud_s_in, + ); +} + +fn run_fixture() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test] +fn matches_fixture() { + run_fixture(); +} + +#[test] +fn second_transfer_against_same_allowance() { + // Second happy-path point: same C_a (= same dvk_i, same v_a, same + // sigma_a) with a different v_tx. Exercises the case where the operator + // hasn't yet rotated the allowance commitment but produces a fresh + // sigma_a_new and a fresh r_e on every call. The transfer-side ECDH + // chain (O5..O9) is fully rederived; only O11/O12 share dvk_i + the new + // sigma_a_new with the canonical fixture. + let v_tx_2: Field = 250; + let v_a_new_2: Field = V_A - v_tx_2; // 750 + let sigma_a_new_2: Field = 0x03; + let r_e_2: Field = 0xfacecafe; + + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + + let s_x = ecdh(r_e_2, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_2, r_tx); + let v_tilde_2 = encrypt_amount(v_tx_2, s_x, SIGMA_A); + let r_a_new_2 = derive_allow_r(DVK_I, sigma_a_new_2); + let c_a_new_2 = commit(v_a_new_2, r_a_new_2); + let a_tilde_new_2 = encrypt_allowance(v_a_new_2, DVK_I, sigma_a_new_2); + let r_e_2_pt = scalar_mul(r_e_2, H); + let s_a_r_x = ecdh(r_e_2, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(r_e_2, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_2, + r_e_2, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_2.x, + c_a_new_2.y, + c_tx.x, + c_tx.y, + r_e_2_pt.x, + r_e_2_pt.y, + v_tilde_2, + a_tilde_new_2, + sigma_a_new_2, + v_tx_2 + m_r[0], + r_tx + m_r[1], + v_tx_2 + m_s[0], + v_a_new_2 + m_s[1], + ); +} + +#[test] +fn full_allowance_transfer() { + // Boundary: v_tx = v_a, so v_a_new = 0. C_a' commits to (0, r_a_new'), + // which is a valid commitment to zero. a_tilde' and a_tilde_aud_s both + // bind to 0 (plus mask) so the owner's auditor sees the post-transfer + // allowance reach zero. The delegation entry isn't deleted (that's + // revoke_operator's job, Section 7.9) -- it now escrows zero value. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(V_A, r_tx); + let v_tilde = encrypt_amount(V_A, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(0, r_a_new); + let a_tilde_new = encrypt_allowance(0, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_A, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new, + SIGMA_A_NEW, + V_A + m_r[0], + r_tx + m_r[1], + V_A + m_s[0], + 0 + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_sk_op() { + // O1 fires: scalar_mul(SK_OP + 1, H) != Y_op. + run_main( + SK_OP + 1, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_dvk_against_wrapper() { + // Wrapper-binding regression. The OperatorTransfer circuit does NOT carry + // a vk derivation -- the operator never sees the owner's sk -- so a naive + // reading might worry that an operator could reuse a proof's witness + // across wrappers. The defense lives in O3. + // + // Mechanism (Section 7.8 final paragraph): + // * SetOperator derived dvk_i from a wrapper-specific vk: S2/S5 chain + // vk = Poseidon2(VK, sk_owner, wrap), then dvk_i = Poseidon2(DVK, vk, + // op_i). A different `wrap` produces a different vk, and therefore a + // different dvk_i. + // * That dvk_i then determined r_a (S6: r_a = Poseidon2(ALLOW_R, dvk_i, + // sigma_a)) and thereby C_a (S7). + // * On the OperatorTransfer side, C_a and sigma_a are PUBLIC inputs. + // O3 forces the prover's witness dvk_i to satisfy r_a == + // Poseidon2(ALLOW_R, dvk_i, sigma_a), and O2 then forces commit(v_a, + // r_a) == C_a. The combination pins dvk_i: only the dvk_i that + // SetOperator produced under that wrapper's vk admits an opening of + // C_a. + // + // This test exercises the failure mode. We keep every public input + // canonical (so C_a, sigma_a, Y_op come from the original wrapper) but + // hand the prover a `dvk_i` from a DIFFERENT wrapper -- i.e., one that + // would have been produced if vk had been derived under wrap_bad rather + // than wrap. With a different dvk_i, derive_allow_r(dvk_i_bad, SIGMA_A) + // yields a different r_a, so the prover-supplied R_A (also derived from + // dvk_i_bad and supplied to keep O3 internally consistent) no longer + // opens C_a under commit(V_A, R_A) -- O2 fails. Alternatively if we + // leave R_A canonical, O3 fails directly. The path here patches the + // private witness `dvk_i` only and keeps the canonical R_A; that + // triggers O3 immediately (r_a_derived != R_A). + // + // Why O3 is the load-bearing constraint: O3 is the *only* in-circuit + // tie between the wrapper-specific dvk_i (chosen at SetOperator) and + // the rest of the OperatorTransfer witness. Without it the operator + // could substitute any dvk_i, satisfying O2 with a re-randomization of + // r_a, and verify a proof against a C_a from a different wrapper. + let dvk_i_bad: Field = DVK_I + 1; + run_main( + SK_OP, + dvk_i_bad, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_allowance_opening() { + // O2 fires: commit(V_A + 1, R_A) != C_a. v_a_new cascades into O11 / O12 + // / O_a8; recompute c_a_new, a_tilde_new, and a_tilde_aud_s against the + // tampered v_a_new so only O2 fires. + let v_a_bad: Field = V_A + 1; + let v_a_new_bad: Field = v_a_bad - V_TX; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new_bad = commit(v_a_new_bad, r_a_new); + let a_tilde_new_bad = encrypt_allowance(v_a_new_bad, DVK_I, SIGMA_A_NEW); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + v_a_bad, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_bad.x, + c_a_new_bad.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new_bad, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + v_a_new_bad + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_a_new_out_of_range() { + // v_tx > v_a: v_a - v_tx underflows in the field to a value >= 2^128. + // O4's `v_a_new` range check fires. Every downstream public is + // recomputed against v_tx_too_large so the single firing constraint is + // O4's v_a_new range check (the v_a and v_tx individual sub-checks pass: + // V_A is in range and v_tx_too_large = V_A + 1 = 1001 is also in range). + let v_tx_too_large: Field = V_A + 1; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_too_large, r_tx); + let v_tilde = encrypt_amount(v_tx_too_large, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new_invalid = commit(V_A - v_tx_too_large, r_a_new); + let a_tilde_new_invalid = encrypt_allowance(V_A - v_tx_too_large, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_too_large, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new_invalid.x, + c_a_new_invalid.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new_invalid, + SIGMA_A_NEW, + v_tx_too_large + m_r[0], + r_tx + m_r[1], + v_tx_too_large + m_s[0], + (V_A - v_tx_too_large) + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_a_out_of_range() { + // 2^127 is exactly the boundary O4 rules out (Section 2.6 / 3.4 -- SEP-41 + // i128 non-negative range is [0, 2^127), so 2^127 is the smallest value + // that must fail). With v_a_huge - V_TX = 2^127 - 100 still in [0, 2^127), + // the v_a_new range check passes; only the `v_a.assert_max_bit_size<127>()` + // sub-check of O4 fires. C_a, c_a_new, and a_tilde_aud_s are all + // recomputed against v_a_huge / v_a_new_huge so O2 / O11 / O12 / O_a8 + // cannot coincidentally fire. R_A is independent of v_a so it stays + // canonical. + let v_a_huge: Field = 0x80000000000000000000000000000000; + let v_a_new_huge: Field = v_a_huge - V_TX; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let c_a = commit(v_a_huge, R_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(v_a_new_huge, r_a_new); + let a_tilde_new = encrypt_allowance(v_a_new_huge, DVK_I, SIGMA_A_NEW); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + v_a_huge, + R_A, + V_TX, + R_E, + c_a.x, + c_a.y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + v_a_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_v_tx_out_of_range() { + // v_tx = 2^127: O4's range check on v_tx fires. Every downstream public + // that depends on v_tx (C_tx, v_tilde, v_tilde_aud_r, v_tilde_aud_s) and + // on v_a_new (c_a_new, a_tilde_new, a_tilde_aud_s) is recomputed so the + // only constraints that fail are O4's range sub-checks. + let v_tx_huge: Field = 0x80000000000000000000000000000000; + let v_a_new_huge: Field = V_A - v_tx_huge; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx = derive_tx_blind(s_x, SIGMA_A); + let c_tx = commit(v_tx_huge, r_tx); + let v_tilde = encrypt_amount(v_tx_huge, s_x, SIGMA_A); + let r_a_new = derive_allow_r(DVK_I, SIGMA_A_NEW); + let c_a_new = commit(v_a_new_huge, r_a_new); + let a_tilde_new = encrypt_allowance(v_a_new_huge, DVK_I, SIGMA_A_NEW); + let s_a_r_x = ecdh(R_E, k_aud_r); + let m_r = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x, SIGMA_A); + let s_a_s_x = ecdh(R_E, k_aud_s); + let m_s = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + v_tx_huge, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + c_tx.x, + c_tx.y, + R_E_X, + R_E_Y, + v_tilde, + a_tilde_new, + SIGMA_A_NEW, + v_tx_huge + m_r[0], + r_tx + m_r[1], + v_tx_huge + m_s[0], + v_a_new_huge + m_s[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_recipient_pvk() { + // PVK_recipient replaced by Y_op (a valid on-curve point that isn't the + // registered recipient's PVK). S = r_e * PVK_recipient changes, so r_tx + // and v_tilde drift and O8 fails (C_tx no longer matches commit(v_tx, + // r_tx_new)). + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + Y_OP_X, + Y_OP_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_poisoned_c_tx() { + // Anti-poisoning (Section 5.4): a prover that commits C_tx with arbitrary + // blinding -- here r_tx + 1 -- desynchronizes the recipient's accumulated + // blinding. O7 + O8 together close this attack: r_tx is uniquely + // determined by Poseidon2(delta_tx_blind, S.x, sigma_a), so any C_tx not + // built from that exact r_tx fails O8. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let s_x = ecdh(R_E, pvk_recipient); + let r_tx_poisoned = derive_tx_blind(s_x, SIGMA_A) + 1; + let c_tx_poisoned = commit(V_TX, r_tx_poisoned); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_poisoned.x, + c_tx_poisoned.y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde() { + // O9 fires: v_tilde + 1 no longer matches v_tx + Poseidon2(TX_AMOUNT, ...). + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE + 1, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +// O10 (`r_a' = Poseidon2(delta_allow_r, dvk_i, sigma_a')`) is exercised +// transitively by the two tests below: a tampered `c_a_new` is detected by +// O11 (which consumes `r_a_new`), and a tampered `a_tilde_new` is detected +// by O12 (which consumes `dvk_i` and `sigma_a'` directly). `dvk_i` is the +// single shared witness between O3 and O10, so isolating O10 with its own +// dedicated negative test would require a second `dvk_i` witness that +// doesn't exist in the circuit. + +#[test(should_fail)] +fn rejects_tampered_a_tilde_new() { + // O12 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW + 1, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_c_a_new() { + // O11 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X + 1, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_sigma_a_new_equal_to_sigma_a() { + // Salt-rotation hardening: the circuit rejects `sigma_a' == sigma_a` + // even though Section 7.8 does not require it. With `sigma_a' = sigma_a`: + // r_a_new = derive_allow_r(dvk_i, sigma_a_new) = r_a + // c_a_new = (V_A - V_TX) * G + r_a * H + // a_tilde_new = encrypt_allowance(V_A - V_TX, dvk_i, sigma_a_new) + // All downstream publics are recomputed consistently so the only + // firing constraint is `assert(sigma_a_new != sigma_a)`. + let r_a_new = derive_allow_r(DVK_I, SIGMA_A); + let c_a_new = commit(V_A_NEW, r_a_new); + let a_tilde_new = encrypt_allowance(V_A_NEW, DVK_I, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + c_a_new.x, + c_a_new.y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + a_tilde_new, + SIGMA_A, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_r_e_zero() { + // O13 fires: r_e = 0 would force R_e = O (identity) and collapse every + // ECDH in this transfer (S, S_{a,r}, S_{a,s} all become O), making every + // mask a knowable constant function of sigma_a. Every r_e-dependent + // public (R_e = (0, 0) identity-encoding, C_tx / v_tilde derived against + // s_x = 0, and both auditor channels' ciphertexts derived against + // s_a_{r,s}.x = 0) is recomputed so the `assert(r_e != 0)` check is the + // only constraint that can catch this -- removing O13 would let the + // proof verify. + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x_zero = ecdh(0, pvk_recipient); + let r_tx_zero = derive_tx_blind(s_x_zero, SIGMA_A); + let c_tx_zero = commit(V_TX, r_tx_zero); + let v_tilde_zero = encrypt_amount(V_TX, s_x_zero, SIGMA_A); + let s_a_r_x_zero = ecdh(0, k_aud_r); + let m_r_zero = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x_zero, SIGMA_A); + let s_a_s_x_zero = ecdh(0, k_aud_s); + let m_s_zero = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_zero, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + 0, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_zero.x, + c_tx_zero.y, + 0, + 0, + v_tilde_zero, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TX + m_r_zero[0], + r_tx_zero + m_r_zero[1], + V_TX + m_s_zero[0], + V_A_NEW + m_s_zero[1], + ); +} + +#[test(should_fail)] +fn rejects_wrong_r_e() { + // r_e mismatches the public R_e: O6 fails because scalar_mul(r_e+1, H) + // no longer equals the canonical (R_E_X, R_E_Y). R_e itself is left + // canonical (so O6 catches the mismatch) but every other r_e-dependent + // public (C_tx, v_tilde, both auditor channels) is recomputed against + // r_e+1 so O8 / O9 / O_a3 / O_a4 / O_a7 / O_a8 cannot coincidentally + // fire. + let r_e_bad: Field = R_E + 1; + let pvk_recipient = + EmbeddedCurvePoint { x: PVK_RECIPIENT_X, y: PVK_RECIPIENT_Y, is_infinite: false }; + let k_aud_r = EmbeddedCurvePoint { x: K_AUD_R_X, y: K_AUD_R_Y, is_infinite: false }; + let k_aud_s = EmbeddedCurvePoint { x: K_AUD_S_X, y: K_AUD_S_Y, is_infinite: false }; + let s_x_bad = ecdh(r_e_bad, pvk_recipient); + let r_tx_bad = derive_tx_blind(s_x_bad, SIGMA_A); + let c_tx_bad = commit(V_TX, r_tx_bad); + let v_tilde_bad = encrypt_amount(V_TX, s_x_bad, SIGMA_A); + let s_a_r_x_bad = ecdh(r_e_bad, k_aud_r); + let m_r_bad = sponge_squeeze_2(domain::AUDITOR_RECIPIENT, s_a_r_x_bad, SIGMA_A); + let s_a_s_x_bad = ecdh(r_e_bad, k_aud_s); + let m_s_bad = sponge_squeeze_2(domain::AUDITOR_SENDER, s_a_s_x_bad, SIGMA_A); + + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + r_e_bad, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + c_tx_bad.x, + c_tx_bad.y, + R_E_X, + R_E_Y, + v_tilde_bad, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TX + m_r_bad[0], + r_tx_bad + m_r_bad[1], + V_TX + m_s_bad[0], + V_A_NEW + m_s_bad[1], + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_r() { + // (1, 2) is off-curve. The on-curve check on K_aud_r fires before O_a1 + // ever consumes it. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + 1, + 2, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_r() { + // K_aud_r = identity collapses ECDH (S_{a,r} = r_e * O = O), so the + // on-curve check explicitly rejects the identity for keys. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + 0, + 0, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_off_curve_k_aud_s() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + 1, + 2, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_identity_k_aud_s() { + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + 0, + 0, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde_aud_r() { + // O_a3 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R + 1, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_r_tilde_aud_r() { + // O_a4 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R + 1, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_v_tilde_aud_s() { + // O_a7 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S + 1, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_tampered_a_tilde_aud_s() { + // O_a8 fires. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S + 1, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_r() { + // K_aud_r replaced by a valid but different on-curve key (Y_op). Shared + // secret changes, recipient-channel masks change, O_a3 fails. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + Y_OP_X, + Y_OP_Y, + K_AUD_S_X, + K_AUD_S_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} + +#[test(should_fail)] +fn rejects_wrong_k_aud_s() { + // K_aud_s replaced by a valid but different on-curve key (Y_op). Shared + // secret changes, owner-channel masks change, O_a7 fails. + run_main( + SK_OP, + DVK_I, + V_A, + R_A, + V_TX, + R_E, + C_A_X, + C_A_Y, + SIGMA_A, + Y_OP_X, + Y_OP_Y, + PVK_RECIPIENT_X, + PVK_RECIPIENT_Y, + K_AUD_R_X, + K_AUD_R_Y, + Y_OP_X, + Y_OP_Y, + C_A_NEW_X, + C_A_NEW_Y, + C_TX_X, + C_TX_Y, + R_E_X, + R_E_Y, + V_TILDE, + A_TILDE_NEW, + SIGMA_A_NEW, + V_TILDE_AUD_R, + R_TILDE_AUD_R, + V_TILDE_AUD_S, + A_TILDE_AUD_S, + ); +} diff --git a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh index 04931d64b..90f436433 100755 --- a/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh +++ b/packages/tokens/src/confidential/circuits/scripts/extract_vks.sh @@ -26,6 +26,7 @@ CIRCUITS=( "withdraw" "transfer" "set_operator" + "operator_transfer" ) OUT_DIR="vks" diff --git a/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json new file mode 100644 index 000000000..2ecd3479f --- /dev/null +++ b/packages/tokens/src/confidential/circuits/vks/operator_transfer.vk.json @@ -0,0 +1 @@ +["0x0000000000000000000000000000000000000000000000000000000000008000","0x0000000000000000000000000000000000000000000000000000000000000028","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000018","0x000000000000000000000000000000728e2976c8a3b381c14c127381048a53ff","0x000000000000000000000000000000000016ce552e1b1a624054e3562b94183d","0x000000000000000000000000000000e60fa7e0f78838ff3d4d02f3afc8b29b9f","0x00000000000000000000000000000000002a0ed00b1a3bcb6075c1d60b0dc89f","0x000000000000000000000000000000659e02009282967f681cda2b0fef4e7e2c","0x00000000000000000000000000000000001ece57a1fdae239761aec60d4d2f25","0x00000000000000000000000000000076c40b6a84b2c331f536d595e672f9257a","0x000000000000000000000000000000000001e0d2d659ad103167f468dd991b77","0x000000000000000000000000000000ce7150dcf2d204eb2d8bbdd353f98aff4a","0x000000000000000000000000000000000018ecded80ed6856d7427a721de3b50","0x0000000000000000000000000000006656fefd05d8031f49135b54b6548f9cd6","0x000000000000000000000000000000000028914c2b4b33d2944993748e964c01","0x000000000000000000000000000000942f0f950562f961c12e5a30a23bf798b2","0x0000000000000000000000000000000000094403e315f3b732308e52d309ecd9","0x000000000000000000000000000000d6f7af0b27604cb87c56eaf34784253cc0","0x000000000000000000000000000000000009eaa1afb41f23289e04fd115cbf65","0x000000000000000000000000000000538bbe78c8ae32a9eeeeb53b4af1087639","0x00000000000000000000000000000000000d080f5cb5cb6e3b57215eb11bc602","0x000000000000000000000000000000186f1aa3fd90bc0890e000547ee558c3b1","0x00000000000000000000000000000000001ffd00e007210aac461fa94c8cde62","0x0000000000000000000000000000005dc11a8c8ab837620d133283d42e0c9771","0x00000000000000000000000000000000000be66f79027be91fc01c120a2e3fac","0x00000000000000000000000000000090ccf0fb57cd89fd7152d35fa10b932cd0","0x000000000000000000000000000000000027d9132527f9a009f6045a53a7079f","0x0000000000000000000000000000005738f222d113ad96d990e7d4a004ec2e57","0x000000000000000000000000000000000014e35bf3517c8beabf7ec116bd4c00","0x000000000000000000000000000000d89185b10a6c32362672867293fa6e87a6","0x00000000000000000000000000000000002cbca48853bc69fafe318ba9e1f9cc","0x0000000000000000000000000000006531231bd6ff91d4b3f4d895d0ce4044a3","0x000000000000000000000000000000000027eaaa4dbfab8179a7e1e099f95c94","0x000000000000000000000000000000cbc798523c917716b3815eda28365b7d15","0x00000000000000000000000000000000000e3a4eab74843d295759ac11ff6bf6","0x00000000000000000000000000000017f3b066aeb378654695d217da8c414c93","0x000000000000000000000000000000000001dc76e9fff2ad3aed6f791dd04bf6","0x0000000000000000000000000000006d2a9c6ef25bf999851bd956d4e71050f2","0x00000000000000000000000000000000001be3c330356f2e79309b2e4636b91c","0x0000000000000000000000000000004a7356b403e142436d7b02f899d35c4c81","0x000000000000000000000000000000000001f092fee03c63fca04f6a02215b35","0x00000000000000000000000000000099c97cf0c563a958e6a0fc32df458ee3da","0x000000000000000000000000000000000011f8f0df3984dd840a2faae3f0c8c2","0x000000000000000000000000000000319ee83108dcfa5796295cfd2f763450f2","0x00000000000000000000000000000000002aa088c410185f5bec00a222d97751","0x000000000000000000000000000000e7ec92b10a8e4322bb5fe40b9cbee9407e","0x00000000000000000000000000000000000722c453c7738d57c7d56a5a72e632","0x00000000000000000000000000000035c52301b11020576c2f70eb174d1aa68d","0x0000000000000000000000000000000000092a840276c46fca44dc58bdf735f9","0x00000000000000000000000000000058a18c8d96166b5de22d19e3edf588437f","0x00000000000000000000000000000000000f6d03e84cdddf9b43214b174f2864","0x0000000000000000000000000000004cc33c276d6010148da999e4902610f75c","0x00000000000000000000000000000000000be4f70ff1fb89a03fa0d9d3c928f2","0x00000000000000000000000000000039f52413eb11726ddde84ac343dd3a91de","0x00000000000000000000000000000000000653c620b5282b1f55e0f082c70716","0x00000000000000000000000000000011d63e320df0558e2736b5f0f15dc835b5","0x00000000000000000000000000000000002250ff33088fbe5c802a11960c11c4","0x000000000000000000000000000000f6a4932bdcbd9cb3f78c242740c58b2954","0x00000000000000000000000000000000001b3378dc0c3bfeb11955970a80a197","0x000000000000000000000000000000087959d56d861d383165f77bef23f34519","0x00000000000000000000000000000000000743a84f758794dc44ab96c9409e07","0x000000000000000000000000000000f2584327702442f77815e5986d216dad21","0x0000000000000000000000000000000000281d55540b2c88cf0e7849375ceae4","0x000000000000000000000000000000f02adfdfffa9d184e8c82e977857ef338b","0x0000000000000000000000000000000000121b6fe0db969b709cf5a331479817","0x00000000000000000000000000000037f198caddfdf78cd773ada269cf5e2a2b","0x000000000000000000000000000000000006f862b3b3a1d2affcb3d7d56c4cea","0x00000000000000000000000000000053b2ea393f429cddf8c1e6ad55fe9057b9","0x00000000000000000000000000000000002e44685af86d29cf672f8443f37e26","0x0000000000000000000000000000002d15b6ca8c2a2901da27d5fd817e5d782c","0x00000000000000000000000000000000001331e12fe784170750affdfed585f6","0x000000000000000000000000000000b2dadba131fd2a171969ce57ac9227323b","0x00000000000000000000000000000000002fa6720d2f031c4b6311fe4331039f","0x000000000000000000000000000000087b458b361a365fe55cfe6d88d328ac53","0x0000000000000000000000000000000000055bd5fb6bb736c2002f3adc4efe39","0x000000000000000000000000000000c61a5f0c8d1a1b10df2a0463f3d1acd107","0x0000000000000000000000000000000000098b842d4862030b2c5c64348ec2d1","0x000000000000000000000000000000a09158db7a1f1b4f5f96db1dabd1cf57be","0x00000000000000000000000000000000001ffce9ce4cf64b235eda36bd320d6b","0x000000000000000000000000000000eb177619e4c76a4c13cc4a7f90510dc65e","0x000000000000000000000000000000000002e5e60f1d28e5468d28c53cd7883d","0x000000000000000000000000000000e0a74ae2ea732e40422855c781c069a9d3","0x0000000000000000000000000000000000294ace59ee79eed594830a9ca62ae8","0x000000000000000000000000000000b2b59b433c9dc526c93c3fbb0c45b5266e","0x000000000000000000000000000000000029ed83d3cd468c72f080fb3dfd9294","0x00000000000000000000000000000048e33de80839b8f732e8d11f520eb3b39c","0x000000000000000000000000000000000000e2778436f01eeb9e8e6713aaf148","0x000000000000000000000000000000ffad1d95a8ee7ccd2287a489375de526f6","0x00000000000000000000000000000000002549709cd2124b19800eed4f003e52","0x00000000000000000000000000000043c4b44d03e114c03c1689f77c5789643c","0x00000000000000000000000000000000000621edcb5a9eab5ae5675b4da9d9a2","0x000000000000000000000000000000d0492104db91551b307d94f2ab08cc79f4","0x000000000000000000000000000000000025f2f754a0726238075d9def0babb9","0x000000000000000000000000000000cb74279f288c14712784e2fa6490e4536b","0x00000000000000000000000000000000001cb622d0b3b58b69ee8edce2674300","0x000000000000000000000000000000e2295164532fcce82a3c013e597c594ef8","0x00000000000000000000000000000000001189d45faed739fc4e16a3dd3f9b7b","0x00000000000000000000000000000019a9d0d71575acbc5f5960590bd2f40a84","0x00000000000000000000000000000000000c0d0b7c8a283361906d364db189df","0x000000000000000000000000000000ffec2c04e68f5bbfb7a031a8dfe73f8bc6","0x00000000000000000000000000000000000a3ebe2336d65cff7e94ebb4a6f2df","0x0000000000000000000000000000000e2e091047811681c2716df0d71df042c8","0x00000000000000000000000000000000001ced77a29b750aec5c8fa1e3c63876","0x0000000000000000000000000000000000000000000000000000000000000001","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000000000000000000000000000000000000002","0x0000000000000000000000000000000000000000000000000000000000000000","0x0000000000000000000000000000005fc9c6f0c0aa2bb71dee6940ba2b444829","0x000000000000000000000000000000000008e1584de58abb84df88eb2afd8eb0","0x0000000000000000000000000000000950590267c551136f6421674fe0e75b1b","0x0000000000000000000000000000000000090a0a0c36773bf2b7368171949f40"] \ No newline at end of file