From 420c958a871524ca8e9d170223f781adbf658738 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Fri, 14 Aug 2026 17:49:45 +0300 Subject: [PATCH 1/4] feat: add ct functions and new operations to secp256k1 --- simf/lib/ct/commitment.simf | 79 ++++++++++++++++++ simf/lib/ct/proofs.simf | 85 ++++++++++++++++++++ simf/lib/ct/relations.simf | 124 +++++++++++++++++++++++++++++ simf/lib/secp256k1/operations.simf | 9 ++- 4 files changed, 296 insertions(+), 1 deletion(-) create mode 100644 simf/lib/ct/commitment.simf create mode 100644 simf/lib/ct/proofs.simf create mode 100644 simf/lib/ct/relations.simf diff --git a/simf/lib/ct/commitment.simf b/simf/lib/ct/commitment.simf new file mode 100644 index 0000000..478e6d2 --- /dev/null +++ b/simf/lib/ct/commitment.simf @@ -0,0 +1,79 @@ +/* + * Confidential Transactions: commitment builders and openings. + * + * H_a = hash_to_curve(asset_id) + abf*G (asset generator) + * C = v*H_a + vbf*G (value commitment) + * + * Every assert in this module takes blinding factors, which means the value or + * asset id it checks becomes PUBLIC to anyone reading the witness. + * For checks that constrain commitments while keeping the values sealed, use + * `crate::lib::ct::relations`. + */ + +use crate::lib::asserts::{assert_eq_64, assert_eq_256}; +use crate::lib::u64::convert::u64_to_u256; + +/// Asserts that `p` has the same x-coordinate as `expected`, i.e. `p == +/-expected`. +/// Only use on points from the transaction, where consensus rules out the negation; +/// a `Point` from the witness could be either sign. +pub fn assert_x_eq(p: Point, expected: Gej) { + let (_, x): (u1, u256) = p; + assert!(jet::gej_x_equiv(x, expected)); +} + +/// Builds the asset generator `H_a = hash_to_curve(asset_id) + abf*G`. +pub fn asset_generator(asset_id: u256, abf: Scalar) -> Gej { + jet::gej_ge_add(jet::generate(abf), jet::hash_to_curve(asset_id)) +} + +/// Builds the value commitment `C = v*asset_gen + vbf*G`. +pub fn value_commitment(v: u64, asset_gen: Gej, vbf: Scalar) -> Gej { + jet::linear_combination_1((u64_to_u256(v), asset_gen), vbf) +} + +/// Asserts that the asset generator `h` is `hash_to_curve(asset_id) + abf*G`. +pub fn assert_asset_generator(h: Point, asset_id: u256, abf: Scalar) { + assert_x_eq(h, asset_generator(asset_id, abf)); +} + +/// Asserts that the value commitment `c` is `v*asset_gen + vbf*G`. +pub fn assert_value_commitment(c: Point, v: u64, asset_gen: Gej, vbf: Scalar) { + assert_x_eq(c, value_commitment(v, asset_gen, vbf)); +} + +/// Asserts that an `(asset, amount)` pair opens to `expected_asset_id` and +/// `expected_amount`. +pub fn assert_opens_to( + asset: Asset1, + amount: Amount1, + expected_asset_id: u256, + expected_amount: u64, + abf: Scalar, + vbf: Scalar +) { + match asset { + Left(conf_asset: Point) => { + let h: Gej = asset_generator(expected_asset_id, abf); + assert_x_eq(conf_asset, h); + + match amount { + Left(conf_amount: Point) => { + assert_value_commitment(conf_amount, expected_amount, h, vbf); + }, + Right(explicit_amount: u64) => assert_eq_64(explicit_amount, expected_amount), + }; + }, + Right(explicit_asset: u256) => { + assert_eq_256(explicit_asset, expected_asset_id); + + match amount { + // An explicit asset commits against the unblinded generator. + Left(conf_amount: Point) => { + let h: Gej = (jet::hash_to_curve(expected_asset_id), 1); + assert_value_commitment(conf_amount, expected_amount, h, vbf); + }, + Right(explicit_amount: u64) => assert_eq_64(explicit_amount, expected_amount), + }; + }, + }; +} diff --git a/simf/lib/ct/proofs.simf b/simf/lib/ct/proofs.simf new file mode 100644 index 0000000..1a02147 --- /dev/null +++ b/simf/lib/ct/proofs.simf @@ -0,0 +1,85 @@ +/* + * Confidential Transactions: exact value and asset proofs. + * + * These checks prove what a commitment holds without revealing any blinding factor: + * + * c - v*H_0 = vbf*G (exact value, explicit asset) + * H_a - H_0 = abf*G (exact asset) + * c + H_a - (v+1)*H_0 = ((v+1)*abf + vbf)*G (exact value and asset) + */ + +use crate::lib::asserts::{assert_eq_64, assert_eq_256}; +use crate::lib::secp256k1::operations::{point_to_ge, point_to_gej, safe_gej_normalize}; +use crate::lib::u64::convert::u64_to_u256; + +/// Asserts that `proof` shows knowledge of `x` with `p == x*G`. +/// Panics if `p` is the point at infinity. +fn assert_dlog(p: Gej, proof: Signature) { + let (px, _): (Fe, Fe) = safe_gej_normalize(p); + jet::bip_0340_verify((px, jet::sig_all_hash()), proof); +} + +/// Asserts that `c` commits to `v` against the generator `h`. +/// The prover's secret is `x` with `c - v*h == x*G`. +pub fn assert_exact_value(c: Point, v: u64, h: Ge, proof: Signature) { + let neg_v_h: Gej = jet::scale(jet::scalar_negate(u64_to_u256(v)), (h, 1)); + assert_dlog(jet::gej_ge_add(neg_v_h, point_to_ge(c)), proof); +} + +/// Asserts that the asset generator `h` blinds `asset_id`. +/// The prover's secret is `abf`. +pub fn assert_exact_asset(h: Point, asset_id: u256, proof: Signature) { + assert_dlog( + jet::gej_ge_add(point_to_gej(h), jet::ge_negate(jet::hash_to_curve(asset_id))), + proof + ); +} + +/// Asserts that `c` commits to `v` of `asset_id`, where `h` is the asset +/// generator of `c`. The prover's secret is `(v+1)*abf + vbf`. +/// Adding `h` keeps the asset bound when `v == 0`. +pub fn assert_exact_value_and_asset(c: Point, h: Point, asset_id: u256, v: u64, proof: Signature) { + let v_plus_1: Scalar = jet::scalar_add(u64_to_u256(v), 1); + let neg_h0: Gej = jet::scale(jet::scalar_negate(v_plus_1), (jet::hash_to_curve(asset_id), 1)); + assert_dlog(jet::gej_ge_add(jet::gej_ge_add(neg_h0, point_to_ge(c)), point_to_ge(h)), proof); +} + +/// Asserts that an `(asset, amount)` pair opens to `asset_id` and `v`, +/// but without blinding factors. The prover's secret for `proof`: +/// +/// - confidential asset, confidential amount: `(v+1)*abf + vbf` +/// - confidential asset, explicit amount: `abf` +/// - explicit asset, confidential amount: `vbf` +/// - explicit asset, explicit amount: unused +pub fn assert_opens_to_proof( + asset: Asset1, + amount: Amount1, + asset_id: u256, + v: u64, + proof: Signature +) { + match asset { + Left(conf_asset: Point) => { + match amount { + Left(conf_amount: Point) => { + assert_exact_value_and_asset(conf_amount, conf_asset, asset_id, v, proof) + }, + Right(explicit_amount: u64) => { + assert_eq_64(explicit_amount, v); + assert_exact_asset(conf_asset, asset_id, proof); + }, + }; + }, + Right(explicit_asset: u256) => { + assert_eq_256(explicit_asset, asset_id); + + match amount { + // An explicit asset commits against the unblinded generator. + Left(conf_amount: Point) => { + assert_exact_value(conf_amount, v, jet::hash_to_curve(asset_id), proof) + }, + Right(explicit_amount: u64) => assert_eq_64(explicit_amount, v), + }; + }, + }; +} diff --git a/simf/lib/ct/relations.simf b/simf/lib/ct/relations.simf new file mode 100644 index 0000000..a4d19c1 --- /dev/null +++ b/simf/lib/ct/relations.simf @@ -0,0 +1,124 @@ +/* + * Confidential Transactions: relations between commitments. + * + * Nothing here takes a blinding factor, so nothing here discloses a committed + * value. For checks that do reveal a value or an asset id, see + * `crate::lib::ct::commitment`. + * + * A value commitment expands to + * + * C = v*H_a + vbf*G = v*H_0 + (v*abf + vbf)*G + */ + +use crate::lib::secp256k1::operations::{point_to_ge, point_to_gej}; +use crate::lib::u64::convert::u64_to_u256; + +/// Returns the generator that an amount of `asset` commits against: +/// the blinded generator if `asset` is confidential, else `hash_to_curve(asset_id)`. +pub fn asset_to_ge(asset: Asset1) -> Ge { + match asset { + Left(conf_asset: Point) => point_to_ge(conf_asset), + Right(explicit_asset: u256) => jet::hash_to_curve(explicit_asset), + } +} + +/// Returns the value commitment of an `(asset, amount)` pair. +/// An explicit amount `v` becomes the unblinded commitment `v*H_a`, as in Elements. +pub fn amount_to_gej(asset: Asset1, amount: Amount1) -> Gej { + match amount { + Left(conf_amount: Point) => point_to_gej(conf_amount), + Right(explicit_amount: u64) => { + jet::scale(u64_to_u256(explicit_amount), (asset_to_ge(asset), 1)) + }, + } +} + +/// The empty accumulator (the point at infinity). +pub fn zero() -> Gej { + jet::gej_infinity() +} + +/// `acc + c` +pub fn add(acc: Gej, c: Point) -> Gej { + jet::gej_ge_add(acc, point_to_ge(c)) +} + +/// `acc - c` +pub fn sub(acc: Gej, c: Point) -> Gej { + jet::gej_ge_add(acc, jet::ge_negate(point_to_ge(c))) +} + +/// `acc + amount_to_gej(asset, amount)` +pub fn add_amount(acc: Gej, asset: Asset1, amount: Amount1) -> Gej { + jet::gej_add(acc, amount_to_gej(asset, amount)) +} + +/// `acc - amount_to_gej(asset, amount)` +pub fn sub_amount(acc: Gej, asset: Asset1, amount: Amount1) -> Gej { + jet::gej_add(acc, jet::gej_negate(amount_to_gej(asset, amount))) +} + +/// `acc + k*c` +pub fn add_scaled(acc: Gej, k: u64, c: Point) -> Gej { + jet::gej_add(acc, jet::scale(u64_to_u256(k), point_to_gej(c))) +} + +/// `acc - k*c` +pub fn sub_scaled(acc: Gej, k: u64, c: Point) -> Gej { + jet::gej_add(acc, jet::gej_negate(jet::scale(u64_to_u256(k), point_to_gej(c)))) +} + +/// Returns true iff `acc == s*G`. +pub fn is_balanced(acc: Gej, s: Scalar) -> bool { + jet::gej_equiv(acc, jet::generate(s)) +} + +/// Asserts that `acc == s*G`. +pub fn assert_balanced(acc: Gej, s: Scalar) { + assert!(is_balanced(acc, s)); +} + +/// Returns true iff `q == k*p + s*G`. +pub fn is_scaled_eq(q: Point, k: u64, p: Point, s: Scalar) -> bool { + jet::gej_ge_equiv( + jet::linear_combination_1((u64_to_u256(k), point_to_gej(p)), s), + point_to_ge(q) + ) +} + +/// Asserts that `q == k*p + s*G`, i.e. that `q` holds `k` times the value of `p`. +pub fn assert_scaled_eq(q: Point, k: u64, p: Point, s: Scalar) { + jet::point_verify_1(((u64_to_u256(k), p), s), q); +} + +/// Returns true iff `q == p + s*G`. +pub fn is_value_eq(q: Point, p: Point, s: Scalar) -> bool { + is_scaled_eq(q, 1, p, s) +} + +/// Asserts that `q == p + s*G`, i.e. that two commitments hold the same value. +pub fn assert_value_eq(q: Point, p: Point, s: Scalar) { + assert_scaled_eq(q, 1, p, s); +} + +/// Returns true iff `q == p + s*G` for two asset generators. +pub fn is_asset_eq(q: Point, p: Point, s: Scalar) -> bool { + is_scaled_eq(q, 1, p, s) +} + +/// Asserts that `q == p + s*G`, i.e. that two asset generators blind the same +/// asset id. Here `s = abf_q - abf_p`. +pub fn assert_asset_eq(q: Point, p: Point, s: Scalar) { + assert_scaled_eq(q, 1, p, s); +} + +/// Asserts that `a*x == b*y + s*G`, i.e. that the values of `x` and `y` are in +/// the ratio `b : a`. +pub fn assert_ratio_eq(a: u64, x: Point, b: u64, y: Point, s: Scalar) { + assert_balanced(sub_scaled(add_scaled(zero(), a, x), b, y), s); +} + +/// Asserts that `c1 + c2 == c_sum + s*G`. +pub fn assert_sum_eq(c1: Point, c2: Point, c_sum: Point, s: Scalar) { + assert_balanced(sub(add(add(zero(), c1), c2), c_sum), s); +} diff --git a/simf/lib/secp256k1/operations.simf b/simf/lib/secp256k1/operations.simf index b4a2fcd..bca4ca2 100644 --- a/simf/lib/secp256k1/operations.simf +++ b/simf/lib/secp256k1/operations.simf @@ -11,9 +11,16 @@ pub fn ge_to_point(p: Ge) -> Point { } } +/// Decompress a `Point` into affine coordinates. +/// Panics if `jet::decompress(p)` returns `None`. +pub fn point_to_ge(p: Point) -> Ge { + unwrap(jet::decompress(p)) +} + /// Decompress a `Point` into a Jacobian point with `z = 1`. +/// Panics if `jet::decompress(p)` returns `None`. pub fn point_to_gej(p: Point) -> Gej { - (unwrap(jet::decompress(p)), 1) + (point_to_ge(p), 1) } /// Convert the point into affine coordinates. From 98c3d73958f9d3cd4b75d1ef41bb0064188ba388 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 1 Oct 2026 16:02:00 +0300 Subject: [PATCH 2/4] fix: bug parity bit in conf asset/amount points. Temporary solution --- simf/lib/ct/commitment.simf | 11 ++++++++--- simf/lib/ct/point.simf | 27 +++++++++++++++++++++++++++ simf/lib/ct/proofs.simf | 13 ++++++++----- simf/lib/ct/relations.simf | 28 +++++++++++++++------------- 4 files changed, 58 insertions(+), 21 deletions(-) create mode 100644 simf/lib/ct/point.simf diff --git a/simf/lib/ct/commitment.simf b/simf/lib/ct/commitment.simf index 478e6d2..c993d5f 100644 --- a/simf/lib/ct/commitment.simf +++ b/simf/lib/ct/commitment.simf @@ -13,12 +13,17 @@ use crate::lib::asserts::{assert_eq_64, assert_eq_256}; use crate::lib::u64::convert::u64_to_u256; -/// Asserts that `p` has the same x-coordinate as `expected`, i.e. `p == +/-expected`. +/// Returns true iff `p` has the same x-coordinate as `expected`, i.e. `p == +/-expected`. /// Only use on points from the transaction, where consensus rules out the negation; /// a `Point` from the witness could be either sign. -pub fn assert_x_eq(p: Point, expected: Gej) { +pub fn is_x_eq(p: Point, expected: Gej) -> bool { let (_, x): (u1, u256) = p; - assert!(jet::gej_x_equiv(x, expected)); + jet::gej_x_equiv(x, expected) +} + +/// Asserts that `p` has the same x-coordinate as `expected`; see `is_x_eq`. +pub fn assert_x_eq(p: Point, expected: Gej) { + assert!(is_x_eq(p, expected)); } /// Builds the asset generator `H_a = hash_to_curve(asset_id) + abf*G`. diff --git a/simf/lib/ct/point.simf b/simf/lib/ct/point.simf new file mode 100644 index 0000000..b22b5b8 --- /dev/null +++ b/simf/lib/ct/point.simf @@ -0,0 +1,27 @@ +/* + * Confidential Transactions: decoding confidential points. + * + * Elements sets the bit of a CT `Point` when y is not a square, but + * `jet::decompress` reads it as "y is odd", so CT points are decoded here. + * See https://github.com/BlockstreamResearch/simplicity/issues/349 + */ + +use crate::lib::u1::convert::u1_to_bool; + +/// Decodes a confidential asset or amount `Point`. +/// Panics if `x` is not on the curve. +pub fn conf_point_to_ge(p: Point) -> Ge { + let (not_square, x): (u1, u256) = p; + // `fe_square_root` always returns the root that is itself a square. + let y: Fe = unwrap(jet::fe_square_root(jet::fe_add(jet::fe_multiply(jet::fe_square(x), x), 7))); + + match u1_to_bool(not_square) { + true => (x, jet::fe_negate(y)), + false => (x, y), + } +} + +/// Decodes a confidential asset or amount `Point` into a Jacobian point with `z = 1`. +pub fn conf_point_to_gej(p: Point) -> Gej { + (conf_point_to_ge(p), 1) +} diff --git a/simf/lib/ct/proofs.simf b/simf/lib/ct/proofs.simf index 1a02147..23ec411 100644 --- a/simf/lib/ct/proofs.simf +++ b/simf/lib/ct/proofs.simf @@ -9,7 +9,8 @@ */ use crate::lib::asserts::{assert_eq_64, assert_eq_256}; -use crate::lib::secp256k1::operations::{point_to_ge, point_to_gej, safe_gej_normalize}; +use crate::lib::ct::point::{conf_point_to_ge, conf_point_to_gej}; +use crate::lib::secp256k1::operations::safe_gej_normalize; use crate::lib::u64::convert::u64_to_u256; /// Asserts that `proof` shows knowledge of `x` with `p == x*G`. @@ -23,25 +24,27 @@ fn assert_dlog(p: Gej, proof: Signature) { /// The prover's secret is `x` with `c - v*h == x*G`. pub fn assert_exact_value(c: Point, v: u64, h: Ge, proof: Signature) { let neg_v_h: Gej = jet::scale(jet::scalar_negate(u64_to_u256(v)), (h, 1)); - assert_dlog(jet::gej_ge_add(neg_v_h, point_to_ge(c)), proof); + assert_dlog(jet::gej_ge_add(neg_v_h, conf_point_to_ge(c)), proof); } /// Asserts that the asset generator `h` blinds `asset_id`. /// The prover's secret is `abf`. pub fn assert_exact_asset(h: Point, asset_id: u256, proof: Signature) { assert_dlog( - jet::gej_ge_add(point_to_gej(h), jet::ge_negate(jet::hash_to_curve(asset_id))), + jet::gej_ge_add(conf_point_to_gej(h), jet::ge_negate(jet::hash_to_curve(asset_id))), proof ); } /// Asserts that `c` commits to `v` of `asset_id`, where `h` is the asset /// generator of `c`. The prover's secret is `(v+1)*abf + vbf`. -/// Adding `h` keeps the asset bound when `v == 0`. pub fn assert_exact_value_and_asset(c: Point, h: Point, asset_id: u256, v: u64, proof: Signature) { let v_plus_1: Scalar = jet::scalar_add(u64_to_u256(v), 1); let neg_h0: Gej = jet::scale(jet::scalar_negate(v_plus_1), (jet::hash_to_curve(asset_id), 1)); - assert_dlog(jet::gej_ge_add(jet::gej_ge_add(neg_h0, point_to_ge(c)), point_to_ge(h)), proof); + assert_dlog( + jet::gej_ge_add(jet::gej_ge_add(neg_h0, conf_point_to_ge(c)), conf_point_to_ge(h)), + proof + ); } /// Asserts that an `(asset, amount)` pair opens to `asset_id` and `v`, diff --git a/simf/lib/ct/relations.simf b/simf/lib/ct/relations.simf index a4d19c1..cf10e47 100644 --- a/simf/lib/ct/relations.simf +++ b/simf/lib/ct/relations.simf @@ -8,16 +8,20 @@ * A value commitment expands to * * C = v*H_a + vbf*G = v*H_0 + (v*abf + vbf)*G + * + * Points used in a calculation are decoded exactly; the point the result is + * compared with is checked by x-coordinate only (see `commitment::is_x_eq`). */ -use crate::lib::secp256k1::operations::{point_to_ge, point_to_gej}; +use crate::lib::ct::commitment::{is_x_eq, assert_x_eq}; +use crate::lib::ct::point::{conf_point_to_ge, conf_point_to_gej}; use crate::lib::u64::convert::u64_to_u256; /// Returns the generator that an amount of `asset` commits against: /// the blinded generator if `asset` is confidential, else `hash_to_curve(asset_id)`. pub fn asset_to_ge(asset: Asset1) -> Ge { match asset { - Left(conf_asset: Point) => point_to_ge(conf_asset), + Left(conf_asset: Point) => conf_point_to_ge(conf_asset), Right(explicit_asset: u256) => jet::hash_to_curve(explicit_asset), } } @@ -26,7 +30,7 @@ pub fn asset_to_ge(asset: Asset1) -> Ge { /// An explicit amount `v` becomes the unblinded commitment `v*H_a`, as in Elements. pub fn amount_to_gej(asset: Asset1, amount: Amount1) -> Gej { match amount { - Left(conf_amount: Point) => point_to_gej(conf_amount), + Left(conf_amount: Point) => conf_point_to_gej(conf_amount), Right(explicit_amount: u64) => { jet::scale(u64_to_u256(explicit_amount), (asset_to_ge(asset), 1)) }, @@ -40,12 +44,12 @@ pub fn zero() -> Gej { /// `acc + c` pub fn add(acc: Gej, c: Point) -> Gej { - jet::gej_ge_add(acc, point_to_ge(c)) + jet::gej_ge_add(acc, conf_point_to_ge(c)) } /// `acc - c` pub fn sub(acc: Gej, c: Point) -> Gej { - jet::gej_ge_add(acc, jet::ge_negate(point_to_ge(c))) + jet::gej_ge_add(acc, jet::ge_negate(conf_point_to_ge(c))) } /// `acc + amount_to_gej(asset, amount)` @@ -60,12 +64,12 @@ pub fn sub_amount(acc: Gej, asset: Asset1, amount: Amount1) -> Gej { /// `acc + k*c` pub fn add_scaled(acc: Gej, k: u64, c: Point) -> Gej { - jet::gej_add(acc, jet::scale(u64_to_u256(k), point_to_gej(c))) + jet::gej_add(acc, jet::scale(u64_to_u256(k), conf_point_to_gej(c))) } /// `acc - k*c` pub fn sub_scaled(acc: Gej, k: u64, c: Point) -> Gej { - jet::gej_add(acc, jet::gej_negate(jet::scale(u64_to_u256(k), point_to_gej(c)))) + jet::gej_add(acc, jet::gej_negate(jet::scale(u64_to_u256(k), conf_point_to_gej(c)))) } /// Returns true iff `acc == s*G`. @@ -80,15 +84,12 @@ pub fn assert_balanced(acc: Gej, s: Scalar) { /// Returns true iff `q == k*p + s*G`. pub fn is_scaled_eq(q: Point, k: u64, p: Point, s: Scalar) -> bool { - jet::gej_ge_equiv( - jet::linear_combination_1((u64_to_u256(k), point_to_gej(p)), s), - point_to_ge(q) - ) + is_x_eq(q, jet::linear_combination_1((u64_to_u256(k), conf_point_to_gej(p)), s)) } /// Asserts that `q == k*p + s*G`, i.e. that `q` holds `k` times the value of `p`. pub fn assert_scaled_eq(q: Point, k: u64, p: Point, s: Scalar) { - jet::point_verify_1(((u64_to_u256(k), p), s), q); + assert!(is_scaled_eq(q, k, p, s)); } /// Returns true iff `q == p + s*G`. @@ -120,5 +121,6 @@ pub fn assert_ratio_eq(a: u64, x: Point, b: u64, y: Point, s: Scalar) { /// Asserts that `c1 + c2 == c_sum + s*G`. pub fn assert_sum_eq(c1: Point, c2: Point, c_sum: Point, s: Scalar) { - assert_balanced(sub(add(add(zero(), c1), c2), c_sum), s); + let sum: Gej = add(add(zero(), c1), c2); + assert_x_eq(c_sum, jet::gej_add(sum, jet::gej_negate(jet::generate(s)))); } From 0364225e9540b90a39fbf2684de76d71f1aa26d5 Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 1 Oct 2026 16:41:55 +0300 Subject: [PATCH 3/4] feat: add rust-tests with simf/tests/ct directory --- simf/tests/ct/point.simf | 24 ++ simf/tests/ct/relations.simf | 117 ++++++++++ tests/stdlib/ct/helpers.rs | 155 +++++++++++++ tests/stdlib/ct/mod.rs | 3 + tests/stdlib/ct/point.rs | 135 +++++++++++ tests/stdlib/ct/relations.rs | 424 +++++++++++++++++++++++++++++++++++ tests/stdlib/main.rs | 1 + 7 files changed, 859 insertions(+) create mode 100644 simf/tests/ct/point.simf create mode 100644 simf/tests/ct/relations.simf create mode 100644 tests/stdlib/ct/helpers.rs create mode 100644 tests/stdlib/ct/mod.rs create mode 100644 tests/stdlib/ct/point.rs create mode 100644 tests/stdlib/ct/relations.rs diff --git a/simf/tests/ct/point.simf b/simf/tests/ct/point.simf new file mode 100644 index 0000000..6e286ac --- /dev/null +++ b/simf/tests/ct/point.simf @@ -0,0 +1,24 @@ +use crate::lib::ct::point::{conf_point_to_ge, conf_point_to_gej}; +use crate::lib::secp256k1::operations::{assert_ge_eq, assert_gej_ge_eq}; + +use crate::tests::support::dispatch::is_selected; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + let point: Point = witness::POINT; + let exp_ge: Ge = witness::EXPECTED_GE; + + match is_selected(0, fn_idx) { + true => { + assert_ge_eq(conf_point_to_ge(point), exp_ge); + }, + false => (), + }; + match is_selected(1, fn_idx) { + true => { + assert_gej_ge_eq(conf_point_to_gej(point), exp_ge); + }, + false => (), + }; +} diff --git a/simf/tests/ct/relations.simf b/simf/tests/ct/relations.simf new file mode 100644 index 0000000..5feb461 --- /dev/null +++ b/simf/tests/ct/relations.simf @@ -0,0 +1,117 @@ +use crate::lib::ct::relations::{ + asset_to_ge, + amount_to_gej, + zero, + add_amount, + sub_amount, + is_balanced, + assert_balanced, + is_scaled_eq, + assert_scaled_eq, + is_value_eq, + assert_value_eq, + is_asset_eq, + assert_asset_eq, + assert_ratio_eq, + assert_sum_eq +}; +use crate::lib::secp256k1::operations::{assert_ge_eq, assert_gej_ge_eq}; + +use crate::tests::support::dispatch::is_selected; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + // Commitments or asset generators, depending on the function index + let p: Point = witness::P; + let q: Point = witness::Q; + let r: Point = witness::R; + let k: u64 = witness::K; + let k2: u64 = witness::K2; + let s: Scalar = witness::S; + + // Two inputs `a`, `b` and one output `c` + let asset_a: Asset1 = witness::ASSET_A; + let amount_a: Amount1 = witness::AMOUNT_A; + let asset_b: Asset1 = witness::ASSET_B; + let amount_b: Amount1 = witness::AMOUNT_B; + let asset_c: Asset1 = witness::ASSET_C; + let amount_c: Amount1 = witness::AMOUNT_C; + + let exp_ge: Ge = witness::EXPECTED_GE; + + match is_selected(0, fn_idx) { + true => { + assert_ge_eq(asset_to_ge(asset_a), exp_ge); + }, + false => (), + }; + match is_selected(1, fn_idx) { + true => { + assert_gej_ge_eq(amount_to_gej(asset_a, amount_a), exp_ge); + }, + false => (), + }; + match is_selected(2, fn_idx) { + true => { + let inputs: Gej = add_amount(add_amount(zero(), asset_a, amount_a), asset_b, amount_b); + assert_balanced(sub_amount(inputs, asset_c, amount_c), s); + }, + false => (), + }; + match is_selected(3, fn_idx) { + true => { + let inputs: Gej = add_amount(add_amount(zero(), asset_a, amount_a), asset_b, amount_b); + assert!(is_balanced(sub_amount(inputs, asset_c, amount_c), s)); + }, + false => (), + }; + match is_selected(4, fn_idx) { + true => { + assert_sum_eq(p, q, r, s); + }, + false => (), + }; + match is_selected(5, fn_idx) { + true => { + assert_value_eq(q, p, s); + }, + false => (), + }; + match is_selected(6, fn_idx) { + true => { + assert!(is_value_eq(q, p, s)); + }, + false => (), + }; + match is_selected(7, fn_idx) { + true => { + assert_asset_eq(q, p, s); + }, + false => (), + }; + match is_selected(8, fn_idx) { + true => { + assert!(is_asset_eq(q, p, s)); + }, + false => (), + }; + match is_selected(9, fn_idx) { + true => { + assert_scaled_eq(q, k, p, s); + }, + false => (), + }; + match is_selected(10, fn_idx) { + true => { + assert!(is_scaled_eq(q, k, p, s)); + }, + false => (), + }; + match is_selected(11, fn_idx) { + true => { + assert_ratio_eq(k, p, k2, q, s); + }, + false => (), + }; +} diff --git a/tests/stdlib/ct/helpers.rs b/tests/stdlib/ct/helpers.rs new file mode 100644 index 0000000..2bec258 --- /dev/null +++ b/tests/stdlib/ct/helpers.rs @@ -0,0 +1,155 @@ +use num_bigint::BigUint; +use rand::{Rng, RngCore, rngs::OsRng}; +use secp256k1_zkp::{ + All, Generator, PedersenCommitment, Secp256k1, SecretKey, Tag, Tweak, + rand::rngs::OsRng as SecpOsRng, +}; + +/// A confidential asset or amount, as the Elements jets return it. +pub type ConfPoint = (u8, [u8; 32]); +/// Affine coordinates `(x, y)`. +pub type Ge = ([u8; 32], [u8; 32]); + +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F +const SECP_P: [u8; 32] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, 0xFC, 0x2F, +]; + +// FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +const SECP_N: [u8; 32] = [ + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, + 0xBA, 0xAE, 0xDC, 0xE6, 0xAF, 0x48, 0xA0, 0x3B, 0xBF, 0xD2, 0x5E, 0x8C, 0xD0, 0x36, 0x41, 0x41, +]; + +pub fn secp() -> Secp256k1 { + Secp256k1::new() +} + +fn secp_p() -> BigUint { + BigUint::from_bytes_be(&SECP_P) +} + +fn secp_n() -> BigUint { + BigUint::from_bytes_be(&SECP_N) +} + +pub fn to_32_be(x: &BigUint) -> [u8; 32] { + let b = x.to_bytes_be(); + + let mut out = [0u8; 32]; + out[32 - b.len()..].copy_from_slice(&b); + + out +} + +pub fn random_scalar() -> BigUint { + BigUint::from_bytes_be(&SecretKey::new(&mut SecpOsRng).secret_bytes()) +} + +pub fn random_amount() -> u64 { + rand::thread_rng().gen_range(1..=1_000_000_000) +} + +pub fn random_asset_id() -> [u8; 32] { + let mut id = [0u8; 32]; + OsRng.fill_bytes(&mut id); + + id +} + +/// `a + b mod n` +pub fn add_n(a: &BigUint, b: &BigUint) -> BigUint { + (a + b) % secp_n() +} + +/// `a - b mod n` +pub fn sub_n(a: &BigUint, b: &BigUint) -> BigUint { + let n = secp_n(); + (a % &n + &n - b % &n) % &n +} + +/// `a * b mod n` +pub fn mul_n(a: &BigUint, b: &BigUint) -> BigUint { + (a * b) % secp_n() +} + +pub fn scalar(s: &BigUint) -> [u8; 32] { + to_32_be(&(s % secp_n())) +} + +fn tweak(s: &BigUint) -> Tweak { + Tweak::from_inner(scalar(s)).expect("non-zero scalar below n") +} + +/// The unblinded asset generator `H_0 = hash_to_curve(asset_id)`. +pub fn unblinded_generator(asset_id: [u8; 32]) -> Generator { + Generator::new_unblinded(&secp(), Tag::from(asset_id)) +} + +/// The blinded asset generator `H_a = H_0 + abf*G`. +pub fn blinded_generator(asset_id: [u8; 32], abf: &BigUint) -> Generator { + Generator::new_blinded(&secp(), Tag::from(asset_id), tweak(abf)) +} + +/// The value commitment `C = v*generator + vbf*G`. +pub fn commitment(v: u64, generator: Generator, vbf: &BigUint) -> PedersenCommitment { + PedersenCommitment::new(&secp(), v, tweak(vbf), generator) +} + +/// The unblinded commitment `v*generator` that Elements uses for an explicit amount. +pub fn unblinded_commitment(v: u64, generator: Generator) -> PedersenCommitment { + PedersenCommitment::new_unblinded(&secp(), v, generator) +} + +/// The blinding factor of `v*(H_0 + abf*G) + vbf*G` relative to `H_0`, i.e. `v*abf + vbf`. +/// Relations between commitments of the same asset hold up to these factors. +pub fn total_blinding(v: u64, abf: &BigUint, vbf: &BigUint) -> BigUint { + add_n(&mul_n(&BigUint::from(v), abf), vbf) +} + +/// The Elements encoding of a serialized generator or commitment: +/// `bit == 1` iff y is not a square. +pub fn conf_point(serialized: [u8; 33]) -> ConfPoint { + (serialized[0] & 1, serialized[1..].try_into().unwrap()) +} + +/// Reference decoder for `conf_point`: affine coordinates of a serialized +/// generator or commitment. +pub fn decode(serialized: [u8; 33]) -> Ge { + let p = secp_p(); + let x = BigUint::from_bytes_be(&serialized[1..]); + let y_squared = (&x * &x * &x + 7u32) % &p; + + // `a^((p+1)/4)` is the square root that is itself a square. + let y = y_squared.modpow(&((&p + 1u32) >> 2), &p); + let y = if serialized[0] & 1 == 1 { &p - y } else { y }; + + (to_32_be(&x), to_32_be(&y)) +} + +#[cfg(test)] +mod tests { + use super::*; + use secp256k1_zkp::{PublicKey, SecretKey}; + + // A commitment to 0 is `vbf*G`, i.e. the public key of `vbf`, whose + // coordinates `secp256k1` gives us independently. + #[test] + fn decode_matches_public_key() { + let generator = unblinded_generator(random_asset_id()); + + for _ in 0..1_000 { + let vbf = random_scalar(); + let c = commitment(0, generator, &vbf).serialize(); + + let sk = SecretKey::from_slice(&scalar(&vbf)).unwrap(); + let pk = PublicKey::from_secret_key(&secp(), &sk).serialize_uncompressed(); + + assert_eq!( + decode(c), + (pk[1..33].try_into().unwrap(), pk[33..].try_into().unwrap()) + ); + } + } +} diff --git a/tests/stdlib/ct/mod.rs b/tests/stdlib/ct/mod.rs new file mode 100644 index 0000000..f62b99c --- /dev/null +++ b/tests/stdlib/ct/mod.rs @@ -0,0 +1,3 @@ +mod helpers; +mod point; +mod relations; diff --git a/tests/stdlib/ct/point.rs b/tests/stdlib/ct/point.rs new file mode 100644 index 0000000..d356ab9 --- /dev/null +++ b/tests/stdlib/ct/point.rs @@ -0,0 +1,135 @@ +use num_bigint::BigUint; +use secp256k1_zkp::{PublicKey, SecretKey}; + +use crate::common::core::{Expect, run}; + +use simplicityhl_std::artifacts::tests::ct::point::PointProgram as CtPointTestProgram; +use simplicityhl_std::artifacts::tests::ct::point::derived_point::{ + PointArguments as CtPointTestArguments, PointWitness as CtPointTestWitness, +}; + +use super::helpers::{ + ConfPoint, Ge, blinded_generator, commitment, conf_point, decode, random_amount, + random_asset_id, random_scalar, scalar, secp, unblinded_generator, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + ConfPointToGe, + ConfPointToGej, +} + +fn program() -> CtPointTestProgram { + CtPointTestProgram::new(&CtPointTestArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads. +struct Case { + witness: CtPointTestWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: CtPointTestWitness { + function_index: function as u8, + point: (0, [0; 32]), + expected_ge: ([0; 32], [0; 32]), + }, + } +} + +impl Case { + /// `point`: a confidential point in the Elements encoding. + fn point(mut self, point: ConfPoint) -> Self { + self.witness.point = point; + self + } + + /// `expected_ge`: the affine point the arm should decode to. + fn expect_ge(mut self, expected_ge: Ge) -> Self { + self.witness.expected_ge = expected_ge; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + run(context, program(), self.witness, expect) + } +} + +fn multiple_of_g(k: u8) -> (ConfPoint, Ge) { + let vbf = BigUint::from(k); + let c = conf_point(commitment(0, unblinded_generator([0; 32]), &vbf).serialize()); + + let sk = SecretKey::from_slice(&scalar(&vbf)).unwrap(); + let pk = PublicKey::from_secret_key(&secp(), &sk).serialize_uncompressed(); + + ( + c, + (pk[1..33].try_into().unwrap(), pk[33..].try_into().unwrap()), + ) +} + +// conf_point_to_ge +#[simplex::test] +fn conf_point_to_ge_square_y(context: simplex::TestContext) -> anyhow::Result<()> { + let (point, ge) = multiple_of_g(6); + + case(ConfPointToGe).point(point).expect_ge(ge).run(&context) +} + +#[simplex::test] +fn conf_point_to_ge_non_square_y(context: simplex::TestContext) -> anyhow::Result<()> { + let (point, ge) = multiple_of_g(3); + + case(ConfPointToGe).point(point).expect_ge(ge).run(&context) +} + +#[simplex::test] +fn conf_point_to_ge_asset_generator(context: simplex::TestContext) -> anyhow::Result<()> { + let h = blinded_generator(random_asset_id(), &random_scalar()).serialize(); + + case(ConfPointToGe) + .point(conf_point(h)) + .expect_ge(decode(h)) + .run(&context) +} + +#[simplex::test] +fn conf_point_to_ge_rejects_flipped_bit(context: simplex::TestContext) -> anyhow::Result<()> { + let (point, ge) = multiple_of_g(6); + + case(ConfPointToGe) + .point((point.0 ^ 1, point.1)) + .expect_ge(ge) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn conf_point_to_ge_rejects_x_off_curve(context: simplex::TestContext) -> anyhow::Result<()> { + // x = 5: 5^3 + 7 = 132 has no square root modulo p. + let mut x = [0u8; 32]; + x[31] = 5; + + case(ConfPointToGe) + .point((0, x)) + .expecting(&context, Expect::PrunedBranch) +} + +// conf_point_to_gej +#[simplex::test] +fn conf_point_to_gej_value_commitment(context: simplex::TestContext) -> anyhow::Result<()> { + let generator = blinded_generator(random_asset_id(), &random_scalar()); + let c = commitment(random_amount(), generator, &random_scalar()).serialize(); + + case(ConfPointToGej) + .point(conf_point(c)) + .expect_ge(decode(c)) + .run(&context) +} diff --git a/tests/stdlib/ct/relations.rs b/tests/stdlib/ct/relations.rs new file mode 100644 index 0000000..bbdb42e --- /dev/null +++ b/tests/stdlib/ct/relations.rs @@ -0,0 +1,424 @@ +use num_bigint::BigUint; +use simplex::either::Either::{self, Left, Right}; + +use simplicityhl_std::artifacts::tests::ct::relations::RelationsProgram as CtRelationsTestProgram; +use simplicityhl_std::artifacts::tests::ct::relations::derived_relations::{ + RelationsArguments as CtRelationsTestArguments, RelationsWitness as CtRelationsTestWitness, +}; + +use crate::common::core::{Expect, run}; + +use super::helpers::{ + ConfPoint, Ge, add_n, blinded_generator, commitment, conf_point, decode, mul_n, random_amount, + random_asset_id, random_scalar, scalar, sub_n, total_blinding, unblinded_commitment, + unblinded_generator, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + AssetToGe, + AmountToGej, + Balanced, + IsBalanced, + SumEq, + ValueEq, + IsValueEq, + AssetEq, + IsAssetEq, + ScaledEq, + IsScaledEq, + RatioEq, +} + +type Asset1 = Either; +type Amount1 = Either; + +fn program() -> CtRelationsTestProgram { + CtRelationsTestProgram::new(&CtRelationsTestArguments {}) +} + +/// One dispatch arm of the contract, plus the witness it reads. +struct Case { + witness: CtRelationsTestWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: CtRelationsTestWitness { + function_index: function as u8, + p: (0, [0; 32]), + q: (0, [0; 32]), + r: (0, [0; 32]), + k: 0, + k2: 0, + s: [0; 32], + asset_a: Right([0; 32]), + amount_a: Right(0), + asset_b: Right([0; 32]), + amount_b: Right(0), + asset_c: Right([0; 32]), + amount_c: Right(0), + expected_ge: ([0; 32], [0; 32]), + }, + } +} + +impl Case { + fn points(mut self, p: ConfPoint, q: ConfPoint) -> Self { + self.witness.p = p; + self.witness.q = q; + self + } + + fn third(mut self, r: ConfPoint) -> Self { + self.witness.r = r; + self + } + + fn multipliers(mut self, k: u64, k2: u64) -> Self { + self.witness.k = k; + self.witness.k2 = k2; + self + } + + /// `s`: the blinding difference the relation holds up to. + fn blinding(mut self, s: &BigUint) -> Self { + self.witness.s = scalar(s); + self + } + + fn input_a(mut self, asset: Asset1, amount: Amount1) -> Self { + self.witness.asset_a = asset; + self.witness.amount_a = amount; + self + } + + fn input_b(mut self, asset: Asset1, amount: Amount1) -> Self { + self.witness.asset_b = asset; + self.witness.amount_b = amount; + self + } + + fn output_c(mut self, asset: Asset1, amount: Amount1) -> Self { + self.witness.asset_c = asset; + self.witness.amount_c = amount; + self + } + + /// `expected_ge`: the affine point the arm should produce. + fn expect_ge(mut self, expected_ge: Ge) -> Self { + self.witness.expected_ge = expected_ge; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + run(context, program(), self.witness, expect) + } +} + +/// A confidential output: blinded asset `id` holding `v`. +struct Output { + asset: ConfPoint, + amount: ConfPoint, + /// `v*abf + vbf` + blinding: BigUint, +} + +fn output(id: [u8; 32], v: u64) -> Output { + let (abf, vbf) = (random_scalar(), random_scalar()); + let generator = blinded_generator(id, &abf); + + Output { + asset: conf_point(generator.serialize()), + amount: conf_point(commitment(v, generator, &vbf).serialize()), + blinding: total_blinding(v, &abf, &vbf), + } +} + +// asset_to_ge +#[simplex::test] +fn asset_to_ge_confidential(context: simplex::TestContext) -> anyhow::Result<()> { + let h = blinded_generator(random_asset_id(), &random_scalar()).serialize(); + + case(AssetToGe) + .input_a(Left(conf_point(h)), Right(0)) + .expect_ge(decode(h)) + .run(&context) +} + +#[simplex::test] +fn asset_to_ge_explicit(context: simplex::TestContext) -> anyhow::Result<()> { + let id = random_asset_id(); + + case(AssetToGe) + .input_a(Right(id), Right(0)) + .expect_ge(decode(unblinded_generator(id).serialize())) + .run(&context) +} + +// amount_to_gej +#[simplex::test] +fn amount_to_gej_confidential(context: simplex::TestContext) -> anyhow::Result<()> { + let generator = blinded_generator(random_asset_id(), &random_scalar()); + let c = commitment(random_amount(), generator, &random_scalar()).serialize(); + + case(AmountToGej) + .input_a(Left(conf_point(generator.serialize())), Left(conf_point(c))) + .expect_ge(decode(c)) + .run(&context) +} + +#[simplex::test] +fn amount_to_gej_explicit_amount_confidential_asset( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let generator = blinded_generator(random_asset_id(), &random_scalar()); + let v = random_amount(); + + case(AmountToGej) + .input_a(Left(conf_point(generator.serialize())), Right(v)) + .expect_ge(decode(unblinded_commitment(v, generator).serialize())) + .run(&context) +} + +#[simplex::test] +fn amount_to_gej_explicit(context: simplex::TestContext) -> anyhow::Result<()> { + let id = random_asset_id(); + let v = random_amount(); + + case(AmountToGej) + .input_a(Right(id), Right(v)) + .expect_ge(decode( + unblinded_commitment(v, unblinded_generator(id)).serialize(), + )) + .run(&context) +} + +// assert_balanced, is_balanced +// +// Inputs: confidential `v1` and explicit `v2`. Output: confidential `v_out`. +fn balance_case(function: FunctionToTest, v_out_delta: u64) -> Case { + let id = random_asset_id(); + let (v1, v2) = (random_amount(), random_amount()); + + let a = output(id, v1); + let c = output(id, v1 + v2 + v_out_delta); + + case(function) + .input_a(Left(a.asset), Left(a.amount)) + .input_b(Right(id), Right(v2)) + .output_c(Left(c.asset), Left(c.amount)) + .blinding(&sub_n(&a.blinding, &c.blinding)) +} + +#[simplex::test] +fn balanced_mixed_explicit_and_confidential(context: simplex::TestContext) -> anyhow::Result<()> { + balance_case(Balanced, 0).run(&context) +} + +#[simplex::test] +fn balanced_rejects_inflation(context: simplex::TestContext) -> anyhow::Result<()> { + balance_case(Balanced, 1).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn is_balanced_mixed_explicit_and_confidential( + context: simplex::TestContext, +) -> anyhow::Result<()> { + balance_case(IsBalanced, 0).run(&context) +} + +#[simplex::test] +fn is_balanced_rejects_inflation(context: simplex::TestContext) -> anyhow::Result<()> { + balance_case(IsBalanced, 1).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn balanced_rejects_other_asset(context: simplex::TestContext) -> anyhow::Result<()> { + let (v1, v2) = (random_amount(), random_amount()); + let a = output(random_asset_id(), v1); + let c = output(random_asset_id(), v1 + v2); + let id_b = random_asset_id(); + + case(Balanced) + .input_a(Left(a.asset), Left(a.amount)) + .input_b(Right(id_b), Right(v2)) + .output_c(Left(c.asset), Left(c.amount)) + .blinding(&sub_n(&a.blinding, &c.blinding)) + .expecting(&context, Expect::AssertFailed) +} + +// assert_sum_eq +fn sum_case(sum_delta: u64) -> Case { + let id = random_asset_id(); + let (v1, v2) = (random_amount(), random_amount()); + + let c1 = output(id, v1); + let c2 = output(id, v2); + let sum = output(id, v1 + v2 + sum_delta); + + case(SumEq) + .points(c1.amount, c2.amount) + .third(sum.amount) + .blinding(&sub_n(&add_n(&c1.blinding, &c2.blinding), &sum.blinding)) +} + +#[simplex::test] +fn sum_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + sum_case(0).run(&context) +} + +#[simplex::test] +fn sum_eq_rejects_wrong_sum(context: simplex::TestContext) -> anyhow::Result<()> { + sum_case(1).expecting(&context, Expect::AssertFailed) +} + +// assert_value_eq, is_value_eq +// +// `q` and `p` hold `v_q` of `id_q` and `v_p` of `id_p`, each blinded differently. +fn value_eq_case(function: FunctionToTest, same_value: bool, same_asset: bool) -> Case { + let id_p = random_asset_id(); + let id_q = if same_asset { id_p } else { random_asset_id() }; + let v_p = random_amount(); + let v_q = if same_value { v_p } else { v_p + 1 }; + + let p = output(id_p, v_p); + let q = output(id_q, v_q); + + case(function) + .points(p.amount, q.amount) + .blinding(&sub_n(&q.blinding, &p.blinding)) +} + +#[simplex::test] +fn value_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + value_eq_case(ValueEq, true, true).run(&context) +} + +#[simplex::test] +fn value_eq_rejects_other_value(context: simplex::TestContext) -> anyhow::Result<()> { + value_eq_case(ValueEq, false, true).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn value_eq_rejects_other_asset(context: simplex::TestContext) -> anyhow::Result<()> { + value_eq_case(ValueEq, true, false).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn is_value_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + value_eq_case(IsValueEq, true, true).run(&context) +} + +#[simplex::test] +fn is_value_eq_rejects_other_value(context: simplex::TestContext) -> anyhow::Result<()> { + value_eq_case(IsValueEq, false, true).expecting(&context, Expect::AssertFailed) +} + +// assert_asset_eq, is_asset_eq +fn asset_eq_case(function: FunctionToTest, same_asset: bool) -> Case { + let id_p = random_asset_id(); + let id_q = if same_asset { id_p } else { random_asset_id() }; + let (abf_p, abf_q) = (random_scalar(), random_scalar()); + + case(function) + .points( + conf_point(blinded_generator(id_p, &abf_p).serialize()), + conf_point(blinded_generator(id_q, &abf_q).serialize()), + ) + .blinding(&sub_n(&abf_q, &abf_p)) +} + +#[simplex::test] +fn asset_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + asset_eq_case(AssetEq, true).run(&context) +} + +#[simplex::test] +fn asset_eq_rejects_other_asset(context: simplex::TestContext) -> anyhow::Result<()> { + asset_eq_case(AssetEq, false).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn is_asset_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + asset_eq_case(IsAssetEq, true).run(&context) +} + +#[simplex::test] +fn is_asset_eq_rejects_other_asset(context: simplex::TestContext) -> anyhow::Result<()> { + asset_eq_case(IsAssetEq, false).expecting(&context, Expect::AssertFailed) +} + +// assert_scaled_eq, is_scaled_eq +// +// `q` holds `k*v + q_delta`, `p` holds `v`. +fn scaled_eq_case(function: FunctionToTest, q_delta: u64) -> Case { + let id = random_asset_id(); + let v = random_amount(); + let k = random_amount() % 1_000 + 2; + + let p = output(id, v); + let q = output(id, k * v + q_delta); + + case(function) + .points(p.amount, q.amount) + .multipliers(k, 0) + .blinding(&sub_n(&q.blinding, &mul_n(&BigUint::from(k), &p.blinding))) +} + +#[simplex::test] +fn scaled_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + scaled_eq_case(ScaledEq, 0).run(&context) +} + +#[simplex::test] +fn scaled_eq_rejects_wrong_multiple(context: simplex::TestContext) -> anyhow::Result<()> { + scaled_eq_case(ScaledEq, 1).expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn is_scaled_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + scaled_eq_case(IsScaledEq, 0).run(&context) +} + +#[simplex::test] +fn is_scaled_eq_rejects_wrong_multiple(context: simplex::TestContext) -> anyhow::Result<()> { + scaled_eq_case(IsScaledEq, 1).expecting(&context, Expect::AssertFailed) +} + +// assert_ratio_eq +// +// `x` holds `5*w`, `y` holds `3*w + y_delta`, so `3*x == 5*y` iff `y_delta == 0`. +fn ratio_case(y_delta: u64) -> Case { + let id = random_asset_id(); + let w = random_amount(); + let (a, b) = (3u64, 5u64); + + let x = output(id, b * w); + let y = output(id, a * w + y_delta); + + case(RatioEq) + .points(x.amount, y.amount) + .multipliers(a, b) + .blinding(&sub_n( + &mul_n(&BigUint::from(a), &x.blinding), + &mul_n(&BigUint::from(b), &y.blinding), + )) +} + +#[simplex::test] +fn ratio_eq_holds(context: simplex::TestContext) -> anyhow::Result<()> { + ratio_case(0).run(&context) +} + +#[simplex::test] +fn ratio_eq_rejects_wrong_ratio(context: simplex::TestContext) -> anyhow::Result<()> { + ratio_case(1).expecting(&context, Expect::AssertFailed) +} diff --git a/tests/stdlib/main.rs b/tests/stdlib/main.rs index 8698e0e..4ac199a 100644 --- a/tests/stdlib/main.rs +++ b/tests/stdlib/main.rs @@ -2,6 +2,7 @@ mod common; mod asserts; mod binary; +mod ct; mod op_return; mod secp256k1; mod u1; From 63b565a2d063d9bff9e8fc54168798e84953643e Mon Sep 17 00:00:00 2001 From: LesterEvSe Date: Thu, 1 Oct 2026 17:28:09 +0300 Subject: [PATCH 4/4] feat: add tests for commitment.simf file --- simf/tests/ct/commitment.simf | 52 ++++++++ tests/stdlib/ct/commitment.rs | 217 ++++++++++++++++++++++++++++++++++ tests/stdlib/ct/mod.rs | 1 + 3 files changed, 270 insertions(+) create mode 100644 simf/tests/ct/commitment.simf create mode 100644 tests/stdlib/ct/commitment.rs diff --git a/simf/tests/ct/commitment.simf b/simf/tests/ct/commitment.simf new file mode 100644 index 0000000..ece980e --- /dev/null +++ b/simf/tests/ct/commitment.simf @@ -0,0 +1,52 @@ +use crate::lib::ct::commitment::{ + asset_generator, + value_commitment, + assert_x_eq, + assert_asset_generator, + assert_opens_to +}; +use crate::lib::secp256k1::operations::assert_gej_ge_eq; + +use crate::tests::support::dispatch::is_selected; + +fn main() { + let fn_idx: u8 = witness::FUNCTION_INDEX; + + // An asset generator or a value commitment, depending on the function index + let point: Point = witness::POINT; + + let asset_id: u256 = witness::ASSET_ID; + let v: u64 = witness::V; + let abf: Scalar = witness::ABF; + let vbf: Scalar = witness::VBF; + + let asset: Asset1 = witness::ASSET; + let amount: Amount1 = witness::AMOUNT; + + let exp_ge: Ge = witness::EXPECTED_GE; + + match is_selected(0, fn_idx) { + true => { + assert_gej_ge_eq(value_commitment(v, asset_generator(asset_id, abf), vbf), exp_ge); + }, + false => (), + }; + match is_selected(1, fn_idx) { + true => { + assert_x_eq(point, (exp_ge, 1)); + }, + false => (), + }; + match is_selected(2, fn_idx) { + true => { + assert_asset_generator(point, asset_id, abf); + }, + false => (), + }; + match is_selected(3, fn_idx) { + true => { + assert_opens_to(asset, amount, asset_id, v, abf, vbf); + }, + false => (), + }; +} diff --git a/tests/stdlib/ct/commitment.rs b/tests/stdlib/ct/commitment.rs new file mode 100644 index 0000000..5c6aa98 --- /dev/null +++ b/tests/stdlib/ct/commitment.rs @@ -0,0 +1,217 @@ +use num_bigint::BigUint; +use simplex::either::Either::{self, Left, Right}; + +use simplicityhl_std::artifacts::tests::ct::commitment::CommitmentProgram as CtCommitmentTestProgram; +use simplicityhl_std::artifacts::tests::ct::commitment::derived_commitment::{ + CommitmentArguments as CtCommitmentTestArguments, CommitmentWitness as CtCommitmentTestWitness, +}; + +use crate::common::core::{Expect, run}; + +use super::helpers::{ + ConfPoint, Ge, blinded_generator, commitment, conf_point, decode, random_amount, + random_asset_id, random_scalar, scalar, unblinded_generator, +}; + +use FunctionToTest::*; + +enum FunctionToTest { + ValueCommitment, + XEq, + AssertAssetGenerator, + OpensTo, +} + +fn program() -> CtCommitmentTestProgram { + CtCommitmentTestProgram::new(&CtCommitmentTestArguments {}) +} + +struct Case { + witness: CtCommitmentTestWitness, +} + +fn case(function: FunctionToTest) -> Case { + Case { + witness: CtCommitmentTestWitness { + function_index: function as u8, + point: (0, [0; 32]), + asset_id: [0; 32], + v: 0, + abf: [0; 32], + vbf: [0; 32], + asset: Right([0; 32]), + amount: Right(0), + expected_ge: ([0; 32], [0; 32]), + }, + } +} + +impl Case { + /// `point`: the on-chain asset generator or value commitment. + fn point(mut self, point: ConfPoint) -> Self { + self.witness.point = point; + self + } + + /// The claimed opening: asset id, value and both blinding factors. + fn opening(mut self, asset_id: [u8; 32], v: u64, abf: &BigUint, vbf: &BigUint) -> Self { + self.witness.asset_id = asset_id; + self.witness.v = v; + self.witness.abf = scalar(abf); + self.witness.vbf = scalar(vbf); + self + } + + /// The on-chain `(asset, amount)` pair. + fn pair(mut self, asset: Either, amount: Either) -> Self { + self.witness.asset = asset; + self.witness.amount = amount; + self + } + + /// `expected_ge`: the affine point the arm should produce. + fn expect_ge(mut self, expected_ge: Ge) -> Self { + self.witness.expected_ge = expected_ge; + self + } + + /// Fund, spend, and expect the spend to succeed. + fn run(self, context: &simplex::TestContext) -> anyhow::Result<()> { + self.expecting(context, Expect::Ok) + } + + /// Fund, spend, and expect `expect`. + fn expecting(self, context: &simplex::TestContext, expect: Expect) -> anyhow::Result<()> { + run(context, program(), self.witness, expect) + } +} + +/// A random confidential output and the opening that produced it. +struct Output { + id: [u8; 32], + v: u64, + abf: BigUint, + vbf: BigUint, + asset: [u8; 33], + amount: [u8; 33], +} + +fn random_output() -> Output { + let (id, v, abf, vbf) = ( + random_asset_id(), + random_amount(), + random_scalar(), + random_scalar(), + ); + let generator = blinded_generator(id, &abf); + + Output { + id, + v, + asset: generator.serialize(), + amount: commitment(v, generator, &vbf).serialize(), + abf, + vbf, + } +} + +// value_commitment +#[simplex::test] +fn value_commitment_matches_elements(context: simplex::TestContext) -> anyhow::Result<()> { + let out = random_output(); + + case(ValueCommitment) + .opening(out.id, out.v, &out.abf, &out.vbf) + .expect_ge(decode(out.amount)) + .run(&context) +} + +// assert_x_eq +#[simplex::test] +fn x_eq_accepts_negation(context: simplex::TestContext) -> anyhow::Result<()> { + // Only x is compared, so the bit is ignored by design. + let amount = random_output().amount; + let (bit, x) = conf_point(amount); + + case(XEq) + .point((bit ^ 1, x)) + .expect_ge(decode(amount)) + .run(&context) +} + +// assert_asset_generator +#[simplex::test] +fn asset_generator_holds(context: simplex::TestContext) -> anyhow::Result<()> { + let out = random_output(); + + case(AssertAssetGenerator) + .point(conf_point(out.asset)) + .opening(out.id, 0, &out.abf, &out.vbf) + .run(&context) +} + +// assert_opens_to +#[simplex::test] +fn opens_to_confidential(context: simplex::TestContext) -> anyhow::Result<()> { + let out = random_output(); + + case(OpensTo) + .pair(Left(conf_point(out.asset)), Left(conf_point(out.amount))) + .opening(out.id, out.v, &out.abf, &out.vbf) + .run(&context) +} + +#[simplex::test] +fn opens_to_confidential_asset_explicit_amount( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let out = random_output(); + + case(OpensTo) + .pair(Left(conf_point(out.asset)), Right(out.v)) + .opening(out.id, out.v, &out.abf, &out.vbf) + .run(&context) +} + +#[simplex::test] +fn opens_to_explicit_asset_confidential_amount( + context: simplex::TestContext, +) -> anyhow::Result<()> { + let (id, v, vbf) = (random_asset_id(), random_amount(), random_scalar()); + let amount = commitment(v, unblinded_generator(id), &vbf).serialize(); + + case(OpensTo) + .pair(Right(id), Left(conf_point(amount))) + .opening(id, v, &random_scalar(), &vbf) + .run(&context) +} + +#[simplex::test] +fn opens_to_explicit(context: simplex::TestContext) -> anyhow::Result<()> { + let (id, v) = (random_asset_id(), random_amount()); + + case(OpensTo) + .pair(Right(id), Right(v)) + .opening(id, v, &random_scalar(), &random_scalar()) + .run(&context) +} + +#[simplex::test] +fn opens_to_rejects_other_amount(context: simplex::TestContext) -> anyhow::Result<()> { + let out = random_output(); + + case(OpensTo) + .pair(Left(conf_point(out.asset)), Left(conf_point(out.amount))) + .opening(out.id, out.v + 1, &out.abf, &out.vbf) + .expecting(&context, Expect::AssertFailed) +} + +#[simplex::test] +fn opens_to_rejects_other_asset(context: simplex::TestContext) -> anyhow::Result<()> { + let out = random_output(); + + case(OpensTo) + .pair(Left(conf_point(out.asset)), Left(conf_point(out.amount))) + .opening(random_asset_id(), out.v, &out.abf, &out.vbf) + .expecting(&context, Expect::AssertFailed) +} diff --git a/tests/stdlib/ct/mod.rs b/tests/stdlib/ct/mod.rs index f62b99c..cff1f35 100644 --- a/tests/stdlib/ct/mod.rs +++ b/tests/stdlib/ct/mod.rs @@ -1,3 +1,4 @@ +mod commitment; mod helpers; mod point; mod relations;