From 28c21baecbbc7d084a11d572a26cc32720abfd82 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Sun, 2 Nov 2025 17:26:16 +0100 Subject: [PATCH 01/12] fix: check if points are on the curve before addition --- contracts/src/libs/proving/Delta.sol | 30 ++- contracts/test/libs/EllipticCurve.t.sol | 262 ++++++++++++++++++++++++ 2 files changed, 290 insertions(+), 2 deletions(-) create mode 100644 contracts/test/libs/EllipticCurve.t.sol diff --git a/contracts/src/libs/proving/Delta.sol b/contracts/src/libs/proving/Delta.sol index bf8083b3..dc31c19c 100644 --- a/contracts/src/libs/proving/Delta.sol +++ b/contracts/src/libs/proving/Delta.sol @@ -20,15 +20,27 @@ library Delta { uint256 y; } - /// @notice The constant of the secp256k1 (K-256) elliptic curve. + /// @notice The x-coordinate of the curve generator point. + uint256 internal constant _GX = 0x79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798; + + /// @notice The y-coordinate of the curve generator point. + uint256 internal constant _GY = 0x483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8; + + // @notice The coefficient a of th secp256k1 (K-256) elliptic curve (y² = x³ + ax + b). uint256 internal constant _AA = 0; - /// @notice The modulus of the secp256k1 (K-256) elliptic curve. + // @notice The coefficient b of th secp256k1 (K-256) elliptic curve (y² = x³ + ax + b). + uint256 internal constant _BB = 7; + + /// @notice The field prime modulus (2^256 - 2^32 - 977) of the secp256k1 (K-256) elliptic curve (y² = x³ + ax + b). uint256 internal constant _PP = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; /// @notice Thrown if the recovered delta public key doesn't match the delta instance. error DeltaMismatch(address expected, address actual); + /// @notice Thrown when a provided point is not on the curve. + error PointNotOnCurve(CurvePoint point); + /// @notice Returns the elliptic curve point representing the zero delta. /// @return zeroDelta The zero delta. function zero() internal pure returns (CurvePoint memory zeroDelta) { @@ -40,9 +52,23 @@ library Delta { /// @param p2 The second curve point. /// @return sum The resulting curve point. function add(CurvePoint memory p1, CurvePoint memory p2) internal pure returns (CurvePoint memory sum) { + if (!p1.isZero() && !EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: _AA, _bb: _BB, _pp: _PP})) { + revert PointNotOnCurve(p1); + } + if (!p2.isZero() && !EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: _AA, _bb: _BB, _pp: _PP})) { + revert PointNotOnCurve(p2); + } + (sum.x, sum.y) = EllipticCurve.ecAdd({_x1: p1.x, _y1: p1.y, _x2: p2.x, _y2: p2.y, _aa: _AA, _pp: _PP}); } + /// @notice Returns whether a point is the zero delta or not. + /// @param p The point to check. + /// @return isZeroDelta Whether the point is the zero delta or not. + function isZero(CurvePoint memory p) internal pure returns (bool isZeroDelta) { + isZeroDelta = p.x == 0 && p.y == 0; + } + /// @notice Converts an elliptic curve point to an Ethereum account address. /// @param delta The elliptic curve point. /// @return account The associated account. diff --git a/contracts/test/libs/EllipticCurve.t.sol b/contracts/test/libs/EllipticCurve.t.sol new file mode 100644 index 00000000..4d66195a --- /dev/null +++ b/contracts/test/libs/EllipticCurve.t.sol @@ -0,0 +1,262 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.30; + +import {EllipticCurve} from "@elliptic-curve-solidity/contracts/EllipticCurve.sol"; +import {Test} from "forge-std/Test.sol"; + +import {Delta} from "../../src/libs/proving/Delta.sol"; + +/** + * @title EllipticCurvePropertiesTest + * @dev Property-based tests for EllipticCurve.ecAdd using Foundry fuzzing. + * + * These tests verify GROUP PROPERTIES with points VERIFIED to be on the curve. + * Points are generated via scalar multiplication: P = k × G + * This guarantees all test points satisfy the curve equation y² = x³ + ax + b + * + * Run with: forge test -vv + * Run with more fuzz runs: forge test --fuzz-runs 10000 -vv + */ +contract EllipticCurvePropertiesTest is Test { + using EllipticCurve for *; + + /* GROUP PROPERTY TESTS (Points Verified On Curve) */ + + /// @notice GROUP PROPERTY: Commutativity - P + Q = Q + P + /// @dev Uses secp256k1 with points VERIFIED to be on the curve + function testGroupProperty_Commutativity_Secp256k1(uint256 scalar1, uint256 scalar2) public pure { + // Generate two points on secp256k1 by scalar multiplication of G + scalar1 = bound(scalar1, 1, 20); // Keep small for performance + scalar2 = bound(scalar2, 1, 20); + + // P1 = scalar1 * G + (uint256 x1, uint256 y1) = + EllipticCurve.ecMul({_k: scalar1, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // P2 = scalar2 * G + (uint256 x2, uint256 y2) = + EllipticCurve.ecMul({_k: scalar2, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Verify both points are on curve + assertTrue( + EllipticCurve.isOnCurve({_x: x1, _y: y1, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "P1 must be on curve" + ); + assertTrue( + EllipticCurve.isOnCurve({_x: x2, _y: y2, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "P2 must be on curve" + ); + + // Test commutativity: P1 + P2 = P2 + P1 + (uint256 resultX1, uint256 resultY1) = + EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: x2, _y2: y2, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 resultX2, uint256 resultY2) = + EllipticCurve.ecAdd({_x1: x2, _y1: y2, _x2: x1, _y2: y1, _aa: Delta._AA, _pp: Delta._PP}); + + assertEq(resultX1, resultX2, "GROUP PROPERTY: P + Q must equal Q + P"); + assertEq(resultY1, resultY2, "GROUP PROPERTY: P + Q must equal Q + P"); + } + + /// @notice GROUP PROPERTY: Identity - P + O = P + function testGroupProperty_Identity_Secp256k1(uint256 scalar) public pure { + scalar = bound(scalar, 1, 20); + + // P = scalar * G (guaranteed on curve) + (uint256 x, uint256 y) = + EllipticCurve.ecMul({_k: scalar, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Verify P is on curve + assertTrue( + EllipticCurve.isOnCurve({_x: x, _y: y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "P must be on curve" + ); + + // P + O should equal P + (uint256 resultX, uint256 resultY) = + EllipticCurve.ecAdd({_x1: x, _y1: y, _x2: 0, _y2: 0, _aa: Delta._AA, _pp: Delta._PP}); + + assertEq(resultX, x, "GROUP PROPERTY: P + O must equal P"); + assertEq(resultY, y, "GROUP PROPERTY: P + O must equal P"); + } + + /// @notice GROUP PROPERTY: Inverse - P + (-P) = O + function testGroupProperty_Inverse_Secp256k1(uint256 scalar) public pure { + scalar = bound(scalar, 1, 20); + + // P = scalar * G (guaranteed on curve) + (uint256 x, uint256 y) = + EllipticCurve.ecMul({_k: scalar, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Verify P is on curve + assertTrue( + EllipticCurve.isOnCurve({_x: x, _y: y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "P must be on curve" + ); + + // Get -P + (uint256 invX, uint256 invY) = EllipticCurve.ecInv({_x: x, _y: y, _pp: Delta._PP}); + + // Verify -P is on curve + assertTrue( + EllipticCurve.isOnCurve({_x: invX, _y: invY, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "-P must be on curve" + ); + + // P + (-P) should equal O + (uint256 resultX, uint256 resultY) = + EllipticCurve.ecAdd({_x1: x, _y1: y, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); + + assertTrue( + Delta.isZero(Delta.CurvePoint(resultX, resultY)), "GROUP PROPERTY: P + (-P) must equal point at infinity" + ); + } + + /// @notice GROUP PROPERTY: Associativity - (P + Q) + R = P + (Q + R) + function testGroupProperty_Associativity_Secp256k1(uint256 scalar1, uint256 scalar2, uint256 scalar3) public pure { + scalar1 = bound(scalar1, 1, 10); // Keep small for performance + scalar2 = bound(scalar2, 1, 10); + scalar3 = bound(scalar3, 1, 10); + + // Generate three points on curve + (uint256 x1, uint256 y1) = + EllipticCurve.ecMul({_k: scalar1, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 x2, uint256 y2) = + EllipticCurve.ecMul({_k: scalar2, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 x3, uint256 y3) = + EllipticCurve.ecMul({_k: scalar3, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Verify all points are on curve + assertTrue(EllipticCurve.isOnCurve({_x: x1, _y: y1, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + assertTrue(EllipticCurve.isOnCurve({_x: x2, _y: y2, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + assertTrue(EllipticCurve.isOnCurve({_x: x3, _y: y3, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + + // Compute (P + Q) + R + (uint256 pqX, uint256 pqY) = + EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: x2, _y2: y2, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 resultX1, uint256 resultY1) = + EllipticCurve.ecAdd({_x1: pqX, _y1: pqY, _x2: x3, _y2: y3, _aa: Delta._AA, _pp: Delta._PP}); + + // Compute P + (Q + R) + (uint256 qrX, uint256 qrY) = + EllipticCurve.ecAdd({_x1: x2, _y1: y2, _x2: x3, _y2: y3, _aa: Delta._AA, _pp: Delta._PP}); + + (uint256 resultX2, uint256 resultY2) = + EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: qrX, _y2: qrY, _aa: Delta._AA, _pp: Delta._PP}); + + assertEq(resultX1, resultX2, "GROUP PROPERTY: Associativity must hold"); + assertEq(resultY1, resultY2, "GROUP PROPERTY: Associativity must hold"); + } + + /// @notice GROUP PROPERTY: Closure - If P, Q on curve, then P+Q on curve or at infinity + function testGroupProperty_Closure_Secp256k1(uint256 scalar1, uint256 scalar2) public pure { + scalar1 = bound(scalar1, 1, 20); + scalar2 = bound(scalar2, 1, 20); + + // Generate two points on curve + (uint256 x1, uint256 y1) = + EllipticCurve.ecMul({_k: scalar1, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 x2, uint256 y2) = + EllipticCurve.ecMul({_k: scalar2, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Verify inputs are on curve + assertTrue(EllipticCurve.isOnCurve({_x: x1, _y: y1, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + assertTrue(EllipticCurve.isOnCurve({_x: x2, _y: y2, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + + // Add points + (uint256 resultX, uint256 resultY) = + EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: x2, _y2: y2, _aa: Delta._AA, _pp: Delta._PP}); + + // Result must be on curve or at infinity + bool atInfinity = Delta.isZero(Delta.CurvePoint(resultX, resultY)); + bool onCurve = + EllipticCurve.isOnCurve({_x: resultX, _y: resultY, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}); + + assertTrue(atInfinity || onCurve, "GROUP PROPERTY: Closure - result must be on curve or at infinity"); + } + + /* PRECONDITION TESTS (UNREDUCED COORDINATES) */ + + /// @notice PRECONDITION TEST: Demonstrates correct behavior with reduced coordinates + function test_Precondition_ReducedCoordinates() public pure { + // Use a small prime: p = 7, curve parameter a = 2 + uint256 pp = 7; + uint256 aa = 2; + + // Point P: (3, 5), Point Q: (3, 2) where Q is inverse of P since 2 + 5 = 7 ≡ 0 (mod 7) + (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({_x1: 3, _y1: 5, _x2: 3, _y2: 2, _aa: aa, _pp: pp}); + + // P + (-P) should equal point at infinity + assertTrue( + Delta.isZero(Delta.CurvePoint(rx, ry)), + "With REDUCED coordinates: (3,5) + (3,2) correctly gives point at infinity" + ); + } + + /* CONCRETE TESTS (SECP256K1) */ + + /// @notice Concrete: G + G = 2G + function test_Concrete_Doubling_Secp256k1() public pure { + (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({ + _x1: Delta._GX, _y1: Delta._GY, _x2: Delta._GX, _y2: Delta._GY, _aa: Delta._AA, _pp: Delta._PP + }); + + // Expected 2G + assertEq(rx, 0xc6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5); + assertEq(ry, 0x1ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a); + + // Verify result is on curve + assertTrue(EllipticCurve.isOnCurve({_x: rx, _y: ry, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + } + + /// @notice Concrete: G + O = G + function test_Concrete_Identity_Secp256k1() public pure { + (uint256 rx, uint256 ry) = + EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: 0, _y2: 0, _aa: Delta._AA, _pp: Delta._PP}); + + assertEq(rx, Delta._GX); + assertEq(ry, Delta._GY); + } + + /// @notice Concrete: G + (-G) = O + function test_Concrete_Inverse_Secp256k1() public pure { + (uint256 invX, uint256 invY) = EllipticCurve.ecInv({_x: Delta._GX, _y: Delta._GY, _pp: Delta._PP}); + + (uint256 rx, uint256 ry) = + EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); + + assertTrue(Delta.isZero(Delta.CurvePoint(rx, ry)), "G + (-G) should be point at infinity"); + } + + /// @notice Concrete: (G + 2G) + 3G = G + (2G + 3G) = 6G + function test_Concrete_Associativity_Secp256k1() public pure { + // Compute 2G, 3G + + (uint256 p2gX, uint256 p2gY) = + EllipticCurve.ecMul({_k: 2, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 p3gX, uint256 p3gY) = + EllipticCurve.ecMul({_k: 3, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + // Compute (G + 2G) + 3G + (uint256 pg2gX, uint256 pg2gY) = + EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: p2gX, _y2: p2gY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 result1X, uint256 result1Y) = + EllipticCurve.ecAdd({_x1: pg2gX, _y1: pg2gY, _x2: p3gX, _y2: p3gY, _aa: Delta._AA, _pp: Delta._PP}); + + // Compute G + (2G + 3G) + (uint256 p2g3gX, uint256 p2g3gY) = + EllipticCurve.ecAdd({_x1: p2gX, _y1: p2gY, _x2: p3gX, _y2: p3gY, _aa: Delta._AA, _pp: Delta._PP}); + (uint256 result2X, uint256 result2Y) = EllipticCurve.ecAdd({ + _x1: Delta._GX, _y1: Delta._GY, _x2: p2g3gX, _y2: p2g3gY, _aa: Delta._AA, _pp: Delta._PP + }); + + // Both should equal 6G + (uint256 p6gx, uint256 p6gy) = + EllipticCurve.ecMul({_k: 6, _x: Delta._GX, _y: Delta._GY, _aa: Delta._AA, _pp: Delta._PP}); + + assertEq(result1X, result2X, "Associativity: (G+2G)+3G = G+(2G+3G)"); + + assertEq(result1Y, result2Y, "Associativity: (G+2G)+3G = G+(2G+3G)"); + assertEq(result1X, p6gx, "Result should be 6G"); + assertEq(result1Y, p6gy, "Result should be 6G"); + } +} From b72fa3f1c5152d66e3e3bc2243dac83914d3bd8a Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Mon, 3 Nov 2025 16:25:47 +0100 Subject: [PATCH 02/12] fix: name clash --- bindings/src/conversion.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/bindings/src/conversion.rs b/bindings/src/conversion.rs index dc3e33d6..86dc935c 100644 --- a/bindings/src/conversion.rs +++ b/bindings/src/conversion.rs @@ -7,7 +7,7 @@ use arm_risc0::logic_instance::{AppData, ExpirableBlob}; use arm_risc0::logic_proof::LogicVerifierInputs; use arm_risc0::proving_system::encode_seal; -use arm_risc0::transaction::{Delta, Transaction}; +use arm_risc0::transaction::{Delta as ArmDelta, Transaction}; use arm_risc0::utils::words_to_bytes; sol!( @@ -120,8 +120,8 @@ impl From for ProtocolAdapter::Action { impl From for ProtocolAdapter::Transaction { fn from(tx: Transaction) -> Self { let delta_proof = match &tx.delta_proof { - Delta::Witness(_) => panic!("Unbalanced Transactions cannot be converted"), - Delta::Proof(proof) => proof.to_bytes().to_vec(), + ArmDelta::Witness(_) => panic!("Unbalanced Transactions cannot be converted"), + ArmDelta::Proof(proof) => proof.to_bytes().to_vec(), }; Self { From 0573ec6682a303a0adf0e6d93d383353d1216d26 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 4 Nov 2025 09:25:27 +0100 Subject: [PATCH 03/12] refactor: simplify check following the recommendation by Informal Systems --- contracts/src/libs/proving/Delta.sol | 12 +----------- contracts/test/libs/EllipticCurve.t.sol | 16 ++++++++++++---- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/contracts/src/libs/proving/Delta.sol b/contracts/src/libs/proving/Delta.sol index dc31c19c..40cc64ed 100644 --- a/contracts/src/libs/proving/Delta.sol +++ b/contracts/src/libs/proving/Delta.sol @@ -52,23 +52,13 @@ library Delta { /// @param p2 The second curve point. /// @return sum The resulting curve point. function add(CurvePoint memory p1, CurvePoint memory p2) internal pure returns (CurvePoint memory sum) { - if (!p1.isZero() && !EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: _AA, _bb: _BB, _pp: _PP})) { - revert PointNotOnCurve(p1); - } - if (!p2.isZero() && !EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: _AA, _bb: _BB, _pp: _PP})) { + if (!EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: _AA, _bb: _BB, _pp: _PP})) { revert PointNotOnCurve(p2); } (sum.x, sum.y) = EllipticCurve.ecAdd({_x1: p1.x, _y1: p1.y, _x2: p2.x, _y2: p2.y, _aa: _AA, _pp: _PP}); } - /// @notice Returns whether a point is the zero delta or not. - /// @param p The point to check. - /// @return isZeroDelta Whether the point is the zero delta or not. - function isZero(CurvePoint memory p) internal pure returns (bool isZeroDelta) { - isZeroDelta = p.x == 0 && p.y == 0; - } - /// @notice Converts an elliptic curve point to an Ethereum account address. /// @param delta The elliptic curve point. /// @return account The associated account. diff --git a/contracts/test/libs/EllipticCurve.t.sol b/contracts/test/libs/EllipticCurve.t.sol index 4d66195a..11f699c7 100644 --- a/contracts/test/libs/EllipticCurve.t.sol +++ b/contracts/test/libs/EllipticCurve.t.sol @@ -107,7 +107,8 @@ contract EllipticCurvePropertiesTest is Test { EllipticCurve.ecAdd({_x1: x, _y1: y, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); assertTrue( - Delta.isZero(Delta.CurvePoint(resultX, resultY)), "GROUP PROPERTY: P + (-P) must equal point at infinity" + _isPointAtInfinity(Delta.CurvePoint(resultX, resultY)), + "GROUP PROPERTY: P + (-P) must equal point at infinity" ); } @@ -167,7 +168,7 @@ contract EllipticCurvePropertiesTest is Test { EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: x2, _y2: y2, _aa: Delta._AA, _pp: Delta._PP}); // Result must be on curve or at infinity - bool atInfinity = Delta.isZero(Delta.CurvePoint(resultX, resultY)); + bool atInfinity = _isPointAtInfinity(Delta.CurvePoint(resultX, resultY)); bool onCurve = EllipticCurve.isOnCurve({_x: resultX, _y: resultY, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}); @@ -187,7 +188,7 @@ contract EllipticCurvePropertiesTest is Test { // P + (-P) should equal point at infinity assertTrue( - Delta.isZero(Delta.CurvePoint(rx, ry)), + _isPointAtInfinity(Delta.CurvePoint(rx, ry)), "With REDUCED coordinates: (3,5) + (3,2) correctly gives point at infinity" ); } @@ -224,7 +225,7 @@ contract EllipticCurvePropertiesTest is Test { (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); - assertTrue(Delta.isZero(Delta.CurvePoint(rx, ry)), "G + (-G) should be point at infinity"); + assertTrue(_isPointAtInfinity(Delta.CurvePoint(rx, ry)), "G + (-G) should be point at infinity"); } /// @notice Concrete: (G + 2G) + 3G = G + (2G + 3G) = 6G @@ -259,4 +260,11 @@ contract EllipticCurvePropertiesTest is Test { assertEq(result1X, p6gx, "Result should be 6G"); assertEq(result1Y, p6gy, "Result should be 6G"); } + + /// @notice Returns whether a point is at infinity or not.. + /// @param p The point to check. + /// @return isAtInfinity Whether the point is at infinity or not. + function _isPointAtInfinity(Delta.CurvePoint memory p) internal pure returns (bool isAtInfinity) { + isAtInfinity = p.x == 0 && p.y == 0; + } } From f5096be3a4048fff56c97f7b1249768ec68a296c Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 4 Nov 2025 17:35:26 +0100 Subject: [PATCH 04/12] refactor: rename struct --- contracts/src/ProtocolAdapter.sol | 6 ++--- contracts/src/libs/proving/Delta.sol | 16 ++++++------ contracts/test/libs/DeltaGen.sol | 4 +-- contracts/test/libs/EllipticCurve.t.sol | 11 ++++---- contracts/test/libs/TxGen.sol | 2 +- contracts/test/proofs/DeltaProof.t.sol | 34 ++++++++++++++----------- 6 files changed, 38 insertions(+), 35 deletions(-) diff --git a/contracts/src/ProtocolAdapter.sol b/contracts/src/ProtocolAdapter.sol index 7d41b313..63ce4bf7 100644 --- a/contracts/src/ProtocolAdapter.sol +++ b/contracts/src/ProtocolAdapter.sol @@ -35,7 +35,7 @@ contract ProtocolAdapter is CommitmentTree, NullifierSet { - using Delta for Delta.CurvePoint; + using Delta for Delta.Point; using MerkleTree for bytes32[]; using Logic for Logic.VerifierInput[]; using Logic for Logic.VerifierInput; @@ -61,7 +61,7 @@ contract ProtocolAdapter is bytes32[] tags; bytes32[] logicRefs; bytes32 latestCommitmentTreeRoot; - Delta.CurvePoint transactionDelta; + Delta.Point transactionDelta; uint256 tagCounter; /* Proof aggregation-related variables */ bool isProofAggregated; @@ -146,7 +146,7 @@ contract ProtocolAdapter is // Add the unit delta to the transaction delta. vars.transactionDelta = vars.transactionDelta .add( - Delta.CurvePoint({ + Delta.Point({ x: uint256(complianceVerifierInput.instance.unitDeltaX), y: uint256(complianceVerifierInput.instance.unitDeltaY) }) diff --git a/contracts/src/libs/proving/Delta.sol b/contracts/src/libs/proving/Delta.sol index 40cc64ed..5216b795 100644 --- a/contracts/src/libs/proving/Delta.sol +++ b/contracts/src/libs/proving/Delta.sol @@ -10,12 +10,12 @@ import {EfficientHashLib} from "@solady/utils/EfficientHashLib.sol"; /// @notice A library containing methods of the delta proving system. /// @custom:security-contact security@anoma.foundation library Delta { - using Delta for CurvePoint; + using Delta for Point; /// @notice An elliptic curve point representing a delta value. /// @param x The x component of the point. /// @param y The y component of the point. - struct CurvePoint { + struct Point { uint256 x; uint256 y; } @@ -39,19 +39,19 @@ library Delta { error DeltaMismatch(address expected, address actual); /// @notice Thrown when a provided point is not on the curve. - error PointNotOnCurve(CurvePoint point); + error PointNotOnCurve(Point point); /// @notice Returns the elliptic curve point representing the zero delta. /// @return zeroDelta The zero delta. - function zero() internal pure returns (CurvePoint memory zeroDelta) { - zeroDelta = CurvePoint({x: 0, y: 0}); + function zero() internal pure returns (Point memory zeroDelta) { + zeroDelta = Point({x: 0, y: 0}); } /// @notice Adds two elliptic curve points and returns the resulting value. /// @param p1 The first curve point. /// @param p2 The second curve point. /// @return sum The resulting curve point. - function add(CurvePoint memory p1, CurvePoint memory p2) internal pure returns (CurvePoint memory sum) { + function add(Point memory p1, Point memory p2) internal pure returns (Point memory sum) { if (!EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: _AA, _bb: _BB, _pp: _PP})) { revert PointNotOnCurve(p2); } @@ -62,7 +62,7 @@ library Delta { /// @notice Converts an elliptic curve point to an Ethereum account address. /// @param delta The elliptic curve point. /// @return account The associated account. - function toAccount(CurvePoint memory delta) internal pure returns (address account) { + function toAccount(Point memory delta) internal pure returns (address account) { // Hash the public key with Keccak-256. bytes32 hashedKey = EfficientHashLib.hash(delta.x, delta.y); @@ -82,7 +82,7 @@ library Delta { /// @param proof The delta proof. /// @param instance The transaction delta. /// @param verifyingKey The Keccak-256 hash of all nullifiers and commitments as ordered in the compliance units. - function verify(bytes memory proof, CurvePoint memory instance, bytes32 verifyingKey) internal pure { + function verify(bytes memory proof, Point memory instance, bytes32 verifyingKey) internal pure { // Verify the delta proof using the ECDSA.recover API to obtain the address address recovered = ECDSA.recover({hash: verifyingKey, signature: proof}); diff --git a/contracts/test/libs/DeltaGen.sol b/contracts/test/libs/DeltaGen.sol index da3fc414..fc1f02fc 100644 --- a/contracts/test/libs/DeltaGen.sol +++ b/contracts/test/libs/DeltaGen.sol @@ -49,7 +49,7 @@ library DeltaGen { /// @return instance The delta instance corresponding to the parameters function generateInstance(VmSafe vm, InstanceInputs memory deltaInputs) internal - returns (Delta.CurvePoint memory instance) + returns (Delta.Point memory instance) { deltaInputs.valueCommitmentRandomness = deltaInputs.valueCommitmentRandomness.modOrder(); if (deltaInputs.valueCommitmentRandomness == 0) { @@ -69,7 +69,7 @@ library DeltaGen { VmSafe.Wallet memory valueWallet = vm.createWallet(preDelta); // Extract the transaction delta from the wallet - instance = Delta.CurvePoint({x: valueWallet.publicKeyX, y: valueWallet.publicKeyY}); + instance = Delta.Point({x: valueWallet.publicKeyX, y: valueWallet.publicKeyY}); } /// @notice Generates a transaction delta proof by signing verifyingKey with diff --git a/contracts/test/libs/EllipticCurve.t.sol b/contracts/test/libs/EllipticCurve.t.sol index 11f699c7..8dbcb923 100644 --- a/contracts/test/libs/EllipticCurve.t.sol +++ b/contracts/test/libs/EllipticCurve.t.sol @@ -107,8 +107,7 @@ contract EllipticCurvePropertiesTest is Test { EllipticCurve.ecAdd({_x1: x, _y1: y, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); assertTrue( - _isPointAtInfinity(Delta.CurvePoint(resultX, resultY)), - "GROUP PROPERTY: P + (-P) must equal point at infinity" + _isPointAtInfinity(Delta.Point(resultX, resultY)), "GROUP PROPERTY: P + (-P) must equal point at infinity" ); } @@ -168,7 +167,7 @@ contract EllipticCurvePropertiesTest is Test { EllipticCurve.ecAdd({_x1: x1, _y1: y1, _x2: x2, _y2: y2, _aa: Delta._AA, _pp: Delta._PP}); // Result must be on curve or at infinity - bool atInfinity = _isPointAtInfinity(Delta.CurvePoint(resultX, resultY)); + bool atInfinity = _isPointAtInfinity(Delta.Point(resultX, resultY)); bool onCurve = EllipticCurve.isOnCurve({_x: resultX, _y: resultY, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}); @@ -188,7 +187,7 @@ contract EllipticCurvePropertiesTest is Test { // P + (-P) should equal point at infinity assertTrue( - _isPointAtInfinity(Delta.CurvePoint(rx, ry)), + _isPointAtInfinity(Delta.Point(rx, ry)), "With REDUCED coordinates: (3,5) + (3,2) correctly gives point at infinity" ); } @@ -225,7 +224,7 @@ contract EllipticCurvePropertiesTest is Test { (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: invX, _y2: invY, _aa: Delta._AA, _pp: Delta._PP}); - assertTrue(_isPointAtInfinity(Delta.CurvePoint(rx, ry)), "G + (-G) should be point at infinity"); + assertTrue(_isPointAtInfinity(Delta.Point(rx, ry)), "G + (-G) should be point at infinity"); } /// @notice Concrete: (G + 2G) + 3G = G + (2G + 3G) = 6G @@ -264,7 +263,7 @@ contract EllipticCurvePropertiesTest is Test { /// @notice Returns whether a point is at infinity or not.. /// @param p The point to check. /// @return isAtInfinity Whether the point is at infinity or not. - function _isPointAtInfinity(Delta.CurvePoint memory p) internal pure returns (bool isAtInfinity) { + function _isPointAtInfinity(Delta.Point memory p) internal pure returns (bool isAtInfinity) { isAtInfinity = p.x == 0 && p.y == 0; } } diff --git a/contracts/test/libs/TxGen.sol b/contracts/test/libs/TxGen.sol index 0569c160..86c78622 100644 --- a/contracts/test/libs/TxGen.sol +++ b/contracts/test/libs/TxGen.sol @@ -50,7 +50,7 @@ library TxGen { bytes32 cm = commitment(created); // Construct the delta for consumption based on kind and quantity - Delta.CurvePoint memory unitDelta = DeltaGen.generateInstance( + Delta.Point memory unitDelta = DeltaGen.generateInstance( vm, DeltaGen.InstanceInputs({ kind: kind(consumed), quantity: consumed.quantity, consumed: true, valueCommitmentRandomness: 1 diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index 316ce573..1a1d4bc2 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -20,14 +20,14 @@ library DeltaFuzzing { } /// @dev This function exposes `Delta.verify` for the fuzzer. - function verify(bytes memory proof, Delta.CurvePoint memory instance, bytes32 verifyingKey) public pure { + function verify(bytes memory proof, Delta.Point memory instance, bytes32 verifyingKey) public pure { Delta.verify({proof: proof, instance: instance, verifyingKey: verifyingKey}); } } contract DeltaProofTest is Test { using SignMagnitude for SignMagnitude.Number; - using Delta for Delta.CurvePoint; + using Delta for Delta.Point; using DeltaGen for DeltaGen.InstanceInputs[]; using DeltaGen for DeltaGen.InstanceInputs; using DeltaGen for uint256; @@ -52,7 +52,7 @@ contract DeltaProofTest is Test { DeltaGen.ProofInputs({valueCommitmentRandomness: valueCommitmentRandomness, verifyingKey: verifyingKey}); // Generate a delta instance from the above inputs - Delta.CurvePoint memory instance = DeltaGen.generateInstance(vm, deltaInstanceInputs); + Delta.Point memory instance = DeltaGen.generateInstance(vm, deltaInstanceInputs); // Generate a delta proof from the above inputs bytes memory proof = DeltaGen.generateProof(vm, deltaProofInputs); @@ -110,12 +110,12 @@ contract DeltaProofTest is Test { vm.assume(summedDeltaInputs.computePreDelta() != 0); // Generate a delta proof and instance from the above tags and preimage - Delta.CurvePoint memory instance1 = DeltaGen.generateInstance(vm, deltaInputs1); - Delta.CurvePoint memory instance2 = DeltaGen.generateInstance(vm, deltaInputs2); - Delta.CurvePoint memory expectedDelta = DeltaGen.generateInstance(vm, summedDeltaInputs); + Delta.Point memory instance1 = DeltaGen.generateInstance(vm, deltaInputs1); + Delta.Point memory instance2 = DeltaGen.generateInstance(vm, deltaInputs2); + Delta.Point memory expectedDelta = DeltaGen.generateInstance(vm, summedDeltaInputs); // Verify that the deltas add correctly - Delta.CurvePoint memory computedDelta = Delta.add(instance1, instance2); + Delta.Point memory computedDelta = Delta.add(instance1, instance2); assertEq(computedDelta.x, expectedDelta.x); assertEq(computedDelta.y, expectedDelta.y); @@ -140,7 +140,7 @@ contract DeltaProofTest is Test { }); // Generate a delta proof and instance from the above tags and preimage - Delta.CurvePoint memory instance = DeltaGen.generateInstance(vm, deltaInstanceInputs); + Delta.Point memory instance = DeltaGen.generateInstance(vm, deltaInstanceInputs); bytes memory proof = DeltaGen.generateProof(vm, deltaProofInputs); vm.expectPartialRevert(Delta.DeltaMismatch.selector); DeltaFuzzing.verify({proof: proof, instance: instance, verifyingKey: deltaProofInputs.verifyingKey}); @@ -165,7 +165,7 @@ contract DeltaProofTest is Test { DeltaGen.ProofInputs({valueCommitmentRandomness: valueCommitmentRandomness1, verifyingKey: verifyingKey}) ); - Delta.CurvePoint memory instanceRcv2; + Delta.Point memory instanceRcv2; { DeltaGen.InstanceInputs memory deltaInputs2 = DeltaGen.InstanceInputs({ kind: kind, quantity: 0, consumed: consumed, valueCommitmentRandomness: valueCommitmentRandomness2 @@ -195,7 +195,7 @@ contract DeltaProofTest is Test { DeltaGen.ProofInputs({valueCommitmentRandomness: valueCommitmentRandomness, verifyingKey: verifyingKey1}) ); - Delta.CurvePoint memory instance; + Delta.Point memory instance; { DeltaGen.InstanceInputs memory deltaInputs2 = DeltaGen.InstanceInputs({ kind: kind, quantity: 0, consumed: consumed, valueCommitmentRandomness: valueCommitmentRandomness @@ -218,7 +218,7 @@ contract DeltaProofTest is Test { kind = bound(kind, 1, DeltaGen.SECP256K1_ORDER - 1); DeltaGen.InstanceInputs[] memory deltaInputs = _getBoundedDeltaInstances(kind, fuzzerInputs); - Delta.CurvePoint memory deltaAcc = Delta.zero(); + Delta.Point memory deltaAcc = Delta.zero(); // Make sure that the delta quantities balance out ( @@ -248,7 +248,7 @@ contract DeltaProofTest is Test { vm.assume(wrappedDeltaInputs[i].valueCommitmentRandomness != 0); vm.assume(wrappedDeltaInputs[i].computePreDelta() != 0); - Delta.CurvePoint memory instance = DeltaGen.generateInstance(vm, wrappedDeltaInputs[i]); + Delta.Point memory instance = DeltaGen.generateInstance(vm, wrappedDeltaInputs[i]); deltaAcc = deltaAcc.add(instance); } @@ -270,7 +270,7 @@ contract DeltaProofTest is Test { kind = bound(kind, 1, DeltaGen.SECP256K1_ORDER - 1); DeltaGen.InstanceInputs[] memory deltaInputs = _getBoundedDeltaInstances(kind, fuzzerInputs); - Delta.CurvePoint memory deltaAcc = Delta.zero(); + Delta.Point memory deltaAcc = Delta.zero(); // Accumulate the total quantity and randomness commitment ( @@ -289,7 +289,7 @@ contract DeltaProofTest is Test { vm.assume(wrappedDeltaInputs[i].valueCommitmentRandomness != 0); vm.assume(wrappedDeltaInputs[i].computePreDelta() != 0); - Delta.CurvePoint memory instance = DeltaGen.generateInstance(vm, wrappedDeltaInputs[i]); + Delta.Point memory instance = DeltaGen.generateInstance(vm, wrappedDeltaInputs[i]); deltaAcc = deltaAcc.add(instance); } // Compute the proof for the balanced transaction @@ -307,7 +307,7 @@ contract DeltaProofTest is Test { DeltaFuzzing.verify({ proof: txn.deltaProof, - instance: Delta.CurvePoint({ + instance: Delta.Point({ x: uint256(txn.actions[0].complianceVerifierInputs[0].instance.unitDeltaX), y: uint256(txn.actions[0].complianceVerifierInputs[0].instance.unitDeltaY) }), @@ -338,4 +338,8 @@ contract DeltaProofTest is Test { }); } } + + function _mul(Delta.Point memory p, uint256 k) internal pure returns (Delta.Point memory product) { + (product.x, product.y) = EllipticCurve.ecMul({_k: k, _x: p.x, _y: p.y, _aa: Delta._AA, _pp: Delta._PP}); + } } From fe5cdbe95cf73ee6a566fd6b17ce9e2f02c56c87 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Tue, 4 Nov 2025 17:43:58 +0100 Subject: [PATCH 05/12] feat: add tests --- contracts/test/proofs/DeltaProof.t.sol | 61 ++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index 1a1d4bc2..b8a5f090 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -1,6 +1,7 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.30; +import {EllipticCurve} from "@elliptic-curve-solidity/contracts/EllipticCurve.sol"; import {Test} from "forge-std/Test.sol"; import {Delta} from "../../src/libs/proving/Delta.sol"; @@ -302,6 +303,66 @@ contract DeltaProofTest is Test { DeltaFuzzing.verify({proof: proof, instance: deltaAcc, verifyingKey: verifyingKey}); } + function test_add_reverts_if_the_second_point_is_not_on_the_curve(uint8 k) public { + // Generate a random point on the curve. + Delta.Point memory p1 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + assertTrue( + EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Point 1 must be on the curve." + ); + + Delta.Point memory p2 = Delta.zero(); + + // Attempt to add a point being not on the curve + vm.expectRevert(abi.encodeWithSelector(Delta.PointNotOnCurve.selector, p2), address(this)); + p1.add(p2); + } + + function test_add_adding_two_curve_points_produces_a_point_on_the_curve(uint8 k1, uint8 k2) public pure { + // Generate two random points on the curve. + Delta.Point memory p1 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); + assertTrue( + EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Point 1 must be on the curve." + ); + + Delta.Point memory p2 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k2}); + assertTrue( + EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Point 2 must be on the curve." + ); + + // Add the two points. + Delta.Point memory sum = Delta.add(p1, p2); + assertTrue( + EllipticCurve.isOnCurve({_x: sum.x, _y: sum.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Sum must be on the curve." + ); + } + + function test_add_adding_a_curve_point_to_zero_produces_a_point_on_the_curve(uint8 k) public pure { + Delta.Point memory zero = Delta.zero(); + + // Generate a random point on the curve. + Delta.Point memory p = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + assertTrue( + EllipticCurve.isOnCurve({_x: p.x, _y: p.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "The point must be on the curve." + ); + + // Add the two points. + Delta.Point memory sum = zero.add(p); + assertTrue( + EllipticCurve.isOnCurve({_x: sum.x, _y: sum.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Sum must be on the curve." + ); + } + + function test_zero_is_not_on_the_curve() public pure { + Delta.Point memory p2 = Delta.zero(); + assertFalse(EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + } + function test_verify_example_delta_proof() public pure { Transaction memory txn = TransactionExample.transaction(); From 89b149c6d15ebfd2c5c7f1a0190f5a8903387bff Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 09:41:05 +0100 Subject: [PATCH 06/12] refactor: docs, variable names, and tests --- contracts/src/libs/proving/Delta.sol | 15 ++++--- contracts/test/proofs/DeltaProof.t.sol | 62 +++++++++++++------------- 2 files changed, 38 insertions(+), 39 deletions(-) diff --git a/contracts/src/libs/proving/Delta.sol b/contracts/src/libs/proving/Delta.sol index 5216b795..c3605e14 100644 --- a/contracts/src/libs/proving/Delta.sol +++ b/contracts/src/libs/proving/Delta.sol @@ -47,16 +47,17 @@ library Delta { zeroDelta = Point({x: 0, y: 0}); } - /// @notice Adds two elliptic curve points and returns the resulting value. - /// @param p1 The first curve point. - /// @param p2 The second curve point. + /// @notice Adds two delta points and returns the sum. + /// @param lhs The left-hand side point that can also be the zero delta. + /// @param rhs The right-hand side point that must be a curve point. /// @return sum The resulting curve point. - function add(Point memory p1, Point memory p2) internal pure returns (Point memory sum) { - if (!EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: _AA, _bb: _BB, _pp: _PP})) { - revert PointNotOnCurve(p2); + /// @dev Note that only the right-hand side point is checked to allow adding the zero delta from the left. + function add(Point memory lhs, Point memory rhs) internal pure returns (Point memory sum) { + if (!EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: _AA, _bb: _BB, _pp: _PP})) { + revert PointNotOnCurve(rhs); } - (sum.x, sum.y) = EllipticCurve.ecAdd({_x1: p1.x, _y1: p1.y, _x2: p2.x, _y2: p2.y, _aa: _AA, _pp: _PP}); + (sum.x, sum.y) = EllipticCurve.ecAdd({_x1: lhs.x, _y1: lhs.y, _x2: rhs.x, _y2: rhs.y, _aa: _AA, _pp: _PP}); } /// @notice Converts an elliptic curve point to an Ethereum account address. diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index b8a5f090..511cc775 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -303,55 +303,53 @@ contract DeltaProofTest is Test { DeltaFuzzing.verify({proof: proof, instance: deltaAcc, verifyingKey: verifyingKey}); } - function test_add_reverts_if_the_second_point_is_not_on_the_curve(uint8 k) public { + function test_add_reverts_when_adding_a_non_curve_from_the_right(uint8 k, Delta.Point memory rhs) public { + // Ensure that `rhs` is not on the curve. + vm.assume(!EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); + // Generate a random point on the curve. - Delta.Point memory p1 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); assertTrue( - EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), - "Point 1 must be on the curve." + EllipticCurve.isOnCurve({_x: lhs.x, _y: lhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Left-hand side point must be on the curve." ); - Delta.Point memory p2 = Delta.zero(); - - // Attempt to add a point being not on the curve - vm.expectRevert(abi.encodeWithSelector(Delta.PointNotOnCurve.selector, p2), address(this)); - p1.add(p2); + // Attempt to add a point being not on the curve. + vm.expectRevert(abi.encodeWithSelector(Delta.PointNotOnCurve.selector, rhs), address(this)); + lhs.add(rhs); } - function test_add_adding_two_curve_points_produces_a_point_on_the_curve(uint8 k1, uint8 k2) public pure { - // Generate two random points on the curve. - Delta.Point memory p1 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); + function test_add_reverts_when_adding_zero_from_the_right(uint8 k) public { + // Generate a random point on the curve. + Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); assertTrue( - EllipticCurve.isOnCurve({_x: p1.x, _y: p1.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), - "Point 1 must be on the curve." + EllipticCurve.isOnCurve({_x: lhs.x, _y: lhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Left-hand side point must be on the curve." ); - Delta.Point memory p2 = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k2}); - assertTrue( - EllipticCurve.isOnCurve({_x: p2.x, _y: p2.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), - "Point 2 must be on the curve." - ); + Delta.Point memory zero = Delta.zero(); // Add the two points. - Delta.Point memory sum = Delta.add(p1, p2); - assertTrue( - EllipticCurve.isOnCurve({_x: sum.x, _y: sum.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), - "Sum must be on the curve." - ); + vm.expectRevert(abi.encodeWithSelector(Delta.PointNotOnCurve.selector, zero), address(this)); + lhs.add(zero); } - function test_add_adding_a_curve_point_to_zero_produces_a_point_on_the_curve(uint8 k) public pure { - Delta.Point memory zero = Delta.zero(); + function test_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { + // Generate two random points on the curve. + Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); + assertTrue( + EllipticCurve.isOnCurve({_x: lhs.x, _y: lhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Left-hand side point must be on the curve." + ); - // Generate a random point on the curve. - Delta.Point memory p = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + Delta.Point memory rhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k2}); assertTrue( - EllipticCurve.isOnCurve({_x: p.x, _y: p.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), - "The point must be on the curve." + EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Right-hand side must be on the curve." ); - // Add the two points. - Delta.Point memory sum = zero.add(p); + // Add the two points and check that the sum is a curve point. + Delta.Point memory sum = lhs.add(rhs); assertTrue( EllipticCurve.isOnCurve({_x: sum.x, _y: sum.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), "Sum must be on the curve." From 9260e53d3cf7d76409939fa27d54a9b033d7b98a Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 09:51:02 +0100 Subject: [PATCH 07/12] style: harmonize test names --- contracts/test/libs/EllipticCurve.t.sol | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/contracts/test/libs/EllipticCurve.t.sol b/contracts/test/libs/EllipticCurve.t.sol index 8dbcb923..98fec42c 100644 --- a/contracts/test/libs/EllipticCurve.t.sol +++ b/contracts/test/libs/EllipticCurve.t.sol @@ -24,7 +24,7 @@ contract EllipticCurvePropertiesTest is Test { /// @notice GROUP PROPERTY: Commutativity - P + Q = Q + P /// @dev Uses secp256k1 with points VERIFIED to be on the curve - function testGroupProperty_Commutativity_Secp256k1(uint256 scalar1, uint256 scalar2) public pure { + function testFuzz_group_property_commutativity_secp256k1(uint256 scalar1, uint256 scalar2) public pure { // Generate two points on secp256k1 by scalar multiplication of G scalar1 = bound(scalar1, 1, 20); // Keep small for performance scalar2 = bound(scalar2, 1, 20); @@ -58,7 +58,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice GROUP PROPERTY: Identity - P + O = P - function testGroupProperty_Identity_Secp256k1(uint256 scalar) public pure { + function testFuzz_group_property_identity_secp256k1(uint256 scalar) public pure { scalar = bound(scalar, 1, 20); // P = scalar * G (guaranteed on curve) @@ -80,7 +80,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice GROUP PROPERTY: Inverse - P + (-P) = O - function testGroupProperty_Inverse_Secp256k1(uint256 scalar) public pure { + function testFuzz_group_property_inverse_secp256k1(uint256 scalar) public pure { scalar = bound(scalar, 1, 20); // P = scalar * G (guaranteed on curve) @@ -112,7 +112,10 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice GROUP PROPERTY: Associativity - (P + Q) + R = P + (Q + R) - function testGroupProperty_Associativity_Secp256k1(uint256 scalar1, uint256 scalar2, uint256 scalar3) public pure { + function testFuzz_group_property_associativity_secp256k1(uint256 scalar1, uint256 scalar2, uint256 scalar3) + public + pure + { scalar1 = bound(scalar1, 1, 10); // Keep small for performance scalar2 = bound(scalar2, 1, 10); scalar3 = bound(scalar3, 1, 10); @@ -148,7 +151,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice GROUP PROPERTY: Closure - If P, Q on curve, then P+Q on curve or at infinity - function testGroupProperty_Closure_Secp256k1(uint256 scalar1, uint256 scalar2) public pure { + function testFuzz_group_property_Closure_secp256k1(uint256 scalar1, uint256 scalar2) public pure { scalar1 = bound(scalar1, 1, 20); scalar2 = bound(scalar2, 1, 20); @@ -177,7 +180,7 @@ contract EllipticCurvePropertiesTest is Test { /* PRECONDITION TESTS (UNREDUCED COORDINATES) */ /// @notice PRECONDITION TEST: Demonstrates correct behavior with reduced coordinates - function test_Precondition_ReducedCoordinates() public pure { + function test_precondition_reduced_coordinates() public pure { // Use a small prime: p = 7, curve parameter a = 2 uint256 pp = 7; uint256 aa = 2; @@ -195,7 +198,7 @@ contract EllipticCurvePropertiesTest is Test { /* CONCRETE TESTS (SECP256K1) */ /// @notice Concrete: G + G = 2G - function test_Concrete_Doubling_Secp256k1() public pure { + function test_doubling_secp256k1() public pure { (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({ _x1: Delta._GX, _y1: Delta._GY, _x2: Delta._GX, _y2: Delta._GY, _aa: Delta._AA, _pp: Delta._PP }); @@ -209,7 +212,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice Concrete: G + O = G - function test_Concrete_Identity_Secp256k1() public pure { + function test_group_property_identity_secp256k1() public pure { (uint256 rx, uint256 ry) = EllipticCurve.ecAdd({_x1: Delta._GX, _y1: Delta._GY, _x2: 0, _y2: 0, _aa: Delta._AA, _pp: Delta._PP}); @@ -218,7 +221,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice Concrete: G + (-G) = O - function test_Concrete_Inverse_Secp256k1() public pure { + function test_inverse_secp256k1() public pure { (uint256 invX, uint256 invY) = EllipticCurve.ecInv({_x: Delta._GX, _y: Delta._GY, _pp: Delta._PP}); (uint256 rx, uint256 ry) = @@ -228,7 +231,7 @@ contract EllipticCurvePropertiesTest is Test { } /// @notice Concrete: (G + 2G) + 3G = G + (2G + 3G) = 6G - function test_Concrete_Associativity_Secp256k1() public pure { + function test_associativity_secp256k1() public pure { // Compute 2G, 3G (uint256 p2gX, uint256 p2gY) = From 995ef2e4c8e5a4001e77065bd073e1338029fa6d Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 09:51:12 +0100 Subject: [PATCH 08/12] chore: add author --- contracts/test/libs/EllipticCurve.t.sol | 1 + 1 file changed, 1 insertion(+) diff --git a/contracts/test/libs/EllipticCurve.t.sol b/contracts/test/libs/EllipticCurve.t.sol index 98fec42c..e176eced 100644 --- a/contracts/test/libs/EllipticCurve.t.sol +++ b/contracts/test/libs/EllipticCurve.t.sol @@ -8,6 +8,7 @@ import {Delta} from "../../src/libs/proving/Delta.sol"; /** * @title EllipticCurvePropertiesTest + * @author Informal Systems * @dev Property-based tests for EllipticCurve.ecAdd using Foundry fuzzing. * * These tests verify GROUP PROPERTIES with points VERIFIED to be on the curve. From 16c9b68c089067b36ebf5f2fafbd1372ea026bda Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 12:37:29 +0100 Subject: [PATCH 09/12] doc: improve comment --- contracts/src/libs/proving/Delta.sol | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/contracts/src/libs/proving/Delta.sol b/contracts/src/libs/proving/Delta.sol index c3605e14..55cb12ab 100644 --- a/contracts/src/libs/proving/Delta.sol +++ b/contracts/src/libs/proving/Delta.sol @@ -51,7 +51,9 @@ library Delta { /// @param lhs The left-hand side point that can also be the zero delta. /// @param rhs The right-hand side point that must be a curve point. /// @return sum The resulting curve point. - /// @dev Note that only the right-hand side point is checked to allow adding the zero delta from the left. + /// @dev Note that only the right-hand side point is checked to allow adding the zero delta from the left. This is + /// done due to the delta points being added sequentially starting from the zero delta in the + /// `ProtocolAdapter.execute()` function. function add(Point memory lhs, Point memory rhs) internal pure returns (Point memory sum) { if (!EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: _AA, _bb: _BB, _pp: _PP})) { revert PointNotOnCurve(rhs); From ee556a8c339d5d6d1bc9628743328ec988028c0a Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 12:59:56 +0100 Subject: [PATCH 10/12] refactor: fix fuzz test name prefixes --- contracts/test/proofs/DeltaProof.t.sol | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index 511cc775..a853256c 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -303,7 +303,7 @@ contract DeltaProofTest is Test { DeltaFuzzing.verify({proof: proof, instance: deltaAcc, verifyingKey: verifyingKey}); } - function test_add_reverts_when_adding_a_non_curve_from_the_right(uint8 k, Delta.Point memory rhs) public { + function testFuzz_add_reverts_when_adding_a_non_curve_from_the_right(uint8 k, Delta.Point memory rhs) public { // Ensure that `rhs` is not on the curve. vm.assume(!EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); @@ -319,7 +319,7 @@ contract DeltaProofTest is Test { lhs.add(rhs); } - function test_add_reverts_when_adding_zero_from_the_right(uint8 k) public { + function testFuzz_add_reverts_when_adding_zero_from_the_right(uint8 k) public { // Generate a random point on the curve. Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); assertTrue( @@ -335,6 +335,7 @@ contract DeltaProofTest is Test { } function test_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { + function testFuzz_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { // Generate two random points on the curve. Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); assertTrue( From 6d7c7ac03dfe0f74c5f0338e2471344843001591 Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 13:00:41 +0100 Subject: [PATCH 11/12] feat: add tests --- contracts/test/proofs/DeltaProof.t.sol | 35 +++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index a853256c..bed1eebc 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -334,7 +334,40 @@ contract DeltaProofTest is Test { lhs.add(zero); } - function test_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { + function testFuzz_add_adding_zero_from_the_left_produces_a_curve_point(uint8 k) public pure { + Delta.Point memory zero = Delta.zero(); + + // Generate a random point on the curve. + Delta.Point memory rhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + assertTrue( + EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Right-hand side point must be on the curve." + ); + + // Add the two points and check that the sum is a curve point. + Delta.Point memory sum = zero.add(rhs); + assertTrue( + EllipticCurve.isOnCurve({_x: sum.x, _y: sum.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Sum must be on the curve." + ); + } + + function testFuzz_add_adding_zero_from_the_left_is_the_identity_operation(uint8 k) public pure { + Delta.Point memory zero = Delta.zero(); + + // Generate a random point on the curve. + Delta.Point memory rhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); + assertTrue( + EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP}), + "Right-hand side point must be on the curve." + ); + + // Add the two points and check that the sum is a curve point. + Delta.Point memory sum = zero.add(rhs); + assertEq(sum.x, rhs.x); + assertEq(sum.y, rhs.y); + } + function testFuzz_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { // Generate two random points on the curve. Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); From 95f23e103b6fe97ed53b425b648a310b1e18777b Mon Sep 17 00:00:00 2001 From: Michael Heuer Date: Wed, 5 Nov 2025 19:48:18 +0100 Subject: [PATCH 12/12] refactor: increase fuzzing space --- contracts/test/proofs/DeltaProof.t.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/test/proofs/DeltaProof.t.sol b/contracts/test/proofs/DeltaProof.t.sol index bed1eebc..e07c899a 100644 --- a/contracts/test/proofs/DeltaProof.t.sol +++ b/contracts/test/proofs/DeltaProof.t.sol @@ -303,7 +303,7 @@ contract DeltaProofTest is Test { DeltaFuzzing.verify({proof: proof, instance: deltaAcc, verifyingKey: verifyingKey}); } - function testFuzz_add_reverts_when_adding_a_non_curve_from_the_right(uint8 k, Delta.Point memory rhs) public { + function testFuzz_add_reverts_when_adding_a_non_curve_from_the_right(uint32 k, Delta.Point memory rhs) public { // Ensure that `rhs` is not on the curve. vm.assume(!EllipticCurve.isOnCurve({_x: rhs.x, _y: rhs.y, _aa: Delta._AA, _bb: Delta._BB, _pp: Delta._PP})); @@ -319,7 +319,7 @@ contract DeltaProofTest is Test { lhs.add(rhs); } - function testFuzz_add_reverts_when_adding_zero_from_the_right(uint8 k) public { + function testFuzz_add_reverts_when_adding_zero_from_the_right(uint32 k) public { // Generate a random point on the curve. Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k}); assertTrue( @@ -334,7 +334,7 @@ contract DeltaProofTest is Test { lhs.add(zero); } - function testFuzz_add_adding_zero_from_the_left_produces_a_curve_point(uint8 k) public pure { + function testFuzz_add_adding_zero_from_the_left_produces_a_curve_point(uint32 k) public pure { Delta.Point memory zero = Delta.zero(); // Generate a random point on the curve. @@ -352,7 +352,7 @@ contract DeltaProofTest is Test { ); } - function testFuzz_add_adding_zero_from_the_left_is_the_identity_operation(uint8 k) public pure { + function testFuzz_add_adding_zero_from_the_left_is_the_identity_operation(uint32 k) public pure { Delta.Point memory zero = Delta.zero(); // Generate a random point on the curve. @@ -368,7 +368,7 @@ contract DeltaProofTest is Test { assertEq(sum.y, rhs.y); } - function testFuzz_add_adding_two_curve_points_produces_a_curve_point(uint8 k1, uint8 k2) public pure { + function testFuzz_add_adding_two_curve_points_produces_a_curve_point(uint32 k1, uint32 k2) public pure { // Generate two random points on the curve. Delta.Point memory lhs = _mul({p: Delta.Point({x: Delta._GX, y: Delta._GY}), k: k1}); assertTrue(