Two non-zero numbers
As an example of how this is relevant in EVM, Seaport 1.1 has the notion of ‘partial fills’, where one can place an order for exchanging, say, 12 ERC1155 tokens for 36 wei while allowing it to be partially filled–this can be filled, as long as you can divide both sides perfectly, i.e., for example by supplying 3 wei, 12 wei, 18 wei, etc to receive 1 token, 3 tokens and 6 tokens respectively.
Seaport’s partial fill mechanism requires implementing a rudimentary
rational arithmetic in EVM. A filler can fill certain orders
partially, where the fraction will be provided as an input (two
uint120
numbers). The ideal representation of such a fraction
We talk about some techniques to verify this:
The GCD can be computed using euclidean division algorithm. A
recursive algorithm involves computing mod
repeatedly. One can
implement a straightforward iterative solution for this as well,
which is what’s implemented in Seaport 1.1.
It’s interesting to understand what numbers can lead to the maximum
number iterations for the Euclidean GCD algorithm. It’s surprising
that the smallest numbers for which the GCD takes uint120
numbers are the two largest Fibonacci numbers below type(uint120).max
,
i.e.,
But can we do better?
The numbers
The idea here is for the user to compute Bézout coefficients as
witnesses off-chain and provide these witnesses into the calldata
to
perform a cheap verification on-chain.
Here’s an implementation of it in solidity.
function verify_coprimality(uint120 a, uint120 b, int x, int y) pure {
require(int(uint256(a)) * x + int(uint256(b)) * y == 1);
}
We still need to ensure that there is a witness that satisfies the following:
-
$a ⋅ x$ and$b ⋅ x$ does not overflow (in 256 bit signed integer arithmetic). -
$a ⋅ x + b ⋅ x$ does not overflow (in 256 bit signed integer arithmetic).
Given a Bezout pair
The second requirement follows as
A fun open question is whether the above computation can be made unchecked? We already mentioned that there is always a witness that avoids the overflow. However, is it possible to produce a fake witnesses if the arithmetic operations wrap, i.e., all arithmetic is done in modulo 2256 arithmetic? Saw-mon & Natalie has followed up with an explicit example for this–it is indeed possible to produce fake witnesses if we replace the checked arithmetic by wrapping arithmetic!
For the general case, i.e., if the numbers
Two numbers
Note: Bézout coefficients from the last section, can be used as the
numbers mulmod
in EVM! Note: there is a unique solution for
Similar to the above, the idea here is for the user to compute the inverse off-chain, by the extended euclidean algorithm or some clever exponentiation tricks.
function verify_coprimality(uint120 a, uint120 b, uint120 a_inverse) pure {
require(mulmod(a, a_inverse, b) == 1);
}
Gas: very cheap! mulmod
is 8
gas.
Note: this works even when uint120
is replaced by uint
, which
solves some of the issues mentioned before.
Here’s a generic implementation:
function verify_coprimality(uint a, uint b, uint a_inverse) pure {
require(mulmod(a, a_inverse, b) == 1);
}