From 901e96987786837c137153657230fc9e977c36e1 Mon Sep 17 00:00:00 2001 From: Uncle Fatso Date: Mon, 13 Oct 2025 19:58:28 +0300 Subject: [PATCH] simultaneous multiplication for triplets based on Straus-Shamir Trick Signed-off-by: Uncle Fatso --- README.md | 54 ++++++------ src/MathTester.sol | 35 ++++++++ src/libraries/ECMathProjective.sol | 129 +++++++++++++++++++++++++++++ test/MathTester.t.sol | 40 ++++++++- 4 files changed, 232 insertions(+), 26 deletions(-) diff --git a/README.md b/README.md index 0330b34..3ef6bd7 100644 --- a/README.md +++ b/README.md @@ -32,30 +32,36 @@ For full background and protocol details see the [project wiki](https://git.ghos `Jacobian` is the original implementation used as a reference implementation, while `Projective` is optimized one. -``` -╭----------------------------------------+-----------------+-------+--------+-------+---------╮ -| src/MathTester.sol:MathTester Contract | | | | | | -+=============================================================================================+ -| Deployment Cost | Deployment Size | | | | | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| 1065323 | 4715 | | | | | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| | | | | | | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| Function Name | Min | Avg | Median | Max | # Calls | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| addJacobian | 1768 | 1768 | 1768 | 1768 | 44 | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| addProjective | 992 | 992 | 992 | 992 | 44 | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| doubleJacobian | 777 | 777 | 777 | 777 | 45 | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| doubleProjective | 532 | 532 | 532 | 532 | 45 | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| toAffineJacobian | 30564 | 36206 | 36300 | 40841 | 89 | -|----------------------------------------+-----------------+-------+--------+-------+---------| -| toAffineProjective | 11838 | 13792 | 13796 | 15155 | 89 | -╰----------------------------------------+-----------------+-------+--------+-------+---------╯ +```bash +╭----------------------------------------+-----------------+---------+---------+---------+---------╮ +| src/MathTester.sol:MathTester Contract | | | | | | ++==================================================================================================+ +| Deployment Cost | Deployment Size | | | | | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| 2011993 | 9094 | | | | | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| | | | | | | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| Function Name | Min | Avg | Median | Max | # Calls | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| addJacobian | 1800 | 1800 | 1800 | 1800 | 88 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| addProjective | 1015 | 1015 | 1015 | 1015 | 44 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| addProjectiveMixed | 967 | 967 | 967 | 967 | 44 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| doubleJacobian | 794 | 794 | 794 | 794 | 45 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| doubleProjective | 546 | 546 | 546 | 546 | 45 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| mulEcTriplet | 205548 | 1140310 | 1546125 | 1958623 | 43 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| mulProjectiveTriplet | 6436 | 193298 | 334976 | 385511 | 43 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| toAffineJacobian | 40275 | 47740 | 47859 | 53863 | 133 | +|----------------------------------------+-----------------+---------+---------+---------+---------| +| toAffineProjective | 11056 | 13787 | 13904 | 16105 | 176 | +╰----------------------------------------+-----------------+---------+---------+---------+---------╯ ``` ## Contributing diff --git a/src/MathTester.sol b/src/MathTester.sol index f13406d..b3435e7 100644 --- a/src/MathTester.sol +++ b/src/MathTester.sol @@ -46,6 +46,41 @@ contract MathTester { return EllipticCurveProjective.projectiveAddMixed(x1, y1, 1, x2, y2); } + function mulEcTriplet( + uint256 x1, + uint256 y1, + uint256 k1, + uint256 x2, + uint256 y2, + uint256 k2, + uint256 x3, + uint256 y3, + uint256 k3 + ) public pure returns(uint256, uint256) { + (x1, y1) = EllipticCurve.ecMul(k1, x1, y1, A, P); + (x2, y2) = EllipticCurve.ecMul(k2, x2, y2, A, P); + (x3, y3) = EllipticCurve.ecMul(k3, x3, y3, A, P); + + (x1, y1) = EllipticCurve.ecAdd(x1, y1, x2, y2, A, P); + (x1, y1) = EllipticCurve.ecAdd(x1, y1, x3, y3, A, P); + + return (x1, y1); + } + + function mulProjectiveTriplet( + uint256 x1, + uint256 y1, + uint256 k1, + uint256 x2, + uint256 y2, + uint256 k2, + uint256 x3, + uint256 y3, + uint256 k3 + ) public pure returns(uint256, uint256, uint256) { + return EllipticCurveProjective.mulAddProjectiveTriplet(x1, y1, k1, x2, y2, k2, x3, y3, k3); + } + function toAffineJacobian(uint256 x, uint256 y, uint256 z) public pure returns (uint256, uint256) { return EllipticCurve.toAffine(x, y, z, P); } diff --git a/src/libraries/ECMathProjective.sol b/src/libraries/ECMathProjective.sol index 5649409..c09364c 100644 --- a/src/libraries/ECMathProjective.sol +++ b/src/libraries/ECMathProjective.sol @@ -2,6 +2,9 @@ pragma solidity ^0.8.0; +// TODO: are there better algorithms than Straus-Shamir trick for +// k1*P1 + k2*P2 + k3*P3 + // Vectors for secp256k1 are difficult to find. These are the vectors from: // https://web.archive.org/web/20190724010836/https://chuckbatson.wordpress.com/2014/11/26/secp256k1-test-vectors @@ -14,6 +17,132 @@ library EllipticCurveProjective { uint256 private constant B3 = 21; + function findMaxBitLength( + uint256 k1, + uint256 k2, + uint256 k3 + ) internal pure returns (uint256 bits) { + assembly { + // Find maximum of the three scalars + let max := k1 + if gt(k2, max) { max := k2 } + if gt(k3, max) { max := k3 } + + // if (v >> 128 != 0) { v >>= 128; bits += 128; } + if gt(max, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF) { + max := shr(128, max) + bits := 128 + } + // if (v >> 64 != 0) { v >>= 64; bits += 64; } + if gt(max, 0xFFFFFFFFFFFFFFFF) { + max := shr(64, max) + bits := add(bits, 64) + } + // if (v >> 32 != 0) { v >>= 32; bits += 32; } + if gt(max, 0xFFFFFFFF) { + max := shr(32, max) + bits := add(bits, 32) + } + // if (v >> 16 != 0) { v >>= 16; bits += 16; } + if gt(max, 0xFFFF) { + max := shr(16, max) + bits := add(bits, 16) + } + // if (v >> 8 != 0) { v >>= 8; bits += 8; } + if gt(max, 0xFF) { + max := shr(8, max) + bits := add(bits, 8) + } + // if (v >> 4 != 0) { v >>= 4; bits += 4; } + if gt(max, 0xF) { + max := shr(4, max) + bits := add(bits, 4) + } + // if (v >> 2 != 0) { v >>= 2; bits += 2; } + if gt(max, 0x3) { + max := shr(2, max) + bits := add(bits, 2) + } + // if (v >> 1 != 0) { /* v >>= 1; */ bits += 1; } + if gt(max, 0x1) { + bits := add(bits, 1) + } + bits := add(bits, 1) + } + } + + function mulAddProjectiveTriplet( + uint256 x1, uint256 y1, uint256 k1, + uint256 x2, uint256 y2, uint256 k2, + uint256 x3, uint256 y3, uint256 k3 + ) internal pure returns (uint256 x4, uint256 y4, uint256 z4) { + // We implement the Straus-Shamir trick described in + // Trading Inversions for Multiplications in Elliptic Curve Cryptography. + // (https://eprint.iacr.org/2003/257.pdf Page 7). + + uint256 bits = findMaxBitLength(k1, k2, k3); + + uint256[8] memory precomputedXs; + uint256[8] memory precomputedYs; + uint256[8] memory precomputedZs; + + precomputedXs[1] = x3; precomputedYs[1] = y3; precomputedZs[1] = 1; // 001: P3 + precomputedXs[2] = x2; precomputedYs[2] = y2; precomputedZs[2] = 1; // 010: P2 + precomputedXs[4] = x1; precomputedYs[4] = y1; precomputedZs[4] = 1; // 100: P1 + + (precomputedXs[6], precomputedYs[6], precomputedZs[6]) = projectiveAddMixed(x1, y1, 1, x2, y2); // 110: P1+P2 + (precomputedXs[5], precomputedYs[5], precomputedZs[5]) = projectiveAddMixed(x1, y1, 1, x3, y3); // 101: P1+P3 + + (x4, y4, z4) = projectiveAddMixed(x2, y2, 1, x3, y3); // 011: P2+P3 + precomputedXs[3] = x4; + precomputedYs[3] = y4; + precomputedZs[3] = z4; + + (x4, y4, z4) = projectiveAddMixed(x4, y4, z4, x1, y1); // 111: P1+P2+P3 + precomputedXs[7] = x4; + precomputedYs[7] = y4; + precomputedZs[7] = z4; + + x4 = 0; + y4 = 1; + z4 = 0; + + for (; bits > 0;) { + unchecked { --bits; } + + (x4, y4, z4) = projectiveDouble(x4, y4, z4); + + uint8 mask; + uint8 bitmask; + + assembly { + mask := or( + shl(2, and(shr(bits, k1), 1)), + or( + shl(1, and(shr(bits, k2), 1)), + and(shr(bits, k3), 1) + ) + ) + // bitmask for 1,2,4 is 22 = 0b00010110 + bitmask := and(shl(22, mask), 1) + } + + if (mask == 0) { + continue; + } + + if (bitmask == 1) { + (x4, y4, z4) = projectiveAddMixed( + x4, y4, z4, precomputedXs[mask], precomputedYs[mask] + ); + } else { + (x4, y4, z4) = projectiveAdd( + x4, y4, z4, precomputedXs[mask], precomputedYs[mask], precomputedZs[mask] + ); + } + } + } + function projectiveAddMixed( uint256 X1, uint256 Y1, diff --git a/test/MathTester.t.sol b/test/MathTester.t.sol index 19224c8..f7e0f0d 100644 --- a/test/MathTester.t.sol +++ b/test/MathTester.t.sol @@ -22,7 +22,43 @@ contract MathTesterTest is Test { points = abi.decode(data, (Point[])); } - function test_projectiveAddition() public view { + function test_triplet() public view { + uint256 len = points.length - 2; + for (uint256 i; i < len;) { + (uint256 x_p, uint256 y_p, uint256 z_p) = math.mulProjectiveTriplet( + uint256(points[i].x), + uint256(points[i].y), + uint256(points[i].k), + uint256(points[i+1].x), + uint256(points[i+1].y), + uint256(points[i+1].k), + uint256(points[i+2].x), + uint256(points[i+2].y), + uint256(points[i+2].k) + ); + (x_p, y_p) = math.toAffineProjective(x_p, y_p, z_p); + + (uint256 x_j, uint256 y_j) = math.mulEcTriplet( + uint256(points[i].x), + uint256(points[i].y), + uint256(points[i].k), + uint256(points[i+1].x), + uint256(points[i+1].y), + uint256(points[i+1].k), + uint256(points[i+2].x), + uint256(points[i+2].y), + uint256(points[i+2].k) + ); + // (x_j, y_j) = math.toAffineJacobian(x_j, y_j, z_j); + + assertEq(x_p, x_j); + assertEq(y_p, y_j); + + unchecked { ++i; } + } + } + + function test_addition() public view { uint256 len = points.length - 1; for (uint256 i; i < len;) { (uint256 x_p, uint256 y_p, uint256 z_p) = math.addProjective( @@ -48,7 +84,7 @@ contract MathTesterTest is Test { } } - function test_projectiveAdditionMixed() public view { + function test_mixedAddition() public view { uint256 len = points.length - 1; for (uint256 i; i < len;) { (uint256 x_p, uint256 y_p, uint256 z_p) = math.addProjectiveMixed(