Skip to content
Merged
Show file tree
Hide file tree
Changes from 23 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/cool-mangos-compare.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`Math`: add an `invMod` function to get the modular multiplicative inverse of a number in Z/nZ.
24 changes: 24 additions & 0 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,30 @@ library Math {
return result;
}

/**
* @notice Calculate the modular multiplicative inverse of a number in Z/nZ.
*
* If n is a prime, then Z/nZ is a field. In that case all elements are inversible, expect 0.
* If n is not a prime, then Z/nZ is not a field, and some elements might not be inversible.
*
* If the input value is not inversible, 0 is returned.
*/
function invMod(uint256 a, uint256 n) internal pure returns (uint256) {
unchecked {
if (n == 0) return 0;
uint256 r1 = a % n;
uint256 r2 = n;
int256 t1 = 0;
int256 t2 = 1;
while (r1 != 0) {
uint256 q = r2 / r1;
(t1, t2, r2, r1) = (t2, t1 - t2 * int256(q), r1, r2 - r1 * q);
}
if (r2 != 1) return 0;
return t1 < 0 ? (n - uint256(-t1)) : uint256(t1);
}
}

/**
* @dev Returns the square root of a number. If the number is not a perfect square, the value is rounded
* towards zero.
Expand Down
35 changes: 35 additions & 0 deletions test/utils/math/Math.t.sol
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,41 @@ contract MathTest is Test {
return value * value < ref;
}

// INV
function testInvMod(uint256 value, uint256 p) public {
_testInvMod(value, p, true);
}

function testInvMod2(uint256 seed) public {
uint256 p = 2; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}

function testInvMod17(uint256 seed) public {
uint256 p = 17; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}

function testInvMod65537(uint256 seed) public {
uint256 p = 65537; // prime
_testInvMod(bound(seed, 1, p - 1), p, false);
}

function testInvModP256(uint256 seed) public {
uint256 p = 0xffffffff00000001000000000000000000000000ffffffffffffffffffffffff; // prime
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow now I get why the rumors of a backdoor in secp256r1, this is a weird number

_testInvMod(bound(seed, 1, p - 1), p, false);
}

function _testInvMod(uint256 value, uint256 p, bool allowZero) private {
uint256 inverse = Math.invMod(value, p);
if (inverse != 0) {
assertEq(mulmod(value, inverse, p), 1);
assertLt(inverse, p);
} else {
assertTrue(allowZero);
}
}

// LOG2
function testLog2(uint256 input, uint8 r) public {
Math.Rounding rounding = _asRounding(r);
Expand Down
38 changes: 38 additions & 0 deletions test/utils/math/Math.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ const { PANIC_CODES } = require('@nomicfoundation/hardhat-chai-matchers/panic');

const { Rounding } = require('../../helpers/enums');
const { min, max } = require('../../helpers/math');
const { randomArray, generators } = require('../../helpers/random');

const RoundingDown = [Rounding.Floor, Rounding.Trunc];
const RoundingUp = [Rounding.Ceil, Rounding.Expand];
Expand Down Expand Up @@ -298,6 +299,43 @@ describe('Math', function () {
});
});

describe('invMod', function () {
for (const factors of [
[0n],
[1n],
[2n],
[17n],
[65537n],
[0xffffffff00000001000000000000000000000000ffffffffffffffffffffffffn],
[3n, 5n],
[3n, 7n],
[47n, 53n],
]) {
const p = factors.reduce((acc, f) => acc * f, 1n);

describe(`using p=${p} which is ${p > 1 && factors.length > 1 ? 'not ' : ''}a prime`, function () {
it('trying to inverse 0 returns 0', async function () {
expect(await this.mock.$invMod(0, p)).to.equal(0n);
expect(await this.mock.$invMod(p, p)).to.equal(0n); // p is 0 mod p
});

if (p != 0) {
for (const value of randomArray(generators.uint256, 16)) {
const isInversible = factors.every(f => value % f);
it(`trying to inverse ${value}`, async function () {
const result = await this.mock.$invMod(value, p);
if (isInversible) {
expect((value * result) % p).to.equal(1n);
} else {
expect(result).to.equal(0n);
}
});
}
}
});
}
});

describe('sqrt', function () {
it('rounds down', async function () {
for (const rounding of RoundingDown) {
Expand Down