From 1e45e6e3d802764ba4c37f805fa26a92d9a84d57 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 12:31:43 -0600 Subject: [PATCH 01/19] Add bytes `Math.modExp` --- .changeset/shiny-poets-whisper.md | 2 +- contracts/utils/math/Math.sol | 49 ++++++++++++++ test/utils/math/Math.t.sol | 29 ++++++++ test/utils/math/Math.test.js | 106 ++++++++++++++++++++++-------- 4 files changed, 156 insertions(+), 30 deletions(-) diff --git a/.changeset/shiny-poets-whisper.md b/.changeset/shiny-poets-whisper.md index cdef2391417..92497033acf 100644 --- a/.changeset/shiny-poets-whisper.md +++ b/.changeset/shiny-poets-whisper.md @@ -2,4 +2,4 @@ 'openzeppelin-solidity': minor --- -`Math`: Add `modExp` function that exposes the `EIP-198` precompile. +`Math`: Add `modExp` function that exposes the `EIP-198` precompile. Includes `uint256` and `bytes memory` versions. diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index be05506ec53..c6408579c61 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -335,6 +335,55 @@ library Math { } } + /** + * @dev Variant of {modExp} that supports inputs of arbitrary length. + */ + function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) { + (bool success, bytes memory result) = tryModExp(b, e, m); + if (!success) { + if (_zeroArray(m)) { + Panic.panic(Panic.DIVISION_BY_ZERO); + } else { + revert Address.FailedInnerCall(); + } + } + return result; + } + + /** + * @dev Variant of {tryModExp} that supports inputs of arbitrary length. + */ + function tryModExp( + bytes memory b, + bytes memory e, + bytes memory m + ) internal view returns (bool success, bytes memory result) { + if (_zeroArray(m)) return (false, new bytes(0)); + + // Encode call args and move the free memory pointer + bytes memory args = abi.encodePacked(b.length, e.length, m.length, b, e, m); + + // Given result <= modulus + result = new bytes(m.length); + + /// @solidity memory-safe-assembly + assembly { + success := staticcall(gas(), 0x05, add(args, 0x20), mload(args), add(result, 0x20), mload(m)) + } + } + + /** + * @dev Returns whether the provided array is zero. + */ + function _zeroArray(bytes memory array) private pure returns (bool) { + for (uint256 i; i < array.length; ++i) { + if (array[i] != 0) { + return false; + } + } + return true; + } + /** * @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded * towards zero. diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 7b49e8a8868..fd2ba0bc1b0 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -226,6 +226,35 @@ contract MathTest is Test { } } + function testModExpMemory(uint256 b, uint256 e, uint256 m) public { + if (m == 0) { + vm.expectRevert(stdError.divisionError); + } + bytes memory result = Math.modExp( + abi.encodePacked(b), + abi.encodePacked(e), + abi.encodePacked(m) + ); + uint256 res = abi.decode(result, (uint256)); + assertLt(res, m); + assertEq(res, _nativeModExp(b, e, m)); + } + + function testTryModExpMemory(uint256 b, uint256 e, uint256 m) public { + (bool success, bytes memory result) = Math.tryModExp( + abi.encodePacked(b), + abi.encodePacked(e), + abi.encodePacked(m) + ); + if (success) { + uint256 res = abi.decode(result, (uint256)); + assertLt(res, m); + assertEq(res, _nativeModExp(b, e, m)); + } else { + assertEq(result.length, 0); + } + } + function _nativeModExp(uint256 b, uint256 e, uint256 m) private pure returns (uint256) { if (m == 1) return 0; uint256 r = 1; diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index b75d3b58858..84320b07238 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -6,6 +6,9 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { Rounding } = require('../../helpers/enums'); const { min, max } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); +const { range } = require('../../../scripts/helpers'); +const { toBeHex, dataLength } = require('ethers'); +const { product } = require('../../helpers/iterate'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -141,24 +144,6 @@ describe('Math', function () { }); }); - describe('tryModExp', function () { - it('is correctly returning true and calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; - - expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([true, base ** exponent % modulus]); - }); - - it('is correctly returning false when modulus is 0', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; - - expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([false, 0n]); - }); - }); - describe('max', function () { it('is correctly detected in both position', async function () { await testCommutative(this.mock.$max, 1234n, 5678n, max(1234n, 5678n)); @@ -353,21 +338,84 @@ describe('Math', function () { } }); - describe('modExp', function () { - it('is correctly calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; + describe.only('modExp', function () { + describe('with uint256 inputs', function () { + before(function () { + this.fn = '$modExp(uint256,uint256,uint256)'; + }); - expect(await this.mock.$modExp(base, exponent, modulus)).to.equal(base ** exponent % modulus); + it('is correctly calculating modulus', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 50n; + + expect(await this.mock[this.fn](base, exponent, modulus)).to.equal(base ** exponent % modulus); + }); + + it('is correctly reverting when modulus is zero', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 0n; + + await expect(this.mock[this.fn](base, exponent, modulus)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); + }); }); - it('is correctly reverting when modulus is zero', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; + describe('with bytes memory inputs', function () { + before(function () { + this.fn = '$modExp(bytes,bytes,bytes)'; + }); + + it('is correctly calculating modulus', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 50n; - await expect(this.mock.$modExp(base, exponent, modulus)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); + expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.equal( + toBeHex(base ** exponent % modulus), + ); + }); + + it('is correctly reverting when modulus is zero', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 0n; + + await expect(this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.be.revertedWithPanic( + PANIC_CODES.DIVISION_BY_ZERO, + ); + }); + + for (const [base, exponent, modulusExponent] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { + const b = 2n ** BigInt(base); + const e = 2n ** BigInt(exponent); + const m = 2n ** BigInt(modulusExponent); + + it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); + expect(result).to.equal(toBeHex(b ** e % m, dataLength(toBeHex(m)))); + }); + } + }); + }); + + describe('tryModExp', function () { + describe('with uint256 inputs', function () { + it('is correctly returning true and calculating modulus', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 50n; + + expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([true, base ** exponent % modulus]); + }); + + it('is correctly returning false when modulus is 0', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 0n; + + expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([false, 0n]); + }); }); }); From 780e8166a8e2fc83c2ff1bdf4078f7fbbb6cc968 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 12:40:01 -0600 Subject: [PATCH 02/19] Improve tests --- test/utils/math/Math.test.js | 49 +++++++++++++++++++++++++++++++++--- 1 file changed, 46 insertions(+), 3 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 84320b07238..cd157140425 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -399,14 +399,18 @@ describe('Math', function () { }); }); - describe('tryModExp', function () { + describe.only('tryModExp', function () { describe('with uint256 inputs', function () { + before(function () { + this.fn = '$tryModExp(uint256,uint256,uint256)'; + }); + it('is correctly returning true and calculating modulus', async function () { const base = 3n; const exponent = 200n; const modulus = 50n; - expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([true, base ** exponent % modulus]); + expect(await this.mock[this.fn](base, exponent, modulus)).to.deep.equal([true, base ** exponent % modulus]); }); it('is correctly returning false when modulus is 0', async function () { @@ -414,9 +418,48 @@ describe('Math', function () { const exponent = 200n; const modulus = 0n; - expect(await this.mock.$tryModExp(base, exponent, modulus)).to.deep.equal([false, 0n]); + expect(await this.mock[this.fn](base, exponent, modulus)).to.deep.equal([false, 0n]); }); }); + + describe('with bytes memory inputs', function () { + before(function () { + this.fn = '$tryModExp(bytes,bytes,bytes)'; + }); + + it('is correctly returning true and calculating modulus', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 50n; + + expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.deep.equal([ + true, + toBeHex(base ** exponent % modulus), + ]); + }); + + it('is correctly returning false when modulus is 0', async function () { + const base = 3n; + const exponent = 200n; + const modulus = 0n; + + expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.deep.equal([ + false, + '0x', + ]); + }); + + for (const [base, exponent, modulusExponent] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { + const b = 2n ** BigInt(base); + const e = 2n ** BigInt(exponent); + const m = 2n ** BigInt(modulusExponent); + + it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); + expect(result).to.deep.equal([true, toBeHex(b ** e % m, dataLength(toBeHex(m)))]); + }); + } + }); }); describe('sqrt', function () { From f34fe3fb0806b5a60780868cf54c83a6f8e47bc7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 12:41:05 -0600 Subject: [PATCH 03/19] Improve tests --- test/utils/math/Math.test.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index cd157140425..c9d98f1e90f 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -386,10 +386,10 @@ describe('Math', function () { ); }); - for (const [base, exponent, modulusExponent] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { - const b = 2n ** BigInt(base); - const e = 2n ** BigInt(exponent); - const m = 2n ** BigInt(modulusExponent); + for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { + const b = 2n ** BigInt(baseExp); + const e = 2n ** BigInt(exponentExp); + const m = 2n ** BigInt(modulusExp); it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); @@ -449,10 +449,10 @@ describe('Math', function () { ]); }); - for (const [base, exponent, modulusExponent] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { - const b = 2n ** BigInt(base); - const e = 2n ** BigInt(exponent); - const m = 2n ** BigInt(modulusExponent); + for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { + const b = 2n ** BigInt(baseExp); + const e = 2n ** BigInt(exponentExp); + const m = 2n ** BigInt(modulusExp); it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); From 31f8c4ad15faf9221147d8946b5421f671dbeeb7 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 12:41:53 -0600 Subject: [PATCH 04/19] Lint --- test/utils/math/Math.t.sol | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index fd2ba0bc1b0..0bc3b93d711 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -230,11 +230,7 @@ contract MathTest is Test { if (m == 0) { vm.expectRevert(stdError.divisionError); } - bytes memory result = Math.modExp( - abi.encodePacked(b), - abi.encodePacked(e), - abi.encodePacked(m) - ); + bytes memory result = Math.modExp(abi.encodePacked(b), abi.encodePacked(e), abi.encodePacked(m)); uint256 res = abi.decode(result, (uint256)); assertLt(res, m); assertEq(res, _nativeModExp(b, e, m)); From 3f1aacd1034f212935f7b0cf352acb3ddc14b1a4 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 19:58:03 -0600 Subject: [PATCH 05/19] Avoid unnecessary allocated memory --- contracts/utils/math/Math.sol | 17 ++++++++++++----- test/utils/math/Math.test.js | 4 ++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index c6408579c61..1fd0823f8f8 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -361,14 +361,21 @@ library Math { if (_zeroArray(m)) return (false, new bytes(0)); // Encode call args and move the free memory pointer - bytes memory args = abi.encodePacked(b.length, e.length, m.length, b, e, m); - - // Given result <= modulus - result = new bytes(m.length); + result = abi.encodePacked(b.length, e.length, m.length, b, e, m); /// @solidity memory-safe-assembly assembly { - success := staticcall(gas(), 0x05, add(args, 0x20), mload(args), add(result, 0x20), mload(m)) + let mLen := mload(m) + // Write result on top of args to avoid allocating extra memory. + // | Offset | Content | Content (Hex) | + // |-----------|--------------|--------------------------------------------------------------------| + // | 0x00:0x1f | args length | 0x<.......................................20+20+20+bLen+eLen+mLen> | + // | 0x20+mLen | result | 0x<........................................................result> | + // | 0x..:0x.. | dirty bytes | 0x<............................................20+20+20+bLen+eLen> | + success := staticcall(gas(), 0x05, add(result, 0x20), mload(result), add(result, 0x20), mLen) + // Overwrite the length. + // result.length > returndatasize() is guaranteed because returndatasize() == m.length + mstore(result, mLen) } } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index c9d98f1e90f..00147e51c84 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -338,7 +338,7 @@ describe('Math', function () { } }); - describe.only('modExp', function () { + describe('modExp', function () { describe('with uint256 inputs', function () { before(function () { this.fn = '$modExp(uint256,uint256,uint256)'; @@ -399,7 +399,7 @@ describe('Math', function () { }); }); - describe.only('tryModExp', function () { + describe('tryModExp', function () { describe('with uint256 inputs', function () { before(function () { this.fn = '$tryModExp(uint256,uint256,uint256)'; From c42490031961298e0c32327981d2fe3024a22bed Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 19:59:49 -0600 Subject: [PATCH 06/19] Rewrite mLen --- contracts/utils/math/Math.sol | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 1fd0823f8f8..ba6ff58c17a 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -360,12 +360,13 @@ library Math { ) internal view returns (bool success, bytes memory result) { if (_zeroArray(m)) return (false, new bytes(0)); - // Encode call args and move the free memory pointer - result = abi.encodePacked(b.length, e.length, m.length, b, e, m); + uint256 mLen = m.length; + + // Encode call args in result and move the free memory pointer + result = abi.encodePacked(b.length, e.length, mLen, b, e, m); /// @solidity memory-safe-assembly assembly { - let mLen := mload(m) // Write result on top of args to avoid allocating extra memory. // | Offset | Content | Content (Hex) | // |-----------|--------------|--------------------------------------------------------------------| From ebdd8f70b1f9549573c3b349f6e49596f2f5e68f Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 20:16:25 -0600 Subject: [PATCH 07/19] Improve tests --- test/utils/math/Math.t.sol | 1 + test/utils/math/Math.test.js | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index 0bc3b93d711..ea753c14676 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -233,6 +233,7 @@ contract MathTest is Test { bytes memory result = Math.modExp(abi.encodePacked(b), abi.encodePacked(e), abi.encodePacked(m)); uint256 res = abi.decode(result, (uint256)); assertLt(res, m); + assertEq(result.length, 32); assertEq(res, _nativeModExp(b, e, m)); } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 00147e51c84..c0904a61238 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -450,9 +450,9 @@ describe('Math', function () { }); for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { - const b = 2n ** BigInt(baseExp); - const e = 2n ** BigInt(exponentExp); - const m = 2n ** BigInt(modulusExp); + const b = 2n ** BigInt(baseExp) + 1n; + const e = 2n ** BigInt(exponentExp) + 1n; + const m = 2n ** BigInt(modulusExp) + 1n; it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); From 44616284b0aeb094b19509eb5f6e32a1c06b705a Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 20:19:57 -0600 Subject: [PATCH 08/19] Improve impl --- contracts/utils/math/Math.sol | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index ba6ff58c17a..d723f34b13d 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -358,7 +358,7 @@ library Math { bytes memory e, bytes memory m ) internal view returns (bool success, bytes memory result) { - if (_zeroArray(m)) return (false, new bytes(0)); + if (_zeroBytes(m)) return (false, result); uint256 mLen = m.length; @@ -381,11 +381,11 @@ library Math { } /** - * @dev Returns whether the provided array is zero. + * @dev Returns whether the provided byte array is zero. */ - function _zeroArray(bytes memory array) private pure returns (bool) { - for (uint256 i; i < array.length; ++i) { - if (array[i] != 0) { + function _zeroBytes(bytes memory byteArray) private pure returns (bool) { + for (uint256 i; i < byteArray.length; ++i) { + if (byteArray[i] != 0) { return false; } } From b25061e3b4288c7ae044813407ee1ead42d382cb Mon Sep 17 00:00:00 2001 From: ernestognw Date: Mon, 12 Feb 2024 20:22:31 -0600 Subject: [PATCH 09/19] Fix --- contracts/utils/math/Math.sol | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index d723f34b13d..7954817b40b 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -341,7 +341,7 @@ library Math { function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) { (bool success, bytes memory result) = tryModExp(b, e, m); if (!success) { - if (_zeroArray(m)) { + if (_zeroBytes(m)) { Panic.panic(Panic.DIVISION_BY_ZERO); } else { revert Address.FailedInnerCall(); From 37df9c3a60718c160e5ff513cc3f819bd8a75987 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 07:17:12 -0600 Subject: [PATCH 10/19] Panic regardless of the failure type in modexp --- contracts/utils/math/Math.sol | 13 ++----------- 1 file changed, 2 insertions(+), 11 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 7954817b40b..503f8bac3ed 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -3,7 +3,6 @@ pragma solidity ^0.8.20; -import {Address} from "../Address.sol"; import {Panic} from "../Panic.sol"; import {SafeCast} from "./SafeCast.sol"; @@ -289,11 +288,7 @@ library Math { function modExp(uint256 b, uint256 e, uint256 m) internal view returns (uint256) { (bool success, uint256 result) = tryModExp(b, e, m); if (!success) { - if (m == 0) { - Panic.panic(Panic.DIVISION_BY_ZERO); - } else { - revert Address.FailedInnerCall(); - } + Panic.panic(Panic.DIVISION_BY_ZERO); } return result; } @@ -341,11 +336,7 @@ library Math { function modExp(bytes memory b, bytes memory e, bytes memory m) internal view returns (bytes memory) { (bool success, bytes memory result) = tryModExp(b, e, m); if (!success) { - if (_zeroBytes(m)) { - Panic.panic(Panic.DIVISION_BY_ZERO); - } else { - revert Address.FailedInnerCall(); - } + Panic.panic(Panic.DIVISION_BY_ZERO); } return result; } From f3a17a473d8bb04e041cbae579f6eec38e16b4a9 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 13 Feb 2024 17:03:01 +0100 Subject: [PATCH 11/19] factor test cases --- test/utils/math/Math.test.js | 76 +++++++++++++----------------------- 1 file changed, 28 insertions(+), 48 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index c0904a61238..a8a518d8368 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -339,61 +339,41 @@ describe('Math', function () { }); describe('modExp', function () { - describe('with uint256 inputs', function () { - before(function () { - this.fn = '$modExp(uint256,uint256,uint256)'; - }); - - it('is correctly calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; - - expect(await this.mock[this.fn](base, exponent, modulus)).to.equal(base ** exponent % modulus); - }); - - it('is correctly reverting when modulus is zero', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; - - await expect(this.mock[this.fn](base, exponent, modulus)).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); - }); - }); - - describe('with bytes memory inputs', function () { - before(function () { - this.fn = '$modExp(bytes,bytes,bytes)'; - }); + const bytes = (value, width = undefined) => ethers.Typed.bytes(ethers.toBeHex(value, width)); + const uint256 = (value) => ethers.Typed.uint256(value); - it('is correctly calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; + for (const [ name, type ] of Object.entries({ uint256, bytes })) { + describe(`with ${name} inputs`, function () { + it('is correctly calculating modulus', async function () { + const b = 3n; + const e = 200n; + const m = 50n; - expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.equal( - toBeHex(base ** exponent % modulus), - ); - }); + expect(await this.mock.$modExp(type(b), type(e), type(m))).to.equal(type(b ** e % m).value); + }); - it('is correctly reverting when modulus is zero', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; + it('is correctly reverting when modulus is zero', async function () { + const b = 3n; + const e = 200n; + const m = 0n; - await expect(this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.be.revertedWithPanic( - PANIC_CODES.DIVISION_BY_ZERO, - ); + await expect(this.mock.$modExp(type(b), type(e), type(m))).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); + }); }); + } - for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { - const b = 2n ** BigInt(baseExp); - const e = 2n ** BigInt(exponentExp); - const m = 2n ** BigInt(modulusExp); - + describe(`with large bytes inputs`, function () { + for (const [b, e, m] of product( + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 256, 64).map(i => 2n ** BigInt(i)) + )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { - const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); - expect(result).to.equal(toBeHex(b ** e % m, dataLength(toBeHex(m)))); + const mLength = ethers.dataLength(ethers.toBeHex(m)); + + expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal( + bytes(b ** e % m, mLength).value + ); }); } }); From eb5f166e66414bd76c4ddb71ec13786c0736d9d7 Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 13 Feb 2024 17:13:35 +0100 Subject: [PATCH 12/19] factor test cases --- test/utils/math/Math.test.js | 101 ++++++++++++++--------------------- 1 file changed, 39 insertions(+), 62 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index a8a518d8368..8b23fcfa4d4 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -7,12 +7,16 @@ const { Rounding } = require('../../helpers/enums'); const { min, max } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); const { range } = require('../../../scripts/helpers'); -const { toBeHex, dataLength } = require('ethers'); const { product } = require('../../helpers/iterate'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; +const bytes = (value, width = undefined) => ethers.Typed.bytes(ethers.toBeHex(value, width)); +const uint256 = value => ethers.Typed.uint256(value); +bytes.zero = '0x'; +uint256.zero = 0n; + async function testCommutative(fn, lhs, rhs, expected, ...extra) { expect(await fn(lhs, rhs, ...extra)).to.deep.equal(expected); expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected); @@ -339,10 +343,7 @@ describe('Math', function () { }); describe('modExp', function () { - const bytes = (value, width = undefined) => ethers.Typed.bytes(ethers.toBeHex(value, width)); - const uint256 = (value) => ethers.Typed.uint256(value); - - for (const [ name, type ] of Object.entries({ uint256, bytes })) { + for (const [name, type] of Object.entries({ uint256, bytes })) { describe(`with ${name} inputs`, function () { it('is correctly calculating modulus', async function () { const b = 3n; @@ -357,7 +358,9 @@ describe('Math', function () { const e = 200n; const m = 0n; - await expect(this.mock.$modExp(type(b), type(e), type(m))).to.be.revertedWithPanic(PANIC_CODES.DIVISION_BY_ZERO); + await expect(this.mock.$modExp(type(b), type(e), type(m))).to.be.revertedWithPanic( + PANIC_CODES.DIVISION_BY_ZERO, + ); }); }); } @@ -366,77 +369,51 @@ describe('Math', function () { for (const [b, e, m] of product( range(0, 24, 4).map(i => 2n ** BigInt(i)), range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 256, 64).map(i => 2n ** BigInt(i)) + range(0, 256, 64).map(i => 2n ** BigInt(i)), )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); - expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal( - bytes(b ** e % m, mLength).value - ); + expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(b ** e % m, mLength).value); }); } }); }); - describe('tryModExp', function () { - describe('with uint256 inputs', function () { - before(function () { - this.fn = '$tryModExp(uint256,uint256,uint256)'; - }); - - it('is correctly returning true and calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; - - expect(await this.mock[this.fn](base, exponent, modulus)).to.deep.equal([true, base ** exponent % modulus]); - }); - - it('is correctly returning false when modulus is 0', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; - - expect(await this.mock[this.fn](base, exponent, modulus)).to.deep.equal([false, 0n]); - }); - }); - - describe('with bytes memory inputs', function () { - before(function () { - this.fn = '$tryModExp(bytes,bytes,bytes)'; - }); - - it('is correctly returning true and calculating modulus', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 50n; + describe.only('tryModExp', function () { + for (const [name, type] of Object.entries({ uint256, bytes })) { + describe(`with ${name} inputs`, function () { + it('is correctly calculating modulus', async function () { + const b = 3n; + const e = 200n; + const m = 50n; - expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.deep.equal([ - true, - toBeHex(base ** exponent % modulus), - ]); - }); + expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([true, type(b ** e % m).value]); + }); - it('is correctly returning false when modulus is 0', async function () { - const base = 3n; - const exponent = 200n; - const modulus = 0n; + it('is correctly reverting when modulus is zero', async function () { + const b = 3n; + const e = 200n; + const m = 0n; - expect(await this.mock[this.fn](toBeHex(base), toBeHex(exponent), toBeHex(modulus))).to.deep.equal([ - false, - '0x', - ]); + expect(await this.mock.$tryModExp(type(b), type(e), type(m))).to.deep.equal([false, type.zero]); + }); }); + } - for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { - const b = 2n ** BigInt(baseExp) + 1n; - const e = 2n ** BigInt(exponentExp) + 1n; - const m = 2n ** BigInt(modulusExp) + 1n; - + describe(`with large bytes inputs`, function () { + for (const [b, e, m] of product( + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 256, 64).map(i => 2n ** BigInt(i)), + )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { - const result = await this.mock[this.fn](toBeHex(b), toBeHex(e), toBeHex(m)); - expect(result).to.deep.equal([true, toBeHex(b ** e % m, dataLength(toBeHex(m)))]); + const mLength = ethers.dataLength(ethers.toBeHex(m)); + + expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ + true, + bytes(b ** e % m, mLength).value, + ]); }); } }); From 12ea8ebaabe92003fb0e3540d96428f3d6c6359b Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Tue, 13 Feb 2024 17:27:20 +0100 Subject: [PATCH 13/19] remove slow unit tests that duplicate fuzzing --- test/utils/math/Math.test.js | 35 +---------------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 8b23fcfa4d4..9276b61a7e9 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -6,8 +6,6 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { Rounding } = require('../../helpers/enums'); const { min, max } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); -const { range } = require('../../../scripts/helpers'); -const { product } = require('../../helpers/iterate'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -364,23 +362,9 @@ describe('Math', function () { }); }); } - - describe(`with large bytes inputs`, function () { - for (const [b, e, m] of product( - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 256, 64).map(i => 2n ** BigInt(i)), - )) { - it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { - const mLength = ethers.dataLength(ethers.toBeHex(m)); - - expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(b ** e % m, mLength).value); - }); - } - }); }); - describe.only('tryModExp', function () { + describe('tryModExp', function () { for (const [name, type] of Object.entries({ uint256, bytes })) { describe(`with ${name} inputs`, function () { it('is correctly calculating modulus', async function () { @@ -400,23 +384,6 @@ describe('Math', function () { }); }); } - - describe(`with large bytes inputs`, function () { - for (const [b, e, m] of product( - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 256, 64).map(i => 2n ** BigInt(i)), - )) { - it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { - const mLength = ethers.dataLength(ethers.toBeHex(m)); - - expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ - true, - bytes(b ** e % m, mLength).value, - ]); - }); - } - }); }); describe('sqrt', function () { From 0afb6862ac4e70299c6645b8780b5cb3dc78662c Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 14:20:50 -0600 Subject: [PATCH 14/19] Revert "remove slow unit tests that duplicate fuzzing" This reverts commit 12ea8ebaabe92003fb0e3540d96428f3d6c6359b. --- test/utils/math/Math.test.js | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 9276b61a7e9..8b23fcfa4d4 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -6,6 +6,8 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { Rounding } = require('../../helpers/enums'); const { min, max } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); +const { range } = require('../../../scripts/helpers'); +const { product } = require('../../helpers/iterate'); const RoundingDown = [Rounding.Floor, Rounding.Trunc]; const RoundingUp = [Rounding.Ceil, Rounding.Expand]; @@ -362,9 +364,23 @@ describe('Math', function () { }); }); } + + describe(`with large bytes inputs`, function () { + for (const [b, e, m] of product( + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 256, 64).map(i => 2n ** BigInt(i)), + )) { + it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + const mLength = ethers.dataLength(ethers.toBeHex(m)); + + expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(b ** e % m, mLength).value); + }); + } + }); }); - describe('tryModExp', function () { + describe.only('tryModExp', function () { for (const [name, type] of Object.entries({ uint256, bytes })) { describe(`with ${name} inputs`, function () { it('is correctly calculating modulus', async function () { @@ -384,6 +400,23 @@ describe('Math', function () { }); }); } + + describe(`with large bytes inputs`, function () { + for (const [b, e, m] of product( + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 24, 4).map(i => 2n ** BigInt(i)), + range(0, 256, 64).map(i => 2n ** BigInt(i)), + )) { + it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + const mLength = ethers.dataLength(ethers.toBeHex(m)); + + expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ + true, + bytes(b ** e % m, mLength).value, + ]); + }); + } + }); }); describe('sqrt', function () { From 22654ae3953a1434212d3f0bbc88526e255e9962 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 14:30:49 -0600 Subject: [PATCH 15/19] Add semi-fuzz tests back --- test/utils/math/Math.test.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 8b23fcfa4d4..5eb0360a959 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -365,11 +365,11 @@ describe('Math', function () { }); } - describe(`with large bytes inputs`, function () { + describe('with large bytes inputs', function () { for (const [b, e, m] of product( - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 256, 64).map(i => 2n ** BigInt(i)), + range(4, 20, 6).map(i => 2n ** BigInt(i)), + range(4, 20, 6).map(i => 2n ** BigInt(i)), + range(256, 512, 64).map(i => 2n ** BigInt(i)), )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); @@ -380,7 +380,7 @@ describe('Math', function () { }); }); - describe.only('tryModExp', function () { + describe('tryModExp', function () { for (const [name, type] of Object.entries({ uint256, bytes })) { describe(`with ${name} inputs`, function () { it('is correctly calculating modulus', async function () { @@ -401,11 +401,11 @@ describe('Math', function () { }); } - describe(`with large bytes inputs`, function () { + describe('with large bytes inputs', function () { for (const [b, e, m] of product( - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 24, 4).map(i => 2n ** BigInt(i)), - range(0, 256, 64).map(i => 2n ** BigInt(i)), + range(4, 20, 6).map(i => 2n ** BigInt(i)), + range(4, 20, 6).map(i => 2n ** BigInt(i)), + range(256, 512, 64).map(i => 2n ** BigInt(i)), )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); From 454cffd4c06ad2907e1444dd09595afd310404b8 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 16:05:57 -0600 Subject: [PATCH 16/19] Use only large bytes for modexp and test against reference implementation --- contracts/utils/math/Math.sol | 14 ++++++------ test/utils/math/Math.test.js | 40 ++++++++++++++++++++++++++++------- 2 files changed, 38 insertions(+), 16 deletions(-) diff --git a/contracts/utils/math/Math.sol b/contracts/utils/math/Math.sol index 503f8bac3ed..0f9dce94f44 100644 --- a/contracts/utils/math/Math.sol +++ b/contracts/utils/math/Math.sol @@ -349,7 +349,7 @@ library Math { bytes memory e, bytes memory m ) internal view returns (bool success, bytes memory result) { - if (_zeroBytes(m)) return (false, result); + if (_zeroBytes(m)) return (false, new bytes(0)); uint256 mLen = m.length; @@ -358,16 +358,14 @@ library Math { /// @solidity memory-safe-assembly assembly { + let dataPtr := add(result, 0x20) // Write result on top of args to avoid allocating extra memory. - // | Offset | Content | Content (Hex) | - // |-----------|--------------|--------------------------------------------------------------------| - // | 0x00:0x1f | args length | 0x<.......................................20+20+20+bLen+eLen+mLen> | - // | 0x20+mLen | result | 0x<........................................................result> | - // | 0x..:0x.. | dirty bytes | 0x<............................................20+20+20+bLen+eLen> | - success := staticcall(gas(), 0x05, add(result, 0x20), mload(result), add(result, 0x20), mLen) + success := staticcall(gas(), 0x05, dataPtr, mload(result), dataPtr, mLen) // Overwrite the length. // result.length > returndatasize() is guaranteed because returndatasize() == m.length mstore(result, mLen) + // Set the memory pointer after the returned data. + mstore(0x40, add(dataPtr, mLen)) } } @@ -375,7 +373,7 @@ library Math { * @dev Returns whether the provided byte array is zero. */ function _zeroBytes(bytes memory byteArray) private pure returns (bool) { - for (uint256 i; i < byteArray.length; ++i) { + for (uint256 i = 0; i < byteArray.length; ++i) { if (byteArray[i] != 0) { return false; } diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 5eb0360a959..019f2eb37a3 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -22,6 +22,28 @@ async function testCommutative(fn, lhs, rhs, expected, ...extra) { expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected); } +// Computes modexp without BigInt overflow for large numbers +function nativeModExp(b, e, m) { + let result = 1n; + + // If e is a power of two, modexp can be calculated as: + // for (let result = b, i = 0; i < log2(e); i++) result = modexp(result, 2, m) + // + // Given any natural number can be written in terms of powers of 2 (i.e. binary) + // then modexp can be calculated for any e, by multiplying b**i for all i where + // binary(e)[i] is 1 (i.e. a power of two). + for (let base = b % m; e > 0n; base = base ** 2n % m) { + // Least significant bit is 1 + if (e % 2n == 1n) { + result = (result * base) % m; + } + + e /= 2n; // Binary pop + } + + return result; +} + async function fixture() { const mock = await ethers.deployContract('$Math'); @@ -367,14 +389,16 @@ describe('Math', function () { describe('with large bytes inputs', function () { for (const [b, e, m] of product( - range(4, 20, 6).map(i => 2n ** BigInt(i)), - range(4, 20, 6).map(i => 2n ** BigInt(i)), - range(256, 512, 64).map(i => 2n ** BigInt(i)), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); - expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(b ** e % m, mLength).value); + expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal( + bytes(nativeModExp(b, e, m), mLength).value, + ); }); } }); @@ -403,16 +427,16 @@ describe('Math', function () { describe('with large bytes inputs', function () { for (const [b, e, m] of product( - range(4, 20, 6).map(i => 2n ** BigInt(i)), - range(4, 20, 6).map(i => 2n ** BigInt(i)), - range(256, 512, 64).map(i => 2n ** BigInt(i)), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), )) { it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ true, - bytes(b ** e % m, mLength).value, + bytes(nativeModExp(b, e, m), mLength).value, ]); }); } From 7272f507aaa6cb0597de39cefa5cf65c62394a21 Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 16:44:50 -0600 Subject: [PATCH 17/19] Improve tests readability --- test/helpers/math.js | 23 +++++++++++++++++ test/utils/math/Math.test.js | 50 ++++++++++-------------------------- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/test/helpers/math.js b/test/helpers/math.js index f8a1520ee1a..133254aecc2 100644 --- a/test/helpers/math.js +++ b/test/helpers/math.js @@ -3,8 +3,31 @@ const max = (...values) => values.slice(1).reduce((x, y) => (x > y ? x : y), val const min = (...values) => values.slice(1).reduce((x, y) => (x < y ? x : y), values.at(0)); const sum = (...values) => values.slice(1).reduce((x, y) => x + y, values.at(0)); +// Computes modexp without BigInt overflow for large numbers +function modExp(b, e, m) { + let result = 1n; + + // If e is a power of two, modexp can be calculated as: + // for (let result = b, i = 0; i < log2(e); i++) result = modexp(result, 2, m) + // + // Given any natural number can be written in terms of powers of 2 (i.e. binary) + // then modexp can be calculated for any e, by multiplying b**i for all i where + // binary(e)[i] is 1 (i.e. a power of two). + for (let base = b % m; e > 0n; base = base ** 2n % m) { + // Least significant bit is 1 + if (e % 2n == 1n) { + result = (result * base) % m; + } + + e /= 2n; // Binary pop + } + + return result; +} + module.exports = { min, max, sum, + modExp, }; diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 019f2eb37a3..10220b75c8e 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -4,7 +4,7 @@ const { loadFixture } = require('@nomicfoundation/hardhat-network-helpers'); const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic'); const { Rounding } = require('../../helpers/enums'); -const { min, max } = require('../../helpers/math'); +const { min, max, modExp } = require('../../helpers/math'); const { generators } = require('../../helpers/random'); const { range } = require('../../../scripts/helpers'); const { product } = require('../../helpers/iterate'); @@ -22,28 +22,6 @@ async function testCommutative(fn, lhs, rhs, expected, ...extra) { expect(await fn(rhs, lhs, ...extra)).to.deep.equal(expected); } -// Computes modexp without BigInt overflow for large numbers -function nativeModExp(b, e, m) { - let result = 1n; - - // If e is a power of two, modexp can be calculated as: - // for (let result = b, i = 0; i < log2(e); i++) result = modexp(result, 2, m) - // - // Given any natural number can be written in terms of powers of 2 (i.e. binary) - // then modexp can be calculated for any e, by multiplying b**i for all i where - // binary(e)[i] is 1 (i.e. a power of two). - for (let base = b % m; e > 0n; base = base ** 2n % m) { - // Least significant bit is 1 - if (e % 2n == 1n) { - result = (result * base) % m; - } - - e /= 2n; // Binary pop - } - - return result; -} - async function fixture() { const mock = await ethers.deployContract('$Math'); @@ -388,17 +366,15 @@ describe('Math', function () { } describe('with large bytes inputs', function () { - for (const [b, e, m] of product( - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + for (const [[b, log2b], [e, log2e], [m, log2m]] of product( + range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]), + range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]), + range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]), )) { - it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + it(`calculates b ** e % m (b=2**${log2b}) (e=2**${log2e}) (m=2**${log2m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); - expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal( - bytes(nativeModExp(b, e, m), mLength).value, - ); + expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(modExp(b, e, m), mLength).value); }); } }); @@ -426,17 +402,17 @@ describe('Math', function () { } describe('with large bytes inputs', function () { - for (const [b, e, m] of product( - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), - range(256, 512, 64).map(i => 2n ** BigInt(i) + 1n), + for (const [[b, log2b], [e, log2e], [m, log2m]] of product( + range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]), + range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]), + range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]), )) { - it(`calculates b ** e % m (b=${b}) (e=${e}) (m=${m})`, async function () { + it(`calculates b ** e % m (b=2**${log2b}) (e=2**${log2e}) (m=2**${log2m})`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ true, - bytes(nativeModExp(b, e, m), mLength).value, + bytes(modExp(b, e, m), mLength).value, ]); }); } From 7396538b147155c27cd6f42cc2fef7bd23444ddc Mon Sep 17 00:00:00 2001 From: ernestognw Date: Tue, 13 Feb 2024 16:48:13 -0600 Subject: [PATCH 18/19] Update tests description --- test/utils/math/Math.test.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/utils/math/Math.test.js b/test/utils/math/Math.test.js index 10220b75c8e..bce02610f97 100644 --- a/test/utils/math/Math.test.js +++ b/test/utils/math/Math.test.js @@ -371,7 +371,7 @@ describe('Math', function () { range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]), range(320, 512, 64).map(e => [2n ** BigInt(e) + 1n, e]), )) { - it(`calculates b ** e % m (b=2**${log2b}) (e=2**${log2e}) (m=2**${log2m})`, async function () { + it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); expect(await this.mock.$modExp(bytes(b), bytes(e), bytes(m))).to.equal(bytes(modExp(b, e, m), mLength).value); @@ -407,7 +407,7 @@ describe('Math', function () { range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]), range(320, 513, 64).map(e => [2n ** BigInt(e) + 1n, e]), )) { - it(`calculates b ** e % m (b=2**${log2b}) (e=2**${log2e}) (m=2**${log2m})`, async function () { + it(`calculates b ** e % m (b=2**${log2b}+1) (e=2**${log2e}+1) (m=2**${log2m}+1)`, async function () { const mLength = ethers.dataLength(ethers.toBeHex(m)); expect(await this.mock.$tryModExp(bytes(b), bytes(e), bytes(m))).to.deep.equal([ From 78a5598a4d1eecb02b143f9de65e8269a8210acb Mon Sep 17 00:00:00 2001 From: Hadrien Croubois Date: Wed, 14 Feb 2024 09:50:15 +0100 Subject: [PATCH 19/19] Update Math.t.sol --- test/utils/math/Math.t.sol | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/utils/math/Math.t.sol b/test/utils/math/Math.t.sol index ea753c14676..40f5986f4f6 100644 --- a/test/utils/math/Math.t.sol +++ b/test/utils/math/Math.t.sol @@ -231,9 +231,9 @@ contract MathTest is Test { vm.expectRevert(stdError.divisionError); } bytes memory result = Math.modExp(abi.encodePacked(b), abi.encodePacked(e), abi.encodePacked(m)); + assertEq(result.length, 0x20); uint256 res = abi.decode(result, (uint256)); assertLt(res, m); - assertEq(result.length, 32); assertEq(res, _nativeModExp(b, e, m)); } @@ -244,6 +244,7 @@ contract MathTest is Test { abi.encodePacked(m) ); if (success) { + assertEq(result.length, 0x20); // m is a uint256, so abi.encodePacked(m).length is 0x20 uint256 res = abi.decode(result, (uint256)); assertLt(res, m); assertEq(res, _nativeModExp(b, e, m));