From 60fa8132de3a059b6ec8f3640357e2fb047ee959 Mon Sep 17 00:00:00 2001 From: StackOverflowExcept1on <109800286+StackOverflowExcept1on@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:23:16 +0300 Subject: [PATCH] chore: add more comments --- src/utils/cryptography/ECDSA.sol | 12 ++-- src/utils/cryptography/Schnorr.sol | 104 +++++++++++++++++++++++++++ src/utils/cryptography/Secp256k1.sol | 54 +++++++++++++- 3 files changed, 164 insertions(+), 6 deletions(-) diff --git a/src/utils/cryptography/ECDSA.sol b/src/utils/cryptography/ECDSA.sol index 667d884..fb733a2 100644 --- a/src/utils/cryptography/ECDSA.sol +++ b/src/utils/cryptography/ECDSA.sol @@ -8,16 +8,18 @@ import {Memory} from "../Memory.sol"; */ library ECDSA { /** - * @dev `ecrecover(e, v, r, s)` works according to formula $Q = r^{-1} \( sR - eG \)$ + * @dev Recovers Ethereum address from ECDSA signature. + * `ecrecover(e, v, r, s)` works according to formula $Q = r^{-1} \( sR - eG \)$ * from https://secg.org/sec1-v2.pdf#subsubsection.4.1.6. * @param memPtr Memory pointer for writing 128 bytes of input data. - * @param e Message hash, can be any 256-bit number, will be reduced to valid scalar. + * @param e Message hash, can be any 256-bit number, + * will be reduced to valid scalar (can be zero scalar). * @param v Recovery ID, can be 27 or 28. * Point `R(x, y)` has `yParity = v - 27`, `y` is calculated from `yParity`. - * @param r Scalar r, must be in `[1, Secp256k1.N)` and `x = r` must be on curve. + * @param r Non-zero scalar r, must be in `[1, Secp256k1.N)` and `x = r` must be on curve. * Point `R(x, y)` has coordinate `x = r`. - * @param s Scalar s, must be in `[1, Secp256k1.N)`. - * @return recovered 160-bit ethereum address of `Q` point. + * @param s Non-zero scalar s, must be in `[1, Secp256k1.N)`. + * @return recovered 160-bit Ethereum address of `Q` point. * @dev If `v, r, s` do not satisfy above conditions, then `recovered = 0` */ function recover(uint256 memPtr, uint256 e, uint256 v, uint256 r, uint256 s) diff --git a/src/utils/cryptography/Schnorr.sol b/src/utils/cryptography/Schnorr.sol index 481d9ca..ed6e52b 100644 --- a/src/utils/cryptography/Schnorr.sol +++ b/src/utils/cryptography/Schnorr.sol @@ -4,19 +4,54 @@ pragma solidity ^0.8.28; import {ECDSA} from "./ECDSA.sol"; import {Secp256k1} from "./Secp256k1.sol"; +/** + * @dev Library for verifying Schnorr's signature. + */ library Schnorr { + /** + * @dev Checks if public key `(x, y)` is on curve and that `x % Secp256k1.N != 0`. + * @param publicKeyX Public key x. + * @param publicKeyY Public key y. + * @return isValidPublicKey `true` if public key is valid, `false` otherwise. + */ function isValidPublicKey(uint256 publicKeyX, uint256 publicKeyY) internal pure returns (bool) { return isValidMultiplier(publicKeyX) && Secp256k1.isOnCurve(publicKeyX, publicKeyY); } + /** + * @dev Checks if `signature.R` public key `(x, y)` is on curve. + * @param signatureRX Public key x. + * @param signatureRY Public key y. + * @return isValidSignatureR `true` if `signature.R` public key is on curve, `false` otherwise. + */ function isValidSignatureR(uint256 signatureRX, uint256 signatureRY) internal pure returns (bool) { return Secp256k1.isOnCurve(signatureRX, signatureRY); } + /** + * @dev Checks if `multiplier % Secp256k1.N != 0`. + * @param multiplier Multiplier. + * @return isValidMultiplier `true` if `multiplier % Secp256k1.N != 0`, `false` otherwise. + */ function isValidMultiplier(uint256 multiplier) internal pure returns (bool) { return multiplier % Secp256k1.N != 0; } + /** + * @dev Verifies Schnorr signature by formula $zG - cX = R$. + * - Public key ($X$) must be checked with `Schnorr.isValidPublicKey(publicKeyX, publicKeyY)`. + * - Signature R ($R$) must be checked with `Schnorr.isValidSignatureR(signatureRX, signatureRY)`. + * - Signature Z ($z$) must be checked with `Schnorr.isValidMultiplier(signatureZ)`. + * - Challenge ($c$) must be checked with `Schnorr.isValidMultiplier(challenge)`. + * @param memPtr Memory pointer for writing 128 bytes of input data. + * @param publicKeyX Public key x. + * @param publicKeyY Public key y. + * @param signatureRX Signature R x. + * @param signatureRY Signature R y. + * @param signatureZ Signature Z. + * @param challenge Challenge. + * @return `true` if signature is valid, `false` otherwise. + */ function verifySignature( uint256 memPtr, uint256 publicKeyX, @@ -26,20 +61,89 @@ library Schnorr { uint256 signatureZ, uint256 challenge ) internal view returns (bool) { + // `e` is always in `[1, Secp256k1.N)` and is valid non-zero scalar because: + // + // `mulmod(a, b, Secp256k1.N)` or `(a * b) % N` is always in `[0, N)` for any `a`, `b`. + // `(a * b) % N` can be simplified to `product % N`, right? `product % N` is always in `[0, N)`. + // consider `product % 2`. remainder of division is `0` or `1`, but not `2`. + // + // consider `mulmod(a, b, Secp256k1.N)`: + // - case 1 - minimum value of `mulmod(a, b, Secp256k1.N)` is `0`. + // - case 2 - maximum value of `mulmod(a, b, Secp256k1.N)` is `Secp256k1.N - 1`. + // + // 1. minimum value of `mulmod(a, b, Secp256k1.N)` is `0`. it's not good because: + // `e` can go beyond valid non-zero scalar if `mulmod(a, b, Secp256k1.N) = 0`, + // then `e = Secp256k1.N - 0 = Secp256k1.N`, but `e` must be in `[1, Secp256k1.N)`. + // + // when `mulmod(a, b, Secp256k1.N) = 0`? + // - `a = 0` or `b = 0`. + // - `a = 1` and `b = Secp256k1.N`. + // - `a = Secp256k1.N` and `b = 1`. + // - `a = k` and `b = Secp256k1.N`. + // - `a = Secp256k1.N` and `b = k`. + // + // keep in mind that `Secp256k1.N` is prime number, i.e. it has 2 divisors: `1` and `Secp256k1.N`. + // `Secp256k1.N` can be obtained by multiplying it by `1` and no other way. + // `mulmod(k, Secp256k1.N, Secp256k1.N) = 0`, where `k` is any number, right? + // because product of `k` and `Secp256k1.N` always has `0` in remainder when divided by `Secp256k1.N`. + // + // when `mulmod(a, b, Secp256k1.N) = 0`? it can be simplified to: + // - `a % Secp256k1.N = 0` or `b % Secp256k1.N = 0`. + // + // this statement can also be verified using script: + // ```python + // p = 101 # prime number + // + // for a in range(200): + // for b in range(200): + // if (a * b) % p == 0: + // assert a % p == 0 or b % p == 0 + // ``` + // + // but `a % Secp256k1.N != 0` and `b % Secp256k1.N != 0` because it's checked with: + // - `Schnorr.isValidMultiplier(signatureZ)`. + // - `Schnorr.isValidPublicKey(publicKeyX, publicKeyY)`. + // it also checks `Schnorr.isValidMultiplier(publicKeyX)`. + // + // 2. maximum value of `mulmod(a, b, Secp256k1.N)` is `Secp256k1.N - 1`. it's good because: + // minimum value of `e` is `e = Secp256k1.N - (Secp256k1.N - 1) = 1`. + // + // thus `e` is always in `[1, Secp256k1.N)` and is valid non-zero scalar if: + // - `a % Secp256k1.N != 0` and `b % Secp256k1.N != 0`. + // + // since `e < Secp256k1.N` we can do the operation `negmod(A) = (N - A) mod N)` without `mod N`. + // `e = Secp256k1.N - mulmod_result` should be read as `-mulmod_result % Secp256k1.N`. + // also see: https://us.metamath.org/mpeuni/negmod.html. uint256 e; unchecked { e = Secp256k1.N - mulmod(signatureZ, publicKeyX, Secp256k1.N); } + // `v` is always `27` or `28`. uint256 v = Secp256k1.yParityEthereum(publicKeyY); + // `r` is always in `[1, Secp256k1.N)` and valid non-zero scalar because + // it's checked with `Schnorr.isValidPublicKey(publicKeyX, publicKeyY)`. uint256 r = publicKeyX; + // `s` is always in `[1, Secp256k1.N)` and valid non-zero scalar because + // it's described in more detail above (see `e`). uint256 s; unchecked { s = Secp256k1.N - mulmod(challenge, publicKeyX, Secp256k1.N); } + // TODO: write about formula, negmod, etc. + + // https://github.com/ZcashFoundation/frost/blob/2d88edf1623ee29f671a43966aae0bd4ead2ea7a/frost-core/src/signature.rs#L9 + // https://github.com/ZcashFoundation/frost/blob/2d88edf1623ee29f671a43966aae0bd4ead2ea7a/frost-core/src/verifying_key.rs#L54 + + // `ECDSA.recover(memPtr, e, v, r, s)` returns 160-bit Ethereum address instead of public key, + // so we also need to convert Signature R to Ethereum address using `Secp256k1.toAddress(signatureRX, signatureRY)`. + + // we also previously checked that Signature R is on curve using + // `Schnorr.isValidSignatureR(signatureRX, signatureRY)`. + return ECDSA.recover(memPtr, e, v, r, s) == Secp256k1.toAddress(signatureRX, signatureRY); } } diff --git a/src/utils/cryptography/Secp256k1.sol b/src/utils/cryptography/Secp256k1.sol index 04e170b..737503c 100644 --- a/src/utils/cryptography/Secp256k1.sol +++ b/src/utils/cryptography/Secp256k1.sol @@ -3,20 +3,59 @@ pragma solidity ^0.8.28; import {Hashes} from "./Hashes.sol"; +/** + * @dev Library for interaction with secp256k1 elliptic curve, + * described by equation `y^2 = x^3 + ax + b (mod p)` + * where `a = 0` and `b = 7`. + * @dev Curve parameters taken from: + * - https://en.bitcoin.it/wiki/Secp256k1. + * - https://github.com/ethereum/go-ethereum/blob/5c3b792e6161a7d8a8d0b7c59d7b7bcffc8bf3d5/crypto/secp256k1/curve.go#L282. + */ library Secp256k1 { - uint256 internal constant B = 7; + /** + * @dev Curve parameter `a = 0`. + */ + uint256 internal constant A = 0x0000000000000000000000000000000000000000000000000000000000000000; + /** + * @dev Curve parameter `b = 7`. + */ + uint256 internal constant B = 0x0000000000000000000000000000000000000000000000000000000000000007; + /** + * @dev Prime number, public key `(x, y)`, where `(x, y)` must be in `[0, Secp256k1.P)`. + */ uint256 internal constant P = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F; + /** + * @dev Prime number, scalar `s` must be in `[0, Secp256k1.N)`, non-zero scalar must be in `[1, Secp256k1.N)`. + */ uint256 internal constant N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; + /** + * @dev Checks if public key `(x, y)` is on curve. + * @param x Public key x. + * @param y Public key y. + * @return isOnCurve `true` if public key is on curve, `false` otherwise. + */ function isOnCurve(uint256 x, uint256 y) internal pure returns (bool) { + // https://github.com/ethereum/go-ethereum/blob/5c3b792e6161a7d8a8d0b7c59d7b7bcffc8bf3d5/crypto/secp256k1/curve.go#L94 return mulmod(y, y, P) == addmod(mulmod(x, mulmod(x, x, P), P), B, P); } + /** + * @dev Calculates `yParity` from public key y. + * @param y Public key y. + * @return yParity `0` if `y` is even, `1` if `y` is odd. + */ function yParity(uint256 y) internal pure returns (uint256) { return y & 1; } + /** + * @dev Calculates `yParity` for Ethereum from public key y. + * @param y Public key y. + * @return ethereumYParity `27` if `y` is even, `28` if `y` is odd. + */ function yParityEthereum(uint256 y) internal pure returns (uint256) { + // https://github.com/ethereum/go-ethereum/blob/5c3b792e6161a7d8a8d0b7c59d7b7bcffc8bf3d5/core/vm/contracts.go#L253 uint256 ethereumYParity; unchecked { ethereumYParity = yParity(y) + 27; @@ -24,7 +63,13 @@ library Secp256k1 { return ethereumYParity; } + /** + * @dev Calculates compressed `y`. + * @param y Public key y. + * @return compressedY Compressed `y`, `2` if `y` is even, `3` if `y` is odd. + */ function yCompressed(uint256 y) internal pure returns (uint256) { + // https://github.com/ethereum/go-ethereum/blob/5c3b792e6161a7d8a8d0b7c59d7b7bcffc8bf3d5/crypto/secp256k1/libsecp256k1/src/eckey_impl.h#L45 uint256 compressedY; unchecked { compressedY = yParity(y) + 2; @@ -32,7 +77,14 @@ library Secp256k1 { return compressedY; } + /** + * @dev Computes Ethereum address from full public key `(x, y)`. + * @param x Public key x. + * @param y Public key y. + * @return addr Ethereum address. + */ function toAddress(uint256 x, uint256 y) internal pure returns (uint256 addr) { + // https://github.com/ethereum/go-ethereum/blob/5c3b792e6161a7d8a8d0b7c59d7b7bcffc8bf3d5/core/vm/contracts.go#L272 uint256 fullHash = Hashes.efficientKeccak256(x, y); assembly ("memory-safe") { // addr = fullHash & ((1 << 160) - 1)