ADD: UInt256 implementation of evm's op MOD#9188
Conversation
c2876f7 to
83afdd4
Compare
04317b5 to
4138151
Compare
|
I propose adding a "fully random" case to the microbenchmarks. While it isn’t algorithmically meaningful, it can reveal real-world performance effects (branch prediction, cache behavior, allocation patterns) under heterogeneous inputs : |
|
|
@thomas-quadratic Could you add this shortcut improvement This will resolve the only regression I see from the benchmarks |
fe159ea to
21a3e6a
Compare
There was a problem hiding this comment.
I converted Nethermind's UInt256 tests to Java and found and fixed some bugs (bug fixes mentioned in suggestions): 62c7946 (#9264)
(to compare when the base branch changes https://github.com/hyperledger/besu/compare/thomas-quadratic:feature/uint256-op-modulus..siladu:uint256-op-modulus-parameterised-tests)
There was a recent regression in mod durign your last force push, before yesterday it was passing the parameterised testMod.
There are bugs in shiftLeft and shiftRight but these shouldn't get hit by valid mod inputs...however since they are public methods probably makes sense to fix them?
Finally, do we need a division by zero test for the mod methods?
|
Do you need any of these methods right now just for getting the opcodes going?
If not I would recommend removing them to not overcomplicate right now. They create API consistency problems and with a sign field approach they will require more work to get right and can be added later. |
| * @param sign padding with 0s or 1s whether sign is non-negative. | ||
| * @return HexString | ||
| */ | ||
| public String toString(final int sign) { |
There was a problem hiding this comment.
also can't see why you need to print UInt256 with a sign - plz remove
siladu
left a comment
There was a problem hiding this comment.
I've ported over some more tests for SMod and pushed to #9264
Tests are passing ✅
Not in the PR, but I also tried porting over some of geth's uint256 tests and these were passing except for SMod. The failures were related to how UInt256 were constructed which is what led me to the comment about the signedMod input assumptions.
af9125a to
14774b9
Compare
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
|
@thomas-quadratic I reran the tests from #9264 against your latest commits and testAdd and testLeftShift are failing now. Doesn't seem critical since they aren't actually used, but maybe we should either remove the public methods or fix...not sure these will be included in the fuzzing. |
| Bytes32 expected = | ||
| BigInteger.ZERO.compareTo(big_modulus) == 0 | ||
| ? Bytes32.ZERO | ||
| : bigIntTo32B(big_number.mod(big_modulus)); | ||
| assertThat(remainder).isEqualTo(expected); | ||
| } | ||
| } |
There was a problem hiding this comment.
| Bytes32 expected = | |
| BigInteger.ZERO.compareTo(big_modulus) == 0 | |
| ? Bytes32.ZERO | |
| : bigIntTo32B(big_number.mod(big_modulus)); | |
| assertThat(remainder).isEqualTo(expected); | |
| } | |
| } | |
| BigInteger expected = BigInteger.ZERO; | |
| if (BigInteger.ZERO.compareTo(big_modulus) != 0) { | |
| expected = big_number.mod(big_modulus); | |
| } | |
| assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected); | |
| } | |
| } |
| Bytes32 expected = | ||
| BigInteger.ZERO.compareTo(mbig) == 0 ? Bytes32.ZERO : bigIntTo32B(xbig.add(ybig).mod(mbig)); | ||
| assertThat(remainder).isEqualTo(expected); | ||
| } |
There was a problem hiding this comment.
| Bytes32 expected = | |
| BigInteger.ZERO.compareTo(mbig) == 0 ? Bytes32.ZERO : bigIntTo32B(xbig.add(ybig).mod(mbig)); | |
| assertThat(remainder).isEqualTo(expected); | |
| } | |
| BigInteger expected = BigInteger.ZERO; | |
| if (BigInteger.ZERO.compareTo(mbig) != 0) { | |
| expected = xbig.add(ybig).mod(mbig); | |
| } | |
| assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected); | |
| } |
| Bytes32 expected = | ||
| BigInteger.ZERO.compareTo(cInt) == 0 | ||
| ? Bytes32.ZERO | ||
| : bigIntTo32B(aInt.multiply(bInt).mod(cInt)); | ||
| assertThat(remainder).isEqualTo(expected); | ||
| } | ||
| } |
There was a problem hiding this comment.
| Bytes32 expected = | |
| BigInteger.ZERO.compareTo(cInt) == 0 | |
| ? Bytes32.ZERO | |
| : bigIntTo32B(aInt.multiply(bInt).mod(cInt)); | |
| assertThat(remainder).isEqualTo(expected); | |
| } | |
| } | |
| BigInteger expected = BigInteger.ZERO; | |
| if (BigInteger.ZERO.compareTo(cInt) != 0) { | |
| expected = aInt.multiply(bInt).mod(cInt); | |
| } | |
| assertThat(bytesToBigInt(remainder, 1)).isEqualTo(expected); | |
| } | |
| } |
| private Bytes32 bigIntTo32B(final BigInteger x) { | ||
| byte[] a = x.toByteArray(); | ||
| if (a.length > 32) return Bytes32.wrap(a, a.length - 32); | ||
| return Bytes32.leftPad(Bytes.wrap(a)); | ||
| } | ||
|
|
||
| private Bytes32 bigIntToSigned32B(final BigInteger x) { | ||
| if (x.signum() >= 0) return bigIntTo32B(x); | ||
| byte[] a = new byte[32]; | ||
| Arrays.fill(a, (byte) 0xFF); | ||
| byte[] b = x.toByteArray(); | ||
| System.arraycopy(b, 0, a, 32 - b.length, b.length); | ||
| if (a.length > 32) return Bytes32.wrap(a, a.length - 32); | ||
| return Bytes32.leftPad(Bytes.wrap(a)); | ||
| } |
There was a problem hiding this comment.
I think it is much easier to convert Bytes -> BigInteger than the reverse because BigInteger has better facilities to negate numbers and padding comes for free since BigInteger removes zeros anyway. You just need to figure out the sign.
So I would recommend removing the code here and replace it with:
private static BigInteger bytesToBigInt(final Bytes bytes, final int sign) {
// bytes can be shorter, so it's treated as left padded with zeros
if (bytes.size() < 32) {
return new BigInteger(1, bytes.toArrayUnsafe());
}
return sign < 0
? new BigInteger(bytes.toArrayUnsafe())
: new BigInteger(1, bytes.toArrayUnsafe());
}
Have been running tests indefinitely for an hour with the suggestions above and no failures.
|
Fix for add here thomas-quadratic#3 |
6c36d5c to
0cdb13e
Compare
- shiftRight: wrong limb/index math for edge inputs - shiftLeft: array index out of bounds on all-zero slice - mod: knuth remainder misnormalizes, wrong indices/carries - mod (1-limb): uses signed %, not unsigned bigInteger is the reference model. failures are defects in evm/UInt256.java. Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
Use Bytes32.leftPad to normalize inputs before constructing BigIntegers and UInt256 values in property_signedMod_matchesEvmSemantics. Avoids sign-extension and length mismatch issues for shorter byte arrays. Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
The shiftLeft operation did not make sure to provision enough space for the result of shiftLeftInto. Now it does so, but it is somewhat wasteful at the moment, as max 8 limbs are going to be kept. This should be optimised later on. Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
0cdb13e to
98c67b0
Compare
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
| private final int[] limbs; | ||
| private final int length; | ||
|
|
||
| int[] limbs() { |
There was a problem hiding this comment.
nit: add @VisibleForTesting
| // -------------------------------------------------------------------------- | ||
|
|
||
| UInt256(final int[] limbs, final int length) { | ||
| // Unchecked length: assumes limbs have length == N_LIMBS |
There was a problem hiding this comment.
Is it an optimisation to leave unchecked?
Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com>
|
Thanks everyone for your reviews, I think the API is stronger now. |
* ADD: UInt256 implementation of evm's op MOD Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com> Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com> Co-authored-by: Nikos Baxevanis <nikos.baxevanis@gmail.com>
* ADD: UInt256 implementation of evm's op MOD Signed-off-by: Thomas Zamojski <thomas.zamojski@quadratic-labs.com> Signed-off-by: Nikos Baxevanis <nikos.baxevanis@gmail.com> Co-authored-by: Nikos Baxevanis <nikos.baxevanis@gmail.com> Signed-off-by: stefan.pingel@consensys.net <stefan.pingel@consensys.net>

PR description
This PR implements the MOD operation in the EVM using a custom UInt256 class meant to replace BigInteger for optimised arithmetics.
The UInt256 class should grow to contain other arithmetic operations, but as of now contains mod, computing the remainder of a division by the modulus. It should run about 45% faster than with BigIntegers (depending on integers' sizes). More optimisations should come later as well, but this should be already useful for early release.
Fixed Issue(s)
I did not find an opened issue for this task.
Thanks for sending a pull request! Have you done the following?
doc-change-requiredlabel to this PR if updates are required.Locally, you can run these tests to catch failures early:
./gradlew spotlessApply./gradlew build./gradlew acceptanceTest./gradlew integrationTest./gradlew ethereum:referenceTests:referenceTests