Skip to content
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

Implement P256 verification via RIP-7212 precompile with Solidity fallback #4881

Open
wants to merge 79 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 70 commits
Commits
Show all changes
79 commits
Select commit Hold shift + click to select a range
5e82076
Add P256 implementation and testing
Amxx Feb 7, 2024
da0f27e
enable optimizations by default
Amxx Feb 7, 2024
aa59c67
test recovering address
Amxx Feb 7, 2024
9512947
improved testing
Amxx Feb 7, 2024
a60bf48
spelling
Amxx Feb 7, 2024
9185026
fix lint
Amxx Feb 7, 2024
025e360
expose imports tick
Amxx Feb 7, 2024
803e735
fix lint
Amxx Feb 7, 2024
57fcecd
fix lint
Amxx Feb 7, 2024
4dae298
add changeset
Amxx Feb 7, 2024
6cf039d
improve doc
Amxx Feb 7, 2024
c094fa1
add envvar to force allowUnlimitedContractSize
Amxx Feb 7, 2024
20a03df
fix lint
Amxx Feb 7, 2024
15f1a6b
fix stack too deep error in coverage
Amxx Feb 7, 2024
e2040e4
reoder arguments to match ecrecover and EIP-7212
Amxx Feb 13, 2024
695b732
reduce diff
Amxx Mar 13, 2024
41aaf71
Merge branch 'master' into feature/P256
Amxx Mar 13, 2024
3bf4557
Update contracts/utils/cryptography/P256.sol
Amxx Apr 24, 2024
3cbf426
Merge branch 'master' into feature/P256
Amxx Apr 25, 2024
bba7fa3
update pseudocode reference
Amxx Apr 25, 2024
2812ed8
Update contracts/utils/cryptography/P256.sol
Amxx Apr 25, 2024
e0ef63b
refactor neutral element in jAdd
Amxx Apr 26, 2024
61a244d
add EIP-7212 support
Amxx May 17, 2024
910bc71
Merge branch 'master' into feature/P256
Amxx Jun 12, 2024
2e9d04d
Apply PR suggestions
ernestognw Jun 14, 2024
9062633
move invModPrime to Math.sol
Amxx Jun 17, 2024
a44bb71
update
Amxx Jun 17, 2024
3a6e1f5
update
Amxx Jun 17, 2024
3e71fad
codespell
Amxx Jun 17, 2024
887272b
test signature maleability
Amxx Jun 17, 2024
433548f
Iterate
ernestognw Jun 20, 2024
4f80ca0
Add more comments
ernestognw Jun 21, 2024
be69f5c
remove P256 public key to address derivation
Amxx Jun 21, 2024
fcde23f
Move publicKey from privateKey derivation function to tests
ernestognw Jun 21, 2024
5828566
Remove unnecessary test
ernestognw Jun 21, 2024
9362936
add wycheproof test
cairoeth Jun 21, 2024
921745b
Readd malleability check and rename
ernestognw Jun 22, 2024
2c113f4
Change arguments to bytes32
ernestognw Jun 22, 2024
fb7dc6f
remove unused malleable version
Amxx Jun 24, 2024
f264dae
Update contracts/utils/cryptography/P256.sol
Amxx Jun 24, 2024
2c9a137
Update contracts/utils/cryptography/P256.sol
Amxx Jun 24, 2024
f4cbf51
up
Amxx Jun 24, 2024
0227656
recovery malleability
Amxx Jun 24, 2024
e3a8338
fix bug (inverse return values)
Amxx Jun 24, 2024
cbd2ff5
better private key gen
cairoeth Jun 24, 2024
194f19a
Update contracts/utils/cryptography/P256.sol
ernestognw Jun 24, 2024
704a12e
Fix hardhat tests and add documentation
ernestognw Jun 24, 2024
61d52a5
Update test/utils/cryptography/P256.test.js
Amxx Jun 24, 2024
242c796
Ensure lower s in Foundry tests
ernestognw Jun 24, 2024
787834d
Lint
ernestognw Jun 24, 2024
d8f4f7e
fix bug for valid signatures with large `r` values
cairoeth Jun 24, 2024
fc54017
run original wycheproof in hardhat
Amxx Jun 24, 2024
5a7887b
Merge remote-tracking branch 'amxx/feature/P256' into feature/P256
Amxx Jun 24, 2024
cc82c17
Update test/utils/cryptography/P256.test.js
Amxx Jun 24, 2024
a67e5a2
Almost fix tests
ernestognw Jun 24, 2024
4c93009
Bound r to N so for lower s values
ernestognw Jun 24, 2024
046463c
Remove unnecessary comment
ernestognw Jun 24, 2024
e4df1d1
Remove foundry wycheproof
ernestognw Jun 24, 2024
1bddcf5
Tests nit
ernestognw Jun 24, 2024
e5ba358
Update .changeset/odd-lobsters-wash.md
ernestognw Jun 24, 2024
2eecacf
Update test/utils/cryptography/P256.t.sol
ernestognw Jun 24, 2024
ced4fb8
Update P256.t.sol
Amxx Jun 24, 2024
b82af11
Merge branch 'master' into feature/P256
ernestognw Jun 24, 2024
c6a86d9
Add more docs and nit
ernestognw Jun 24, 2024
9b24014
Manage to compile without via-ir
ernestognw Jun 25, 2024
3616771
Improve comments
ernestognw Jun 25, 2024
be078b1
Remove unnecessary CI flag
ernestognw Jun 25, 2024
ecd3aa2
cleanup _jAdd with memory
Amxx Jun 25, 2024
d83e707
up
Amxx Jun 25, 2024
fbc11f5
Update contracts/utils/cryptography/P256.sol
Amxx Jun 25, 2024
9c88101
Apply suggestions from code review
Amxx Jun 25, 2024
b5e6bd7
Update hardhat.config.js
Amxx Jun 25, 2024
db76353
Update hardhat.config.js
Amxx Jun 25, 2024
0722d93
Update hardhat.config.js
Amxx Jun 25, 2024
306a5f6
Revert all changes to hardhat.config.js
Amxx Jun 26, 2024
e67a456
uniform style
Amxx Jun 26, 2024
1a8cb63
add bound checks to isOnCurve
Amxx Jun 26, 2024
3c3fa27
rename isOnCurve -> isValidPublicKey + add _isProperSignature helper
Amxx Jun 26, 2024
2fe4a16
Update contracts/utils/cryptography/P256.sol
ernestognw Jun 27, 2024
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/odd-lobsters-wash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'openzeppelin-solidity': minor
---

`P256`: Library for verification and public key recovery of P256 (aka secp256r1) signatures.
1 change: 1 addition & 0 deletions contracts/mocks/Stateless.sol
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {ERC721Holder} from "../token/ERC721/utils/ERC721Holder.sol";
import {Math} from "../utils/math/Math.sol";
import {MerkleProof} from "../utils/cryptography/MerkleProof.sol";
import {MessageHashUtils} from "../utils/cryptography/MessageHashUtils.sol";
import {P256} from "../utils/cryptography/P256.sol";
import {Panic} from "../utils/Panic.sol";
import {Packing} from "../utils/Packing.sol";
import {RSA} from "../utils/cryptography/RSA.sol";
Expand Down
5 changes: 5 additions & 0 deletions contracts/utils/Errors.sol
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ library Errors {
* @dev The deployment failed.
*/
error FailedDeployment();

/**
* @dev A necessary precompile is missing.
*/
error MissingPrecompile(address);
}
2 changes: 2 additions & 0 deletions contracts/utils/README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Miscellaneous contracts and libraries containing utility functions you can use t
* {Math}, {SignedMath}: Implementation of various arithmetic functions.
* {SafeCast}: Checked downcasting functions to avoid silent truncation.
* {ECDSA}, {MessageHashUtils}: Libraries for interacting with ECDSA signatures.
* {P256}: Library for verifying and recovering public keys from secp256r1 signatures.
* {RSA}: Library with RSA PKCS#1 v1.5 signature verification utilities.
* {SignatureChecker}: A library helper to support regular ECDSA from EOAs as well as ERC-1271 signatures for smart contracts.
* {Hashes}: Commonly used hash functions.
* {MerkleProof}: Functions for verifying https://en.wikipedia.org/wiki/Merkle_tree[Merkle Tree] proofs.
Expand Down
302 changes: 302 additions & 0 deletions contracts/utils/cryptography/P256.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,302 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;

import {Math} from "../math/Math.sol";
import {Errors} from "../Errors.sol";

/**
* @dev Implementation of secp256r1 verification and recovery functions.
*
* The secp256r1 curve (also known as P256) is a NIST standard curve with wide support in modern devices
* and cryptographic standards. Some notable examples include Apple's Secure Enclave and Android's Keystore
* as well as authentication protocols like FIDO2.
*
* Based on the original https://github.com/itsobvioustech/aa-passkeys-wallet/blob/main/src/Secp256r1.sol[implementation of itsobvioustech].
* Heavily inspired in https://github.com/maxrobot/elliptic-solidity/blob/master/contracts/Secp256r1.sol[maxrobot] and
* https://github.com/tdrerup/elliptic-curve-solidity/blob/master/contracts/curves/EllipticCurve.sol[tdrerup] implementations.
*/
library P256 {
struct JPoint {
uint256 x;
uint256 y;
uint256 z;
}

/// @dev Generator (x component)
uint256 internal constant GX = 0x6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296;
/// @dev Generator (y component)
uint256 internal constant GY = 0x4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5;
/// @dev P (size of the field)
uint256 internal constant P = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF;
/// @dev N (order of G)
uint256 internal constant N = 0xFFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551;
/// @dev A parameter of the weierstrass equation
uint256 internal constant A = 0xFFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC;
/// @dev B parameter of the weierstrass equation
uint256 internal constant B = 0x5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B;
ernestognw marked this conversation as resolved.
Show resolved Hide resolved

/// @dev (P + 1) / 4. Useful to compute sqrt
uint256 private constant P1DIV4 = 0x3fffffffc0000000400000000000000000000000400000000000000000000000;

/// @dev N/2 for excluding higher order `s` values
uint256 private constant HALF_N = 0x7fffffff800000007fffffffffffffffde737d56d38bcf4279dce5617e3192a8;

/**
* @dev Verifies a secp256r1 signature using the RIP-7212 precompile and falls back to the Solidity implementation
* if the precompile is not available. This version should work on all chains, but requires the deployment of more
* bytecode.
*
* @param h - hashed message
* @param r - signature half R
* @param s - signature half S
* @param qx - public key coordinate X
* @param qy - public key coordinate Y
*
* IMPORTANT: This function disallows signatures where the `s` value is above `N/2` to prevent malleability.
* To flip the `s` value, compute `s = N - s`.
*/
function verify(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
(bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
return supported ? valid : verifySolidity(h, r, s, qx, qy);
}

/**
* @dev Same as {verify}, but it will revert if the required precompile is not available.
Copy link
Member

Choose a reason for hiding this comment

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

I guess this would address general concerns about the availability of the precompile

Suggested change
* @dev Same as {verify}, but it will revert if the required precompile is not available.
* @dev Same as {verify}, but it will revert if the required precompile is not available.
*
* IMPORTANT: The use of this function relies on the RIP-7212 precompile to be available at `address(0x100)`.
* Make sure the specified address contains the expected precompile, otherwise the returned value may be
* misinterpreted as a positive boolean.

*/
function verifyNative(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
(bool valid, bool supported) = _tryVerifyNative(h, r, s, qx, qy);
if (supported) {
return valid;
} else {
revert Errors.MissingPrecompile(address(0x100));
}
}

/**
* @dev Same as {verify}, but it will return false if the required precompile is not available.
*/
function _tryVerifyNative(
bytes32 h,
bytes32 r,
bytes32 s,
bytes32 qx,
bytes32 qy
) private view returns (bool valid, bool supported) {
if (r == 0 || uint256(r) >= N || s == 0 || uint256(s) > HALF_N || !isOnCurve(qx, qy)) {
return (false, true); // signature is invalid, and its not because the precompile is missing
}

(bool success, bytes memory returndata) = address(0x100).staticcall(abi.encode(h, r, s, qx, qy));
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
return (success && returndata.length == 0x20) ? (abi.decode(returndata, (bool)), true) : (false, false);
Copy link
Contributor

Choose a reason for hiding this comment

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

What if Ethereum puts another precompile at 0x100 in the future that also returns a 0x20 length return data? Might be an edge case, but EIP and RIP must not align in the long run! Or another example is, if an L2 already put a precompile on 0x100 which is different but also returns 0x20 length (haven't done research on this tbh).

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We were told that EIP-7212 is not up to date, that RIP-7212 is the one to follow, and that precompile address allocation would be organised at the RIP level to avoid conflicts.

If the community can't figure that out, wtf are we (library devs) expected to do ?

Copy link
Contributor

Choose a reason for hiding this comment

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

"would be organised at the RIP level to avoid conflicts." -> talk is cheap ;) so idk, I just don't trust this really. The reason why I haven't provided a native way to call the P256 precompile in my snekmate (Vyper) library is that I leave this decision to the devs deploying it. I.e. they plan to deploy on 5 chains, 3 of them have the precompile and 2 not, so they write their own wrapper around it using e.g. 0x100 as target since it's ensured that it exists there.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

If you don't want to risk it, just use verifySolidity

Copy link
Contributor

Choose a reason for hiding this comment

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

you know how people are: "hey let's gas optimise everything and native looks sweet in our tests since we run it on a P256-supported fork." I think a good compromise is to put a warning in the docs about the current situation?

Copy link
Contributor

Choose a reason for hiding this comment

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

but not with the idea that we should not provide features unless they work on every chain.

I never said this. My main point here is that the precompile check is not future proof as the return data of length 32 bytes could also come from other precompiles in the future on that address. I'm not saying this will happen, but I have seen too much shit over the last years to be paranoid enough about this edge case.

Copy link
Member

@ernestognw ernestognw Jun 26, 2024

Choose a reason for hiding this comment

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

Thanks for pointing out @pcaversaccio. I agree with the concern and now I think more about it I also agree we should explain the situation at the very minimum. We raised a similar concern for Math.modexp that ended up in an explicit documentation mention, so I would consider adding a note here.

Regardless, both RIP-7212 and EIP-7212 agree that the precompile "returns 1 in 32 bytes format". The current implementation reverts if the returndata can't be decoded as a boolean. In my opinion, it's unlikely that a precompile at address(0x100) will randomly return true but I recognize the possibility. Would you agree the decoding constrains the possibility?

Copy link
Contributor

Choose a reason for hiding this comment

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

Would you agree the decoding constrains the possibility?

Overall yes, but I want to raise that this might result in an unexpected behaviour since you don't expect a revert from a precompile usually (but no return data to indicate an error).

Copy link
Member

Choose a reason for hiding this comment

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

I agree it's unexpected behavior, but it's a manageable risk.

I think a good compromise is to put a warning in the docs about the current situation?

I hear you with this and think an "IMPORTANT" block should be enough

Copy link
Contributor

Choose a reason for hiding this comment

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

sgtm!

}

/**
* @dev Same as {verify}, but only the Solidity implementation is used.
*/
function verifySolidity(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
if (r == 0 || uint256(r) >= N || s == 0 || uint256(s) > HALF_N || !isOnCurve(qx, qy)) {
return false;
}
Copy link
Contributor

@cairoeth cairoeth Jun 26, 2024

Choose a reason for hiding this comment

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

for a public key to be valid, it should:

  1. not be point of infinity
  2. have coordinates $(x, y)$ satisfy $0 <= x, y <= p$, and
  3. satisfy the equation $y^2=x3+ax+b$ modulo $p$

on property one: the public key is used in _preComputeJacobianPoints by points[0x01] = JPoint(px, py, 1); // 1,0 (p), which would never be the point of infinity. _affineFromJacobian uses the convention that $(0,0)$ represents the point of infinity, so in this case, the conversion in _preComputeJacobianPoints (L278) would be incorrect, but given that the point of infinity is not valid anyways, it's not an issue as we instead reject the public key $(0,0)$ via isOnCurve.

on property two: x and y coordinates of the public key get reduced to modulo p in calculations, so missing a check for property two means that the verify function will check the signature for the public key with coordinates $(x \bmod p, y \bmod p)$. This would allow for public keys $(x,y)$, where $x<2^{256} - p$ or $y<2^{256} - p$, which there would be another pair $(x^\prime,y^\prime)$ — such as $(x+p,y)$ — that can be used as a public key and for which signatures made for $(x,y)$ would also verify.

on property three: we are checking that it lies on the curve via the Weierstrass equation (isOnCurve).

i think properties 1 and 3 are ok, but missing 2.

Copy link
Collaborator Author

@Amxx Amxx Jun 26, 2024

Choose a reason for hiding this comment

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

  1. Because B != 0 in Sepc256r1, then (0, 0) is not on the curve ... that should be ok
  2. So should we add uint256(qx) >= P || uint256(qy) >= P ? Also, is it P or N?

Copy link
Contributor

@cairoeth cairoeth Jun 26, 2024

Choose a reason for hiding this comment

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

yeah that should be good. it's P because we are using that for the modulus operations on calculations like isOnCurve

Suggested change
function verifySolidity(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
if (r == 0 || uint256(r) >= N || s == 0 || uint256(s) > HALF_N || !isOnCurve(qx, qy)) {
return false;
}
function verifySolidity(bytes32 h, bytes32 r, bytes32 s, bytes32 qx, bytes32 qy) internal view returns (bool) {
if (r == 0 || uint256(r) >= N || s == 0 || uint256(s) > HALF_N || uint256(qx) >= P || uint256(qy) >= P || !isOnCurve(qx, qy)) {
return false;
}

tests pass with this change, but i think it's useful to add a unit test for this property specifically, i can add it

Copy link
Contributor

Choose a reason for hiding this comment

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

i think it also makes sense to have this in _tryVerifyNative

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I added the check directly in isOnCurve

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Note: the public key is unlikelly to be arbitrary user input. It will be recorded/whitelisted onchain, by some admin. So this attack vector is not really critical. But still its good to add the (cheap) check in isOnCurve

Copy link
Member

Choose a reason for hiding this comment

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

This would allow for public keys $(x,y)$, where $x<2^{256} - p$ or $y<2^{256} - p$, which there would be another pair $(x^\prime,y^\prime)$ — such as $(x+p,y)$ — that can be used as a public key and for which signatures made for $(x,y)$ would also verify.'

Although this is true, it's unclear to me whether you can create a signature for public keys $(x,y)$ if it's not on the curve. I still think checking is valuable and pretty much agree with @Amxx's conclusion that mostly public keys won't be arbitrary and the check is cheap.

Copy link
Contributor

@cairoeth cairoeth Jun 26, 2024

Choose a reason for hiding this comment

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

if it's not on the curve: the problem is that you can find $(x^\prime,y^\prime)$ (like $(x+p,y)$ ) that are on the curve based on the original $(x,y)$ which are on the curve too -- it's kinda like a signature malleability attack vector where we should bound the range.

Copy link
Member

Choose a reason for hiding this comment

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

Got it, so it's the other way around. Still it's relevant that public keys are unlikely to be arbitrary, but let's keep the check regardless.


JPoint[16] memory points = _preComputeJacobianPoints(uint256(qx), uint256(qy));
uint256 w = Math.invModPrime(uint256(s), N);
uint256 u1 = mulmod(uint256(h), w, N);
uint256 u2 = mulmod(uint256(r), w, N);
(uint256 x, ) = _jMultShamir(points, u1, u2);
return ((x % N) == uint256(r));
}

/**
* @dev Public key recovery
*
* @param h - hashed message
* @param v - signature recovery param
* @param r - signature half R
* @param s - signature half S
*
* IMPORTANT: This function disallows signatures where the `s` value is above `N/2` to prevent malleability.
* To flip the `s` value, compute `s = N - s` and `v = 1 - v` if (`v = 0 | 1`).
*/
function recovery(bytes32 h, uint8 v, bytes32 r, bytes32 s) internal view returns (bytes32, bytes32) {
if (r == 0 || uint256(r) >= N || s == 0 || uint256(s) > HALF_N || v > 1) return (0, 0);

uint256 rx = uint256(r);
uint256 ry2 = addmod(mulmod(addmod(mulmod(rx, rx, P), A, P), rx, P), B, P); // weierstrass equation y² = x³ + a.x + b
uint256 ry = Math.modExp(ry2, P1DIV4, P); // This formula for sqrt work because P ≡ 3 (mod 4)
if (mulmod(ry, ry, P) != ry2) return (0, 0); // Sanity check
if (ry % 2 != v % 2) ry = P - ry;

JPoint[16] memory points = _preComputeJacobianPoints(rx, ry);
uint256 w = Math.invModPrime(uint256(r), N);
uint256 u1 = mulmod(N - (uint256(h) % N), w, N);
uint256 u2 = mulmod(uint256(s), w, N);
(uint256 x, uint256 y) = _jMultShamir(points, u1, u2);
return (bytes32(x), bytes32(y));
}

/**
* @dev Checks if a point is on the curve.
*/
function isOnCurve(bytes32 x, bytes32 y) internal pure returns (bool result) {
assembly ("memory-safe") {
let p := P
let lhs := mulmod(y, y, p) // y^2
let rhs := addmod(mulmod(addmod(mulmod(x, x, p), A, p), x, p), B, p) // ((x^2 + a) * x) + b = x^3 + ax + b
result := eq(lhs, rhs) // Should conform with the Weierstrass equation
}
}

/**
* @dev Reduce from jacobian to affine coordinates
* @param jx - jacobian coordinate x
* @param jy - jacobian coordinate y
* @param jz - jacobian coordinate z
* @return ax - affine coordinate x
* @return ay - affine coordinate y
*/
function _affineFromJacobian(uint256 jx, uint256 jy, uint256 jz) private view returns (uint256 ax, uint256 ay) {
if (jz == 0) return (0, 0);
uint256 zinv = Math.invModPrime(jz, P);
uint256 zzinv = mulmod(zinv, zinv, P);
uint256 zzzinv = mulmod(zzinv, zinv, P);
ax = mulmod(jx, zzinv, P);
ay = mulmod(jy, zzzinv, P);
}

/**
* @dev Point addition on the jacobian coordinates
* Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#addition-add-1998-cmo-2
*/
function _jAdd(
JPoint memory p1,
uint256 x2,
uint256 y2,
uint256 z2
) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
assembly ("memory-safe") {
let p := P
let z1 := mload(add(p1, 0x40))
let s1 := mulmod(mload(add(p1, 0x20)), mulmod(mulmod(z2, z2, p), z2, p), p) // s1 = y1*z2³
let s2 := mulmod(y2, mulmod(mulmod(z1, z1, p), z1, p), p) // s2 = y2*z1³
let r := addmod(s2, sub(p, s1), p) // r = s2-s1
let u1 := mulmod(mload(p1), mulmod(z2, z2, p), p) // u1 = x1*z2²
let u2 := mulmod(x2, mulmod(z1, z1, p), p) // u2 = x2*z1²
let h := addmod(u2, sub(p, u1), p) // h = u2-u1
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
let hh := mulmod(h, h, p) // h²

// x' = r²-h³-2*u1*h²
rx := addmod(
addmod(mulmod(r, r, p), sub(p, mulmod(h, hh, p)), p),
sub(p, mulmod(2, mulmod(u1, hh, p), p)),
p
)
// y' = r*(u1*h²-x')-s1*h³
ry := addmod(
mulmod(r, addmod(mulmod(u1, hh, p), sub(p, rx), p), p),
sub(p, mulmod(s1, mulmod(h, hh, p), p)),
p
)
// z' = h*z1*z2
rz := mulmod(h, mulmod(z1, z2, p), p)
}
}

/**
* @dev Point doubling on the jacobian coordinates
* Reference: https://www.hyperelliptic.org/EFD/g1p/auto-shortw-jacobian.html#doubling-dbl-1998-cmo-2
*/
function _jDouble(uint256 x, uint256 y, uint256 z) private pure returns (uint256 rx, uint256 ry, uint256 rz) {
assembly ("memory-safe") {
let p := P
let yy := mulmod(y, y, p)
ernestognw marked this conversation as resolved.
Show resolved Hide resolved
let zz := mulmod(z, z, p)
let s := mulmod(4, mulmod(x, yy, p), p) // s = 4*x*y²
let m := addmod(mulmod(3, mulmod(x, x, p), p), mulmod(A, mulmod(zz, zz, p), p), p) // m = 3*x²+a*z⁴
let t := addmod(mulmod(m, m, p), sub(p, mulmod(2, s, p)), p) // t = m²-2*s

// x' = t
rx := t
// y' = m*(s-t)-8*y⁴
ry := addmod(mulmod(m, addmod(s, sub(p, t), p), p), sub(p, mulmod(8, mulmod(yy, yy, p), p)), p)
// z' = 2*y*z
rz := mulmod(2, mulmod(y, z, p), p)
}
}

/**
* @dev Compute P·u1 + Q·u2 using the precomputed points for P and Q (see {_preComputeJacobianPoints}).
*
* Uses Strauss Shamir trick for EC multiplication
* https://stackoverflow.com/questions/50993471/ec-scalar-multiplication-with-strauss-shamir-method
* we optimise on this a bit to do with 2 bits at a time rather than a single bit
* the individual points for a single pass are precomputed
* overall this reduces the number of additions while keeping the same number of doublings
*/
function _jMultShamir(JPoint[16] memory points, uint256 u1, uint256 u2) private view returns (uint256, uint256) {
uint256 x = 0;
uint256 y = 0;
uint256 z = 0;
unchecked {
for (uint256 i = 0; i < 128; ++i) {
if (z > 0) {
(x, y, z) = _jDouble(x, y, z);
(x, y, z) = _jDouble(x, y, z);
}
// Read 2 bits of u1, and 2 bits of u2. Combining the two give a lookup index in the table.
uint256 pos = ((u1 >> 252) & 0xc) | ((u2 >> 254) & 0x3);
if (pos > 0) {
if (z == 0) {
(x, y, z) = (points[pos].x, points[pos].y, points[pos].z);
} else {
(x, y, z) = _jAdd(points[pos], x, y, z);
}
}
u1 <<= 2;
u2 <<= 2;
}
}
return _affineFromJacobian(x, y, z);
}

/**
* @dev Precompute a matrice of useful jacobian points associated with a given P. This can be seen as a 4x4 matrix
* that contains combination of P and G (generator) up to 3 times each. See the table below:
*
* ┌────┬─────────────────────┐
* │ i │ 0 1 2 3 │
* ├────┼─────────────────────┤
* │ 0 │ 0 p 2p 3p │
* │ 4 │ g g+p g+2p g+3p │
* │ 8 │ 2g 2g+p 2g+2p 2g+3p │
* │ 12 │ 3g 3g+p 3g+2p 3g+3p │
* └────┴─────────────────────┘
*/
function _preComputeJacobianPoints(uint256 px, uint256 py) private pure returns (JPoint[16] memory points) {
points[0x00] = JPoint(0, 0, 0); // 0,0
points[0x01] = JPoint(px, py, 1); // 1,0 (p)
points[0x04] = JPoint(GX, GY, 1); // 0,1 (g)
points[0x02] = _jDoublePoint(points[0x01]); // 2,0 (2p)
points[0x08] = _jDoublePoint(points[0x04]); // 0,2 (2g)
points[0x03] = _jAddPoint(points[0x01], points[0x02]); // 3,0 (3p)
points[0x05] = _jAddPoint(points[0x01], points[0x04]); // 1,1 (p+g)
points[0x06] = _jAddPoint(points[0x02], points[0x04]); // 2,1 (2p+g)
points[0x07] = _jAddPoint(points[0x03], points[0x04]); // 3,1 (3p+g)
points[0x09] = _jAddPoint(points[0x01], points[0x08]); // 1,2 (p+2g)
points[0x0a] = _jAddPoint(points[0x02], points[0x08]); // 2,2 (2p+2g)
points[0x0b] = _jAddPoint(points[0x03], points[0x08]); // 3,2 (3p+2g)
points[0x0c] = _jAddPoint(points[0x04], points[0x08]); // 0,3 (g+2g)
points[0x0d] = _jAddPoint(points[0x01], points[0x0c]); // 1,3 (p+3g)
points[0x0e] = _jAddPoint(points[0x02], points[0x0c]); // 2,3 (2p+3g)
points[0x0f] = _jAddPoint(points[0x03], points[0x0C]); // 3,3 (3p+3g)
}

function _jAddPoint(JPoint memory p1, JPoint memory p2) private pure returns (JPoint memory) {
(uint256 x, uint256 y, uint256 z) = _jAdd(p1, p2.x, p2.y, p2.z);
return JPoint(x, y, z);
}

function _jDoublePoint(JPoint memory p) private pure returns (JPoint memory) {
(uint256 x, uint256 y, uint256 z) = _jDouble(p.x, p.y, p.z);
return JPoint(x, y, z);
}
}
19 changes: 17 additions & 2 deletions contracts/utils/math/Math.sol
Original file line number Diff line number Diff line change
Expand Up @@ -237,8 +237,8 @@ library Math {
*
* If the input value is not inversible, 0 is returned.
*
* NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Ferma's little theorem and get the
* inverse using `Math.modExp(a, n - 2, n)`.
* NOTE: If you know for sure that n is (big) a prime, it may be cheaper to use Fermat's little theorem and get the
* inverse using `Math.modExp(a, n - 2, n)`. See {invModPrime}.
*/
function invMod(uint256 a, uint256 n) internal pure returns (uint256) {
unchecked {
Expand Down Expand Up @@ -288,6 +288,21 @@ library Math {
}
}

/**
* @dev Variant of {invMod}. More efficient, but only works if `p` is known to be a prime greater than `2`.
*
* From https://en.wikipedia.org/wiki/Fermat%27s_little_theorem[Fermat's little theorem], we know that if p is
* prime, then `a**(p-1) ≡ 1 mod p`. As a consequence, we have `a * a**(p-2) ≡ 1 mod p`, which means that
* `a**(p-2)` is the modular multiplicative inverse of a in Fp.
*
* NOTE: this function does NOT check that `p` is a prime greater than `2`.
*/
function invModPrime(uint256 a, uint256 p) internal view returns (uint256) {
unchecked {
return Math.modExp(a, p - 2, p);
}
}

/**
* @dev Returns the modular exponentiation of the specified base, exponent and modulus (b ** e % m)
*
Expand Down
Loading
Loading