-
Notifications
You must be signed in to change notification settings - Fork 11.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add bytes memory
version of Math.modExp
#4893
Conversation
|
Name | Type |
---|---|
openzeppelin-solidity | Minor |
Click here to learn what changesets are, and how to add one.
Click here if you're a maintainer who wants to add a changeset to this PR
I updated |
test/utils/math/Math.test.js
Outdated
|
||
expect(await this.mock.$modExp(base, exponent, modulus)).to.equal(base ** exponent % modulus); | ||
for (const [baseExp, exponentExp, modulusExp] of product(range(0, 24, 4), range(0, 24, 4), range(0, 256, 64))) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The ranges were selected to fit within the max BigInt.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
These values are actually pretty small. The max value they take is
b: 1048576
e: 1048576
m: 6277101735386680763835789423207666416102355444464034512896
In comparaison, type(uint256).max is
115792089237316195423570985008687907853269984665640564039457584007913129639935
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
which makes me wonder:
- if all these fit in a uint256, why only test them by the bytes version, and not with the uint256 version
- This looks like fuzzing, but with very specific values (only powers of 2).
Since the values used here are not bigger than then one supported by the foundry fuzzing, are way slower to run, and do not cover a bigger space ... do we need that at all ? I'd remove this part of the tests.
Tests passing but coverage dropped. I tried mocking the it('is correctly reverting if the underlying call reverts', async function () {
const base = 3n;
const exponent = 200n;
const modulus = 50n;
await setCode('0x0000000000000000000000000000000000000005', '0xFD'); // Straight revert
await expect(this.mock[this.fn](base, exponent, modulus)).to.be.reverted();
}); The revert's missing stack arguments but it should revert anyway and it's not. |
Alternatively, we can revert in any case 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) {
Panic.panic(Panic.DIVISION_BY_ZERO);
}
return result;
} We don't recommend relying on revert messages so we can change it if there's a proven way to make the modexp call revert. What do you think @Amxx? |
I'm 100% ok with that. If we do it here, we should do it in both versions of |
Somewhat agree with 12ea8eb but I think we should keep those test cases. The main reason is that the only benefit from the ethers tests vs Foundry is that we can test outside of the I'll revert the change but reconfigure the function to avoid performing excessive operations. |
This reverts commit 12ea8eb.
I readded the large 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]),
) In this way, we're using large numbers for all arguments of modexp and the tests are still running fast. Note I readded the |
function tryModExp( | ||
bytes memory b, | ||
bytes memory e, | ||
bytes memory m | ||
) internal view returns (bool success, bytes memory result) { | ||
if (_zeroBytes(m)) return (false, new bytes(0)); | ||
|
||
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 dataPtr := add(result, 0x20) | ||
// Write result on top of args to avoid allocating extra memory. | ||
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)) | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This works assuming the current solidity behavior:
- result is always allocated (at the free memory pointer)
If solidity ever changes its behavior, reusing memory gaps to put new arrays, then this would set the free memory pointer in an unsafe way.
That is unlikelly to happen for a white. Definitelly not in 0.8.x. So I think its ok.
Fixes #4880
Fixes LIB-1209
PR Checklist
npx changeset add
)