From aae77503645778d002adb639fdd7886f53f5d62f Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 26 Nov 2023 12:40:49 +0100 Subject: [PATCH 01/11] Stealth: Adds draft implementation --- src/stealth-addresses/StealthSecp256k1.sol | 150 +++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 src/stealth-addresses/StealthSecp256k1.sol diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol new file mode 100644 index 0000000..8bbe07c --- /dev/null +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -0,0 +1,150 @@ +/* + + ██████ ██████  ██  ██ ███████  ██████  ██ +██      ██   ██  ██  ██  ██      ██    ██ ██ +██  ██████    ████   ███████ ██  ██ ██ +██  ██   ██   ██        ██ ██  ██ ██ + ██████ ██  ██  ██  ███████  ██████  ███████ + +*/ + +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Vm} from "forge-std/Vm.sol"; + +import {Secp256k1, PrivateKey, PublicKey} from "../curves/Secp256k1.sol"; + +uint constant SCHEME_ID = 1; + +struct StealthMetaAddress { + PublicKey spendingPubKey; + PublicKey viewingPubKey; +} + +struct StealthAddress { + address recipient; + PublicKey ephemeralPubKey; + uint8 viewTag; +} + +library StealthSecp256k1 { + // Stealth Meta Addresses + + // TODO: See https://eips.ethereum.org/EIPS/eip-5564#stealth-meta-address-format. + function toBytes(StealthAddress memory stealthMetaAddress) + internal + pure + returns (bytes memory) + { + return bytes(""); + } + + // Stealth Address + + // TODO: See https://eips.ethereum.org/EIPS/eip-5564#generation---generate-stealth-address-from-stealth-meta-address. + function newStealthAddress(StealthMetaAddress memory sma) + internal + returns (StealthAddress memory) + { + // TODO: Functionality missing in Secp256k1(Arithmetic): + // - [scalar]PublicKey + // - PublicKey + PublicKey + + // Create ephemeral key pair. + PrivateKey ephemeralPrivKey = Secp256k1.newPrivateKey(); + PublicKey memory ephemeralPubKey = ephemeralPrivKey.toPublicKey(); + + // Compute shared secret. + PublicKey memory sharedPubKey = sma.viewingPubKey.mul(ephemeralPrivKey); + // TODO: EIP not exact: sharedSecret must be bounded to field. + // TODO: If sharedSecret is zero, loop with new ephemeral key! + // Currently reverts. + PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( + uint(sharedPubKey.toHash()) % Secp256k1.Q + ); + + // Extract view tag from shared secret. + uint8 viewTag = uint8(sharedSecretPrivKey.asUint() >> 152); + + // Compute public key from shared secret private key. + PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); + + // Compute recipients public key. + PublicKey memory recipientPubKey = + sma.spendingPubKey.add(sharedSecretPubKey); + + // Derive recipients address from their public key. + address recipientAddr = recipientPubKey.toAddress(); + + return StealthAddress(recipientAddr, ephemeralPubKey, viewTag); + } + + /// @custom:invariant Shared secret private key is not zero. + /// ∀ (viewPrivKey, ephPubKey) ∊ (PrivateKey, PublicKey): + /// ([viewPrivKey]ephPubKey).toHash() != 0 (mod Q) + function checkStealthAddress( + PrivateKey viewingPrivKey, + PublicKey spendingPubKey, + StealthAddress memory stealthAddress + ) internal returns (bool) { + // Compute shared secret. + PublicKey memory sharedPubKey = + stealthAddress.ephemeralPubKey.mul(viewingPrivKey); + // TODO: EIP not exact: sharedSecret must be bounded to field. + PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( + uint(sharedPubKey.toHash()) % Secp256k1.Q + ); + + // Extract view tag from shared secret. + uint8 viewTag = uint8(sharedSecretPrivKey.asUint() >> 152); + + // Return early if view tags do not match. + if (viewTag != stealthAddress.viewTag) { + return false; + } + + // Compute public key from shared secret private key. + PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); + + // Compute recipients public key. + PublicKey memory recipientPubKey = + sma.spendingPubKey.add(sharedSecretPubKey); + + // Derive recipients address from their public key. + address recipientAddr = recipientPubKey.toAddress(); + + // Return true if stealth address' address matches computed recipients + // address. + return recipientAddr == stealthAddress.recipientAddr; + } + + // Private Key + + function computeStealthPrivateKey( + PrivateKey spendingPrivKey, + PrivateKey viewingPrivKey, + StealthAddress memory stealthAddress + ) internal returns (PrivateKey) { + // Compute shared secret. + PublicKey memory sharedPubKey = + stealthAddress.ephemeralPubKey.mul(viewingPrivKey); + // TODO: EIP not exact: sharedSecret must be bounded to field. + // TODO: If sharedSecret is zero, loop with new ephemeral key! + // Currently reverts. + PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( + uint(sharedPubKey.toHash()) % Secp256k1.Q + ); + + // Compute stealth private key. + PrivateKey stealthPrivKey = Secp256k1.privateKeyFromUint( + addmod( + spendingPrivKey.asUint(), + sharedSecretPrivKey.asUint(), + Secp256k1.Q + ) + ); + + return stealthPrivKey; + } +} From a306842120450fcf0cbb9ed98cc9d55574f6dfb9 Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 26 Nov 2023 17:00:39 +0100 Subject: [PATCH 02/11] hacking --- src/curves/Secp256k1Arithmetic.sol | 25 ++++++++++++++++++++++ src/stealth-addresses/StealthSecp256k1.sol | 6 +++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index bfe52cb..a0a5fa2 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -11,6 +11,8 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; +// TODO: Rename to Point? +// TODO: Represent point at infinity via zero point? /** * @notice Point is a secp256k1 point in affine coordinates * @@ -151,6 +153,29 @@ library Secp256k1Arithmetic { return point.y & 1; } + function addAffinePoint( + AffinePoint memory point, + AffinePoint memory other + ) internal pure returns (AffinePoint memory) { + AffinePoint memory sum; + + if (point.equals(other)) { + // _double + } else { + // _add + } + + return sum; + } + + function equals(AffinePoint memory point, AffinePoint memory other) + internal + pure + returns (bool) + { + return point.x == other.x && point.y == other.y; + } + //-------------------------------------------------------------------------- // Projective Point // diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 8bbe07c..6a7dfb9 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -56,7 +56,11 @@ library StealthSecp256k1 { PublicKey memory ephemeralPubKey = ephemeralPrivKey.toPublicKey(); // Compute shared secret. - PublicKey memory sharedPubKey = sma.viewingPubKey.mul(ephemeralPrivKey); + // forgefmt: disable-next-item + PublicKey memory sharedPubKey = sma.viewingPubKey + .intoAffinePoint() + .mul(ephemeralPrivKey); + // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. From fcc0d341acfc9f95768e66161dff38b31c0952ca Mon Sep 17 00:00:00 2001 From: pascal Date: Tue, 28 Nov 2023 09:15:18 +0100 Subject: [PATCH 03/11] Adds add and mul point functions --- src/curves/Secp256k1Arithmetic.sol | 163 +++++++++++++++++++-- src/stealth-addresses/StealthSecp256k1.sol | 58 ++++++-- 2 files changed, 193 insertions(+), 28 deletions(-) diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index a0a5fa2..047255d 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -153,33 +153,119 @@ library Secp256k1Arithmetic { return point.y & 1; } - function addAffinePoint( - AffinePoint memory point, - AffinePoint memory other - ) internal pure returns (AffinePoint memory) { - AffinePoint memory sum; + function equals(AffinePoint memory point, AffinePoint memory other) + internal + pure + returns (bool) + { + return point.x == other.x && point.y == other.y; + } + + //---------------------------------- + // Addition + + function addAffinePoint(AffinePoint memory point, AffinePoint memory other) + internal + pure + returns (AffinePoint memory) + { + JacobianPoint memory jSum; + JacobianPoint memory jPoint = point.toJacobianPoint(); if (point.equals(other)) { - // _double + jSum = jPoint.double(); } else { - // _add + jSum = jPoint.add(other.toJacobianPoint()); } - return sum; + return jSum.intoAffinePoint(); } - function equals(AffinePoint memory point, AffinePoint memory other) + //-------------------------------------------------------------------------- + // Projective Point + // + // Current functionality implemented from TODO: [ecmul] by Jordi. + + function mul(JacobianPoint memory jacPoint, uint scalar) internal pure - returns (bool) + returns (JacobianPoint memory) { - return point.x == other.x && point.y == other.y; + if (scalar == 0) { + return ZeroPoint().toJacobianPoint(); + } + + JacobianPoint memory copy = jacPoint; + JacobianPoint memory product = ZeroPoint().toJacobianPoint(); + + while (scalar != 0) { + if (scalar & 1 == 1) { + product = product.add(copy); + } + scalar /= 2; + copy = copy.double(); + } + + return product; } - //-------------------------------------------------------------------------- - // Projective Point - // - // Coming soon... + function add(JacobianPoint memory jacPoint, JacobianPoint memory other) + internal + pure + returns (JacobianPoint memory) + { + if ((jacPoint.x | jacPoint.y) == 0) { + return other; + } + if ((other.x | other.y) == 0) { + return jacPoint; + } + + JacobianPoint memory sum; + uint l; + uint lz; + uint da; + uint db; + + if (jacPoint.x == other.x && jacPoint.y == other.y) { + (l, lz) = _mul(jacPoint.x, jacPoint.z, jacPoint.x, jacPoint.z); + (l, lz) = _mul(l, lz, 3, 1); + (l, lz) = _add(l, lz, A, 1); + + (da, db) = _mul(jacPoint.y, jacPoint.z, 2, 1); + } else { + (l, lz) = _sub(other.y, other.z, jacPoint.y, jacPoint.z); + (da, db) = _sub(other.x, other.z, jacPoint.x, jacPoint.z); + } + + (l, lz) = _div(l, lz, da, db); + + (sum.x, da) = _mul(l, lz, l, lz); + (sum.x, da) = _sub(sum.x, da, jacPoint.x, jacPoint.z); + (sum.x, da) = _sub(sum.x, da, other.x, other.z); + + (sum.y, db) = _sub(jacPoint.x, jacPoint.z, sum.x, da); + (sum.y, db) = _mul(sum.y, db, l, lz); + (sum.y, db) = _sub(sum.y, db, jacPoint.y, jacPoint.z); + + if (da != db) { + sum.x = mulmod(sum.x, db, P); + sum.y = mulmod(sum.y, da, P); + sum.z = mulmod(da, db, P); + } else { + sum.z = da; + } + + return sum; + } + + function double(JacobianPoint memory jacPoint) + internal + pure + returns (JacobianPoint memory) + { + return jacPoint.add(jacPoint); + } //-------------------------------------------------------------------------- // (De)Serialization @@ -318,4 +404,51 @@ library Secp256k1Arithmetic { return mulmod(x, xInv, P) == 1; } + + //-------------------------------------------------------------------------- + // Private Functions + + function _add(uint x1, uint z1, uint x2, uint z2) + private + pure + returns (uint, uint) + { + uint x3 = addmod(mulmod(z2, x1, P), mulmod(x2, z1, P), P); + uint z3 = mulmod(z1, z2, P); + + return (x3, z3); + } + + function _sub(uint x1, uint z1, uint x2, uint z2) + private + pure + returns (uint, uint) + { + uint x3 = addmod(mulmod(z2, x1, P), mulmod(P - x2, z1, P), P); + uint z3 = mulmod(z1, z2, P); + + return (x3, z3); + } + + function _mul(uint x1, uint z1, uint x2, uint z2) + private + pure + returns (uint, uint) + { + uint x3 = mulmod(x1, x2, P); + uint z3 = mulmod(z1, z2, P); + + return (x3, z3); + } + + function _div(uint x1, uint z1, uint x2, uint z2) + private + pure + returns (uint, uint) + { + uint x3 = mulmod(x1, z2, P); + uint z3 = mulmod(z1, x2, P); + + return (x3, z3); + } } diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 6a7dfb9..adc8948 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -14,6 +14,11 @@ pragma solidity ^0.8.16; import {Vm} from "forge-std/Vm.sol"; import {Secp256k1, PrivateKey, PublicKey} from "../curves/Secp256k1.sol"; +import { + Secp256k1Arithmetic, + AffinePoint, + JacobianPoint +} from "../curves/Secp256k1Arithmetic.sol"; uint constant SCHEME_ID = 1; @@ -29,6 +34,12 @@ struct StealthAddress { } library StealthSecp256k1 { + using Secp256k1 for PrivateKey; + using Secp256k1 for PublicKey; + using Secp256k1 for AffinePoint; + using Secp256k1Arithmetic for AffinePoint; + using Secp256k1Arithmetic for JacobianPoint; + // Stealth Meta Addresses // TODO: See https://eips.ethereum.org/EIPS/eip-5564#stealth-meta-address-format. @@ -48,7 +59,6 @@ library StealthSecp256k1 { returns (StealthAddress memory) { // TODO: Functionality missing in Secp256k1(Arithmetic): - // - [scalar]PublicKey // - PublicKey + PublicKey // Create ephemeral key pair. @@ -58,8 +68,10 @@ library StealthSecp256k1 { // Compute shared secret. // forgefmt: disable-next-item PublicKey memory sharedPubKey = sma.viewingPubKey + .toJacobianPoint() + .mul(ephemeralPrivKey.asUint()) .intoAffinePoint() - .mul(ephemeralPrivKey); + .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! @@ -75,8 +87,13 @@ library StealthSecp256k1 { PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); // Compute recipients public key. - PublicKey memory recipientPubKey = - sma.spendingPubKey.add(sharedSecretPubKey); + // forgefmt: disable-next-item + PublicKey memory recipientPubKey = sma.spendingPubKey + .toJacobianPoint() + .add(sharedSecretPubKey + .toJacobianPoint()) + .intoAffinePoint() + .intoPublicKey(); // Derive recipients address from their public key. address recipientAddr = recipientPubKey.toAddress(); @@ -89,13 +106,18 @@ library StealthSecp256k1 { /// ([viewPrivKey]ephPubKey).toHash() != 0 (mod Q) function checkStealthAddress( PrivateKey viewingPrivKey, - PublicKey spendingPubKey, + PublicKey memory spendingPubKey, StealthAddress memory stealthAddress ) internal returns (bool) { // Compute shared secret. - PublicKey memory sharedPubKey = - stealthAddress.ephemeralPubKey.mul(viewingPrivKey); - // TODO: EIP not exact: sharedSecret must be bounded to field. + // forgefmt: disable-next-item + PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey + .toJacobianPoint() + .mul(viewingPrivKey.asUint()) + .intoAffinePoint() + .intoPublicKey(); + + // TODO: EIP not exact: sharedSecret must be bound to field. PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( uint(sharedPubKey.toHash()) % Secp256k1.Q ); @@ -112,15 +134,20 @@ library StealthSecp256k1 { PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); // Compute recipients public key. - PublicKey memory recipientPubKey = - sma.spendingPubKey.add(sharedSecretPubKey); + // forgefmt: disable-next-item + PublicKey memory recipientPubKey = spendingPubKey + .toJacobianPoint() + .add(sharedSecretPubKey + .toJacobianPoint()) + .intoAffinePoint() + .intoPublicKey(); // Derive recipients address from their public key. address recipientAddr = recipientPubKey.toAddress(); // Return true if stealth address' address matches computed recipients // address. - return recipientAddr == stealthAddress.recipientAddr; + return recipientAddr == stealthAddress.recipient; } // Private Key @@ -131,8 +158,13 @@ library StealthSecp256k1 { StealthAddress memory stealthAddress ) internal returns (PrivateKey) { // Compute shared secret. - PublicKey memory sharedPubKey = - stealthAddress.ephemeralPubKey.mul(viewingPrivKey); + // forgefmt: disable-next-item + PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey + .toJacobianPoint() + .mul(viewingPrivKey.asUint()) + .intoAffinePoint() + .intoPublicKey(); + // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. From f655e747e90894f98f6ce2b8510ce8a81fc60f81 Mon Sep 17 00:00:00 2001 From: pascal Date: Wed, 29 Nov 2023 22:01:22 +0100 Subject: [PATCH 04/11] getting there... --- src/curves/Secp256k1Arithmetic.sol | 79 ++++++++++++++++------ src/stealth-addresses/StealthSecp256k1.sol | 34 ++++------ 2 files changed, 71 insertions(+), 42 deletions(-) diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index 047255d..57a1f75 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -11,7 +11,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -// TODO: Rename to Point? // TODO: Represent point at infinity via zero point? /** * @notice Point is a secp256k1 point in affine coordinates @@ -27,8 +26,19 @@ struct Point { /** * @notice ProjectivePoint is a secp256k1 point in projective coordinates * +<<<<<<< HEAD * @dev A projective point represents an affine point (x, y) as (X, Y, Z) * satisfying the following equations: +||||||| parent of cc6390d (getting there...) + * @dev Jacobian point represents Affine point (x, y) as (X, Y, Z) satisfying + * the following equations: +======= + * @dev TODO Note to only use Jacobian Point if you know what you're doing. + * If not, use more user-friendly Point functions. + * + * @dev Jacobian point represents Affine point (x, y) as (X, Y, Z) satisfying + * the following equations: +>>>>>>> cc6390d (getting there...) * x = X / Z² * y = Y / Z³ */ @@ -153,7 +163,7 @@ library Secp256k1Arithmetic { return point.y & 1; } - function equals(AffinePoint memory point, AffinePoint memory other) + function equals(Point memory point, Point memory other) internal pure returns (bool) @@ -164,11 +174,22 @@ library Secp256k1Arithmetic { //---------------------------------- // Addition - function addAffinePoint(AffinePoint memory point, AffinePoint memory other) + /// @dev Returns new point being the sum of points `point` and `other`. + /// + /// @dev TODO Note about performance. intoPoint() conversion is expensive. + /// Also created new point struct in memory. + function add(Point memory point, Point memory other) internal pure - returns (AffinePoint memory) + returns (Point memory) { + if (!point.isOnCurve()) { + revert("PointNotOnCurve(point)"); + } + if (!other.isOnCurve()) { + revert("PointNotOnCurve(other)"); + } + JacobianPoint memory jSum; JacobianPoint memory jPoint = point.toJacobianPoint(); @@ -178,7 +199,23 @@ library Secp256k1Arithmetic { jSum = jPoint.add(other.toJacobianPoint()); } - return jSum.intoAffinePoint(); + return jSum.intoPoint(); + } + + function mul(Point memory point, uint scalar) + internal + pure + returns (Point memory) + { + if (!point.isOnCurve()) { + revert("PointNotOnCurve(point)"); + } + + if (point.isPointAtInfinity() || scalar == 0) { + return PointAtInfinity(); + } + + return point.toJacobianPoint().jMul(scalar).intoPoint(); } //-------------------------------------------------------------------------- @@ -186,7 +223,9 @@ library Secp256k1Arithmetic { // // Current functionality implemented from TODO: [ecmul] by Jordi. - function mul(JacobianPoint memory jacPoint, uint scalar) + // DANGER: Very dangerous. + + function jMul(JacobianPoint memory jPoint, uint scalar) internal pure returns (JacobianPoint memory) @@ -195,7 +234,7 @@ library Secp256k1Arithmetic { return ZeroPoint().toJacobianPoint(); } - JacobianPoint memory copy = jacPoint; + JacobianPoint memory copy = jPoint; JacobianPoint memory product = ZeroPoint().toJacobianPoint(); while (scalar != 0) { @@ -209,16 +248,16 @@ library Secp256k1Arithmetic { return product; } - function add(JacobianPoint memory jacPoint, JacobianPoint memory other) + function add(JacobianPoint memory jPoint, JacobianPoint memory other) internal pure returns (JacobianPoint memory) { - if ((jacPoint.x | jacPoint.y) == 0) { + if ((jPoint.x | jPoint.y) == 0) { return other; } if ((other.x | other.y) == 0) { - return jacPoint; + return jPoint; } JacobianPoint memory sum; @@ -227,26 +266,26 @@ library Secp256k1Arithmetic { uint da; uint db; - if (jacPoint.x == other.x && jacPoint.y == other.y) { - (l, lz) = _mul(jacPoint.x, jacPoint.z, jacPoint.x, jacPoint.z); + if (jPoint.x == other.x && jPoint.y == other.y) { + (l, lz) = _mul(jPoint.x, jPoint.z, jPoint.x, jPoint.z); (l, lz) = _mul(l, lz, 3, 1); (l, lz) = _add(l, lz, A, 1); - (da, db) = _mul(jacPoint.y, jacPoint.z, 2, 1); + (da, db) = _mul(jPoint.y, jPoint.z, 2, 1); } else { - (l, lz) = _sub(other.y, other.z, jacPoint.y, jacPoint.z); - (da, db) = _sub(other.x, other.z, jacPoint.x, jacPoint.z); + (l, lz) = _sub(other.y, other.z, jPoint.y, jPoint.z); + (da, db) = _sub(other.x, other.z, jPoint.x, jPoint.z); } (l, lz) = _div(l, lz, da, db); (sum.x, da) = _mul(l, lz, l, lz); - (sum.x, da) = _sub(sum.x, da, jacPoint.x, jacPoint.z); + (sum.x, da) = _sub(sum.x, da, jPoint.x, jPoint.z); (sum.x, da) = _sub(sum.x, da, other.x, other.z); - (sum.y, db) = _sub(jacPoint.x, jacPoint.z, sum.x, da); + (sum.y, db) = _sub(jPoint.x, jPoint.z, sum.x, da); (sum.y, db) = _mul(sum.y, db, l, lz); - (sum.y, db) = _sub(sum.y, db, jacPoint.y, jacPoint.z); + (sum.y, db) = _sub(sum.y, db, jPoint.y, jPoint.z); if (da != db) { sum.x = mulmod(sum.x, db, P); @@ -259,12 +298,12 @@ library Secp256k1Arithmetic { return sum; } - function double(JacobianPoint memory jacPoint) + function double(JacobianPoint memory jPoint) internal pure returns (JacobianPoint memory) { - return jacPoint.add(jacPoint); + return jPoint.add(jPoint); } //-------------------------------------------------------------------------- diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index adc8948..0eb3e86 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -14,11 +14,7 @@ pragma solidity ^0.8.16; import {Vm} from "forge-std/Vm.sol"; import {Secp256k1, PrivateKey, PublicKey} from "../curves/Secp256k1.sol"; -import { - Secp256k1Arithmetic, - AffinePoint, - JacobianPoint -} from "../curves/Secp256k1Arithmetic.sol"; +import {Secp256k1Arithmetic, Point} from "../curves/Secp256k1Arithmetic.sol"; uint constant SCHEME_ID = 1; @@ -36,9 +32,8 @@ struct StealthAddress { library StealthSecp256k1 { using Secp256k1 for PrivateKey; using Secp256k1 for PublicKey; - using Secp256k1 for AffinePoint; - using Secp256k1Arithmetic for AffinePoint; - using Secp256k1Arithmetic for JacobianPoint; + using Secp256k1 for Point; + using Secp256k1Arithmetic for Point; // Stealth Meta Addresses @@ -68,9 +63,8 @@ library StealthSecp256k1 { // Compute shared secret. // forgefmt: disable-next-item PublicKey memory sharedPubKey = sma.viewingPubKey - .toJacobianPoint() + .intoPoint() .mul(ephemeralPrivKey.asUint()) - .intoAffinePoint() .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bounded to field. @@ -89,10 +83,9 @@ library StealthSecp256k1 { // Compute recipients public key. // forgefmt: disable-next-item PublicKey memory recipientPubKey = sma.spendingPubKey - .toJacobianPoint() + .intoPoint() .add(sharedSecretPubKey - .toJacobianPoint()) - .intoAffinePoint() + .intoPoint()) .intoPublicKey(); // Derive recipients address from their public key. @@ -112,9 +105,8 @@ library StealthSecp256k1 { // Compute shared secret. // forgefmt: disable-next-item PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey - .toJacobianPoint() + .intoPoint() .mul(viewingPrivKey.asUint()) - .intoAffinePoint() .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bound to field. @@ -136,11 +128,10 @@ library StealthSecp256k1 { // Compute recipients public key. // forgefmt: disable-next-item PublicKey memory recipientPubKey = spendingPubKey - .toJacobianPoint() - .add(sharedSecretPubKey - .toJacobianPoint()) - .intoAffinePoint() - .intoPublicKey(); + .intoPoint() + .add(sharedSecretPubKey + .intoPoint()) + .intoPublicKey(); // Derive recipients address from their public key. address recipientAddr = recipientPubKey.toAddress(); @@ -160,9 +151,8 @@ library StealthSecp256k1 { // Compute shared secret. // forgefmt: disable-next-item PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey - .toJacobianPoint() + .intoPoint() .mul(viewingPrivKey.asUint()) - .intoAffinePoint() .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bounded to field. From 479ea5cc585095aadda77eca85cdf466dd258061 Mon Sep 17 00:00:00 2001 From: pascal Date: Sat, 2 Dec 2023 21:21:48 +0100 Subject: [PATCH 05/11] Finishing up arithmetic - still untested --- src/curves/Secp256k1Arithmetic.sol | 393 ++++++++++++++++-- .../secp256k1/Secp256k1Arithmetic.t.sol | 71 +++- 2 files changed, 420 insertions(+), 44 deletions(-) diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index 57a1f75..b170852 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -116,9 +116,20 @@ library Secp256k1Arithmetic { /// /// @dev Note that the identity is represented via: /// point.x = point.y = type(uint).max +<<<<<<< HEAD /// /// @dev Note that the identity is also called point at infinity. function Identity() internal pure returns (Point memory) { +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + function PointAtInfinity() internal pure returns (Point memory) { +======= + /// + /// @dev Note that the point at infinity serves as identity element with + /// following rules: + /// ∀ p ∊ Point: p.isOnCurve() → (p + PointAtInfinity() = p) + /// ∀ x ∊ Uint: [s]PointAtInfinity() = PointAtInfinity() + function PointAtInfinity() internal pure returns (Point memory) { +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) return Point(type(uint).max, type(uint).max); } @@ -138,15 +149,28 @@ library Secp256k1Arithmetic { /// where: /// a = 0 /// b = 7 +<<<<<<< HEAD /// /// @dev Note that the identity is also on the curve. +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) +======= + /// + /// @dev Note that the point at infinity is also on the curve. +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) function isOnCurve(Point memory point) internal pure returns (bool) { +<<<<<<< HEAD if (point.isIdentity()) { +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + // TODO: Point at infinity on curve? + if (point.isPointAtInfinity()) { +======= + if (point.isPointAtInfinity()) { +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) return true; } uint left = mulmod(point.y, point.y, P); - // Note that adding a * x can be waived as ∀x: a * x = 0. + // Note that adding a * x can be waived as ∀ x: a * x = 0. uint right = addmod(mulmod(point.x, mulmod(point.x, point.x, P), P), B, P); @@ -163,6 +187,7 @@ library Secp256k1Arithmetic { return point.y & 1; } + /// @dev Returns whether point `point` equals point `other`. function equals(Point memory point, Point memory other) internal pure @@ -172,17 +197,21 @@ library Secp256k1Arithmetic { } //---------------------------------- - // Addition + // Arithmetic - /// @dev Returns new point being the sum of points `point` and `other`. + /// @dev Returns a new point being the sum of points `point` and `other`. /// /// @dev TODO Note about performance. intoPoint() conversion is expensive. /// Also created new point struct in memory. + /// + /// @dev Reverts if: + /// - Point not on curve function add(Point memory point, Point memory other) internal pure returns (Point memory) { + // Revert if any point not on curve. if (!point.isOnCurve()) { revert("PointNotOnCurve(point)"); } @@ -190,18 +219,34 @@ library Secp256k1Arithmetic { revert("PointNotOnCurve(other)"); } + // Catch addition with identity, ie point at infinity. + if (point.isPointAtInfinity()) { + return other; + } + if (other.isPointAtInfinity()) { + return point; + } + JacobianPoint memory jSum; JacobianPoint memory jPoint = point.toJacobianPoint(); if (point.equals(other)) { - jSum = jPoint.double(); + jSum = _jDouble(jPoint); } else { - jSum = jPoint.add(other.toJacobianPoint()); + jSum = _jAdd(jPoint, other.toJacobianPoint()); } return jSum.intoPoint(); } + /// @dev Returns a new point being the product of point `point` and scalar + /// `scalar`. + /// + /// @dev TODO Note about performance. intoPoint() conversion is expensive. + /// Also created new point struct in memory. + /// + /// @dev Reverts if: + /// - Point not on curve function mul(Point memory point, uint scalar) internal pure @@ -215,42 +260,178 @@ library Secp256k1Arithmetic { return PointAtInfinity(); } - return point.toJacobianPoint().jMul(scalar).intoPoint(); + JacobianPoint memory jProduct; + JacobianPoint memory jPoint = point.toJacobianPoint(); + + jProduct = _jMul(jPoint, scalar); + + return jProduct.intoPoint(); + } + + //---------------------------------- + // Type Conversion + + /// @dev Returns point `point` as Jacobian point. + function toJacobianPoint(Point memory point) + internal + pure + returns (JacobianPoint memory) + { + return JacobianPoint(point.x, point.y, 1); } //-------------------------------------------------------------------------- // Projective Point // - // Current functionality implemented from TODO: [ecmul] by Jordi. + // TODO: Jacobian arithmetic is private for now as they provide less + // security guarantees. + // If you need Jacobian functionality exposed, eg for performance + // gains, let me know! - // DANGER: Very dangerous. + //---------------------------------- + // Type Conversion - function jMul(JacobianPoint memory jPoint, uint scalar) + /// @dev Mutates Jacobian point `jPoint` to Affine point. + function intoPoint(JacobianPoint memory jPoint) internal pure - returns (JacobianPoint memory) + returns (Point memory) { - if (scalar == 0) { - return ZeroPoint().toJacobianPoint(); + // Compute z⁻¹, i.e. the modular inverse of jPoint.z. + uint zInv = modularInverseOf(jPoint.z); + + // Compute (z⁻¹)² (mod P) + uint zInv_2 = mulmod(zInv, zInv, P); + + // Compute jPoint.x * (z⁻¹)² (mod P), i.e. the x coordinate of given + // Jacobian point in Affine representation. + uint x = mulmod(jPoint.x, zInv_2, P); + + // Compute jPoint.y * (z⁻¹)³ (mod P), i.e. the y coordinate of given + // Jacobian point in Affine representation. + uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); + + // Store x and y in jPoint. + assembly ("memory-safe") { + mstore(jPoint, x) + mstore(add(jPoint, 0x20), y) } - JacobianPoint memory copy = jPoint; - JacobianPoint memory product = ZeroPoint().toJacobianPoint(); + // Return as Point(jPoint.x, jPoint.y). + // Note that jPoint.z is from now on dirty memory! + Point memory point; + assembly ("memory-safe") { + point := jPoint + } + return point; + } - while (scalar != 0) { - if (scalar & 1 == 1) { - product = product.add(copy); + //-------------------------------------------------------------------------- + // Utils + + /// @dev Returns the modular inverse of `x` for modulo `P`. + /// + /// The modular inverse of `x` is x⁻¹ such that x * x⁻¹ ≡ 1 (mod P). + /// + /// @dev Reverts if: + /// - x not in [1, P) + /// + /// @dev Uses the Extended Euclidean Algorithm. + /// + /// @custom:invariant Terminates in finite time. + function modularInverseOf(uint x) internal pure returns (uint) { + // TODO: Refactor to use Fermats Little Theorem. + // While generally less performant due to the modexp precompile + // pricing its less cheaper in EVM context. + // For more info, see page 4 in "Speeding up Elliptic Curve Computations for Ethereum Account Abstraction". + + if (x == 0) { + revert("Modular inverse of zero does not exist"); + } + if (x >= P) { + revert("NotAFieldElement(x)"); + } + + uint t; + uint q; + uint newT = 1; + uint r = P; + + assembly ("memory-safe") { + // Implemented in assembly to circumvent division-by-zero + // and over-/underflow protection. + // + // Functionally equivalent Solidity code: + // while (x != 0) { + // q = r / x; + // (t, newT) = (newT, addmod(t, (P - mulmod(q, newT, P)), P)); + // (r, x) = (x, r - (q * x)); + // } + // + // For the division r / x, x is guaranteed to not be zero via the + // loop condition. + // + // The subtraction of form P - mulmod(_, _, P) is guaranteed to not + // underflow due to the subtrahend being a (mod P) result, + // i.e. the subtrahend being guaranteed to be less than P. + // + // The subterm q * x is guaranteed to not overflow because + // q * x ≤ r due to q = ⎣r / x⎦. + // + // The term r - (q * x) is guaranteed to not underflow because + // q * x ≤ r and therefore r - (q * x) ≥ 0. + for {} x {} { + q := div(r, x) + + let tmp := t + t := newT + newT := addmod(tmp, sub(P, mulmod(q, newT, P)), P) + + tmp := r + r := x + x := sub(tmp, mul(q, x)) } - scalar /= 2; - copy = copy.double(); } - return product; + return t; } - function add(JacobianPoint memory jPoint, JacobianPoint memory other) + /// @dev Returns whether `xInv` is the modular inverse of `x`. + /// + /// @dev Note that there is no modular inverse for zero. + /// + /// @dev Reverts if: + /// - x not in [1, P) + /// - xInv not in [1, P) + function areModularInverse(uint x, uint xInv) internal pure + returns (bool) + { + if (x >= P) { + revert("NotAFieldElement(x)"); + } + if (xInv >= P) { + revert("NotAFieldElement(xInv)"); + } + + return mulmod(x, xInv, P) == 1; + } + + //-------------------------------------------------------------------------- + // Private Functions + + //---------------------------------- + // Affine Point + // + // Functionality stolen from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). + + /// @dev Assumptions: + /// - Each point is either on the curve or the zero point + /// - No point is the point at infinity + function _jAdd(JacobianPoint memory jPoint, JacobianPoint memory other) + private + pure returns (JacobianPoint memory) { if ((jPoint.x | jPoint.y) == 0) { @@ -298,14 +479,19 @@ library Secp256k1Arithmetic { return sum; } - function double(JacobianPoint memory jPoint) + /// @dev Assumptions: + /// - Point is on the curve + /// - Point is not the point at infinity + function _jDouble(JacobianPoint memory jPoint) internal pure returns (JacobianPoint memory) { - return jPoint.add(jPoint); + // TODO: There are faster doubling formulas. + return _jAdd(jPoint, jPoint); } +<<<<<<< HEAD //-------------------------------------------------------------------------- // (De)Serialization @@ -315,12 +501,39 @@ library Secp256k1Arithmetic { /// @dev Returns point `point` as projective point. function toProjectivePoint(Point memory point) internal +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + //-------------------------------------------------------------------------- + // (De)Serialization + + //---------------------------------- + // Point + + /// @dev Returns point `point` as Jacobian point. + function toJacobianPoint(Point memory point) + internal +======= + /// @dev Assumptions: + /// - Point is on the curve + /// - Point is not the point at infinity + /// - Scalar is not zero + function _jMul(JacobianPoint memory jPoint, uint scalar) + private +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) pure returns (ProjectivePoint memory) { +<<<<<<< HEAD return ProjectivePoint(point.x, point.y, 1); } +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + return JacobianPoint(point.x, point.y, 1); + } +======= + JacobianPoint memory copy = jPoint; + JacobianPoint memory product = ZeroPoint().toJacobianPoint(); +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) +<<<<<<< HEAD //---------------------------------- // Projective Point @@ -422,12 +635,122 @@ library Secp256k1Arithmetic { tmp := r r := x x := sub(tmp, mul(q, x)) +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + //---------------------------------- + // Jacobian Point + + /// @dev Mutates Jacobian point `jPoint` to Affine point. + function intoPoint(JacobianPoint memory jPoint) + internal + pure + returns (Point memory) + { + // Compute z⁻¹, i.e. the modular inverse of jPoint.z. + uint zInv = modularInverseOf(jPoint.z); + + // Compute (z⁻¹)² (mod P) + uint zInv_2 = mulmod(zInv, zInv, P); + + // Compute jPoint.x * (z⁻¹)² (mod P), i.e. the x coordinate of given + // Jacobian point in Affine representation. + uint x = mulmod(jPoint.x, zInv_2, P); + + // Compute jPoint.y * (z⁻¹)³ (mod P), i.e. the y coordinate of given + // Jacobian point in Affine representation. + uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); + + // Store x and y in jPoint. + assembly ("memory-safe") { + mstore(jPoint, x) + mstore(add(jPoint, 0x20), y) + } + + // Return as Point(jPoint.x, jPoint.y). + // Note that jPoint.z is from now on dirty memory! + Point memory point; + assembly ("memory-safe") { + point := jPoint + } + return point; + } + + //-------------------------------------------------------------------------- + // Utils + + // @todo Use Fermats Little Theorem. While generally less performant, it is + // cheaper on EVM due to the modexp precompile. + // See "Speeding up Elliptic Curve Computations for Ethereum Account Abstraction" page 4. + + /// @dev Returns the modular inverse of `x` for modulo `P`. + /// + /// The modular inverse of `x` is x⁻¹ such that x * x⁻¹ ≡ 1 (mod P). + /// + /// @dev Reverts if: + /// - x not in [1, P) + /// + /// @dev Uses the Extended Euclidean Algorithm. + /// + /// @custom:invariant Terminates in finite time. + function modularInverseOf(uint x) internal pure returns (uint) { + if (x == 0) { + revert("Modular inverse of zero does not exist"); + } + if (x >= P) { + revert("TODO(modularInverse: x >= P)"); + } + + uint t; + uint q; + uint newT = 1; + uint r = P; + + assembly ("memory-safe") { + // Implemented in assembly to circumvent division-by-zero + // and over-/underflow protection. + // + // Functionally equivalent Solidity code: + // while (x != 0) { + // q = r / x; + // (t, newT) = (newT, addmod(t, (P - mulmod(q, newT, P)), P)); + // (r, x) = (x, r - (q * x)); + // } + // + // For the division r / x, x is guaranteed to not be zero via the + // loop condition. + // + // The subtraction of form P - mulmod(_, _, P) is guaranteed to not + // underflow due to the subtrahend being a (mod P) result, + // i.e. the subtrahend being guaranteed to be less than P. + // + // The subterm q * x is guaranteed to not overflow because + // q * x ≤ r due to q = ⎣r / x⎦. + // + // The term r - (q * x) is guaranteed to not underflow because + // q * x ≤ r and therefore r - (q * x) ≥ 0. + for {} x {} { + q := div(r, x) + + let tmp := t + t := newT + newT := addmod(tmp, sub(P, mulmod(q, newT, P)), P) + + tmp := r + r := x + x := sub(tmp, mul(q, x)) +======= + while (scalar != 0) { + if (scalar & 1 == 1) { + product = _jAdd(product, copy); +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) } + scalar /= 2; + copy = _jDouble(copy); } - return t; + return product; } +<<<<<<< HEAD /// @dev Returns whether `xInv` is the modular inverse of `x`. function areModularInverse(uint x, uint xInv) internal @@ -446,6 +769,28 @@ library Secp256k1Arithmetic { //-------------------------------------------------------------------------- // Private Functions +||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) + function areModularInverse(uint x, uint xInv) + internal + pure + returns (bool) + { + if (x == 0 || xInv == 0) { + revert("Modular inverse of zero does not exist"); + } + if (x >= P || xInv >= P) { + revert("TODO(modularInverse: x >= P)"); + } + + return mulmod(x, xInv, P) == 1; + } + + //-------------------------------------------------------------------------- + // Private Functions +======= + //---------------------------------- + // Helpers +>>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) function _add(uint x1, uint z1, uint x2, uint z2) private diff --git a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol index a7194e3..d8903ba 100644 --- a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol @@ -78,6 +78,10 @@ contract Secp256k1ArithmeticTest is Test { assertTrue(wrapper.isOnCurve(point)); } + function test_Point_isOnCurve_PointAtInfinity() public { + assertTrue(wrapper.isOnCurve(Secp256k1Arithmetic.PointAtInfinity())); + } + // -- yParity function testFuzz_Point_yParity(uint x, uint y) public { @@ -88,6 +92,43 @@ contract Secp256k1ArithmeticTest is Test { assertEq(want, got); } + // -- equals + + function testFuzz_Point_equals(PrivateKey privKey) public { + vm.assume(privKey.isValid()); + + Point memory point = privKey.toPublicKey().intoPoint(); + + assertTrue(wrapper.equals(point, point)); + } + + function testFuzz_Point_equals_FailsIfPointsDoNotEqual( + PrivateKey privKey1, + PrivateKey privKey2 + ) public { + vm.assume(privKey1.asUint() != privKey2.asUint()); + vm.assume(privKey1.isValid()); + vm.assume(privKey2.isValid()); + + Point memory point1 = privKey1.toPublicKey().intoPoint(); + Point memory point2 = privKey2.toPublicKey().intoPoint(); + + assertFalse(wrapper.equals(point1, point2)); + } + + function test_Point_equals_DoesNotRevert_IfPointsNotOnCurve( + Point memory point1, + Point memory point2 + ) public { + wrapper.equals(point1, point2); + } + + //---------------------------------- + // TODO: Test: Arithmetic + + //---------------------------------- + // Test: Type Conversion + // -- toProjectivePoint function testFuzz_Point_toProjectivePoint(SecretKey sk) public { @@ -103,6 +144,9 @@ contract Secp256k1ArithmeticTest is Test { //-------------------------------------------------------------------------- // Test: Projective Point + //---------------------------------- + // Test: Type Conversion + // TODO: Test no new memory allocation. // TODO: Not a real test. Use vectors from Paul Miller. function testFuzz_ProjectivePoint_intoPoint(SecretKey sk) public { @@ -141,8 +185,7 @@ contract Secp256k1ArithmeticTest is Test { { vm.assume(x >= Secp256k1Arithmetic.P); - // TODO: Test for proper error message. - vm.expectRevert(); + vm.expectRevert("NotAFieldElement(x)"); wrapper.modularInverseOf(x); } @@ -163,9 +206,7 @@ contract Secp256k1ArithmeticTest is Test { uint x, uint xInv ) public { - vm.assume(x != 0); vm.assume(x < Secp256k1Arithmetic.P); - vm.assume(xInv != 0); vm.assume(xInv < Secp256k1Arithmetic.P); vm.assume(mulmod(x, xInv, Secp256k1Arithmetic.P) != 1); @@ -173,25 +214,12 @@ contract Secp256k1ArithmeticTest is Test { assertFalse(wrapper.areModularInverse(x, xInv)); } - function test_areModularInverse_RevertsIf_XIsZero() public { - // TODO: Test for proper error message. - vm.expectRevert(); - wrapper.areModularInverse(0, 1); - } - - function test_areModularInverse_RevertsIf_XInvIsZero() public { - // TODO: Test for proper error message. - vm.expectRevert(); - wrapper.areModularInverse(1, 0); - } - function testFuzz_areModularInverse_RevertsIf_XEqualToOrBiggerThanP(uint x) public { vm.assume(x >= Secp256k1Arithmetic.P); - // TODO: Test for proper error message. - vm.expectRevert(); + vm.expectRevert("NotAFieldElement(x)"); wrapper.areModularInverse(x, 1); } @@ -200,8 +228,7 @@ contract Secp256k1ArithmeticTest is Test { ) public { vm.assume(xInv >= Secp256k1Arithmetic.P); - // TODO: Test for proper error message. - vm.expectRevert(); + vm.expectRevert("NotAFieldElement(xInv)"); wrapper.areModularInverse(1, xInv); } } @@ -249,6 +276,10 @@ contract Secp256k1ArithmeticWrapper { return point.yParity(); } + function equals(Point memory point, Point memory other) public pure returns (bool) { + return point.equals(other); + } + //-------------------------------------------------------------------------- // (De)Serialization From bc0193113d09d91ddc27f28f4d0807d9e85a0952 Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 3 Dec 2023 00:52:47 +0100 Subject: [PATCH 06/11] Adds announcer stuff --- .../stealth-addresses/StealthSecp256k1.sol | 105 ++++++++++++++++++ foundry.toml | 2 +- src/stealth-addresses/ERC5564Announcer.sol | 94 ++++++++++++++++ src/stealth-addresses/StealthSecp256k1.sol | 14 +++ .../secp256k1/Secp256k1Arithmetic.t.sol | 6 +- 5 files changed, 219 insertions(+), 2 deletions(-) create mode 100644 examples/stealth-addresses/StealthSecp256k1.sol create mode 100644 src/stealth-addresses/ERC5564Announcer.sol diff --git a/examples/stealth-addresses/StealthSecp256k1.sol b/examples/stealth-addresses/StealthSecp256k1.sol new file mode 100644 index 0000000..61c511d --- /dev/null +++ b/examples/stealth-addresses/StealthSecp256k1.sol @@ -0,0 +1,105 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Script} from "forge-std/Script.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import { + StealthSecp256k1, + StealthAddress, + StealthMetaAddress, + SCHEME_ID +} from "src/stealth-addresses/StealthSecp256k1.sol"; +import { + IERC5564Announcer, + ERC5564Announcer +} from "src/stealth-addresses/ERC5564Announcer.sol"; + +import {Secp256k1, PrivateKey, PublicKey} from "src/curves/Secp256k1.sol"; + +contract StealthSecp256k1Example is Script { + using StealthSecp256k1 for PrivateKey; + using StealthSecp256k1 for StealthAddress; + using StealthSecp256k1 for StealthMetaAddress; + + using Secp256k1 for PrivateKey; + using Secp256k1 for PublicKey; + + function run() public { + // Sender key pair. + console.log("Sender: Creates key pair"); + PrivateKey senderPrivKey = Secp256k1.newPrivateKey(); + PublicKey memory senderPubKey = senderPrivKey.toPublicKey(); + + // Receiver key pairs consist of spending and viewing key pairs. + console.log("Receiver: Creates key pairs"); + PrivateKey receiverSpendingPrivKey = Secp256k1.newPrivateKey(); + PublicKey memory receiverSpendingPubKey = + receiverSpendingPrivKey.toPublicKey(); + PrivateKey receiverViewPrivKey = Secp256k1.newPrivateKey(); + PublicKey memory receiverViewPubKey = receiverViewPrivKey.toPublicKey(); + + // There exists an ERC5564Announcer instance. + IERC5564Announcer announcer = IERC5564Announcer(new ERC5564Announcer()); + + // Receiver creates their stealth meta address. + // TODO: Note that these addresses need to be published somehow. + console.log("Receiver: Creates stealth meta address"); + StealthMetaAddress memory receiverStealthMeta; + receiverStealthMeta = StealthMetaAddress({ + spendingPubKey: receiverSpendingPubKey, + viewingPubKey: receiverViewPubKey + }); + + // Sender creates stealth address from receiver's stealth meta address. + console.log( + "Sender: Creates stealth address based on receiver's meta address" + ); + StealthAddress memory stealth = receiverStealthMeta.newStealthAddress(); + + // Sender sends ETH to stealth. + console.log("Sender: Sends ETH to receiver's stealth address"); + vm.deal(senderPubKey.toAddress(), 1 ether); + vm.prank(senderPubKey.toAddress()); + (bool ok, ) = stealth.recipient.call{value: 1 ether}(""); + require(ok, "Sender: ETH transfer failed"); + + // Sender announces tx via ERC5564Announcer. + console.log("Sender: Announces tx via ERC5564Announcer"); + vm.prank(senderPubKey.toAddress()); + announcer.announce({ + schemeId: SCHEME_ID, + stealthAddress: stealth.recipient, + ephemeralPubKey: stealth.ephemeralPubKey.toBytes(), + // See ERC5564Announcer.sol for more info. + metadata: abi.encodePacked( + stealth.viewTag, + hex"eeeeeeee", + hex"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + uint(1 ether) + ) + }); + + // Receiver checks announces stealth address. + console.log("Receiver: Verifies tx is based on own meta address"); + require( + receiverViewPrivKey.checkStealthAddress( + receiverSpendingPubKey, stealth + ), + "Check failed" + ); + + // Receiver computed stealth's private key. + console.log("Receiver: Computes private key for stealth address"); + PrivateKey stealthPrivKey = receiverSpendingPrivKey + .computeStealthPrivateKey(receiverViewPrivKey, stealth); + + // Verify computed private key's address matches stealth's recipient + // address. + console.log("Receiver: Verifies access on stealth address"); + require( + stealthPrivKey.toPublicKey().toAddress() == stealth.recipient, + "Private key computation failed" + ); + } +} diff --git a/foundry.toml b/foundry.toml index e58136b..405d846 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,7 +1,7 @@ [profile.default] src = 'src' out = 'out' -libs = ['lib', 'spec', 'examples', 'unsafe'] +libs = ['lib', 'examples', 'unsafe'] ffi = true # Compilation diff --git a/src/stealth-addresses/ERC5564Announcer.sol b/src/stealth-addresses/ERC5564Announcer.sol new file mode 100644 index 0000000..3a47e29 --- /dev/null +++ b/src/stealth-addresses/ERC5564Announcer.sol @@ -0,0 +1,94 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +/** + * @title IERC5561Announcer + * + * @notice Interface to announce a tx to an [EIP-5564] stealth address + * + * @dev Metadata Specification and Recommendations + * + * The first byte of the metadata MUST be the view tag. The view tag provides + * a probabilistic filter to skip computations when checking announcements. + * + * The following recommendations are given in [EIP-5564]: + * + * - Tx transferring the native token, eg ETH: + * + * Index | Description | Length in bytes + * ----------------------------------------------------------------------------- + * [0x00] | View tag | 1 + * [0x01:0x04] | `0xeeeeeeee` | 4 + * [0x05:0x24] | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | 20 + * [0x18:0x38] | Amount in wei | 32 + * + * - Tx involving a contract call with a single argument, eg ERC-20/ERC-721 + * transfers: + * + * Index | Description | Length in bytes + * ----------------------------------------------------------------------------- + * [0x00] | View tag | 1 + * [0x01:0x04] | Solidity function selector | 4 + * [0x05:0x24] | Contract address | 20 + * [0x18:0x38] | One word argument, eg token amount | 32 + * + * @custom:references + * - [EIP-5564]: https://eips.ethereum.org/EIPS/eip-5564 + * - [EIP-5564 Scheme Registry]: https://eips.ethereum.org/assets/eip-5564/scheme_ids + */ +interface IERC5564Announcer { + /// @notice Emitted to announce a tx to a stealth address. + /// + /// @param schemeId Scheme id based on [EIP-5564 Scheme Registry] registry. + /// @param stealthAddress The stealth address. + /// @param caller The address who announced the tx. + /// @param ephemeralPubKey The ephemeral public key created during the + /// stealth address generation. + /// @param metadata Bytes blob providing the view tag and arbitrary + /// additional metadata. Note that [EIP-5564] provides + /// recommendations. + event Announcement( + uint indexed schemeId, + address indexed stealthAddress, + address indexed caller, + bytes ephemeralPubKey, + bytes metadata + ); + + /// @notice Announces a tx to stealth address `stealthAddress` using scheme + /// `schemeId` and ephemeral public key `ephemeralPubKey`. View tag + /// and additional metadata are provided via `metadata`. + /// + /// @param schemeId Scheme id based on [EIP-5564 Scheme Registry] registry. + /// @param stealthAddress The stealth address. + /// @param ephemeralPubKey The ephemeral public key created during the + /// stealth address generation. + /// @param metadata Bytes blob providing the view tag and arbitrary + /// additional metadata. Note that [EIP-5564] provides + /// recommendations. + function announce( + uint schemeId, + address stealthAddress, + bytes memory ephemeralPubKey, + bytes memory metadata + ) external; +} + +/** + * @title ERC5564Announcer + * + * @notice Minimal [EIP-5564] stealth address announcement contract + */ +contract ERC5564Announcer is IERC5564Announcer { + /// @inheritdoc IERC5564Announcer + function announce( + uint schemeId, + address stealthAddress, + bytes memory ephemeralPubKey, + bytes memory metadata + ) external { + emit Announcement( + schemeId, stealthAddress, msg.sender, ephemeralPubKey, metadata + ); + } +} diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 0eb3e86..050f780 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -29,6 +29,20 @@ struct StealthAddress { uint8 viewTag; } +// TODO: Differentiate between EIPs and ERCs. + +/** + * @title StealthSecp256k1 + * + * @notice Stealth Addresses for secp256k1 following [EIP-5564] + * + * @dev + * + * @custom:references + * - [EIP-5564]: https://eips.ethereum.org/EIPS/eip-5564 + * + * @author crysol (https://github.com/pmerkleplant/crysol) + */ library StealthSecp256k1 { using Secp256k1 for PrivateKey; using Secp256k1 for PublicKey; diff --git a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol index d8903ba..2cc511c 100644 --- a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol @@ -276,7 +276,11 @@ contract Secp256k1ArithmeticWrapper { return point.yParity(); } - function equals(Point memory point, Point memory other) public pure returns (bool) { + function equals(Point memory point, Point memory other) + public + pure + returns (bool) + { return point.equals(other); } From 2199e09c34d0657e4577d7cd4934b1b2ec08688f Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 3 Dec 2023 01:18:31 +0100 Subject: [PATCH 07/11] some cleaning --- .../stealth-addresses/StealthSecp256k1.sol | 2 ++ src/curves/Secp256k1Arithmetic.sol | 5 ++-- src/stealth-addresses/ERC5564Announcer.sol | 18 +++++++------- src/stealth-addresses/StealthSecp256k1.sol | 24 ++++++++++++------- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/examples/stealth-addresses/StealthSecp256k1.sol b/examples/stealth-addresses/StealthSecp256k1.sol index 61c511d..8eff5de 100644 --- a/examples/stealth-addresses/StealthSecp256k1.sol +++ b/examples/stealth-addresses/StealthSecp256k1.sol @@ -50,6 +50,8 @@ contract StealthSecp256k1Example is Script { spendingPubKey: receiverSpendingPubKey, viewingPubKey: receiverViewPubKey }); + console.log("Stealth Meta Address: "); + console.logBytes(receiverStealthMeta.toBytes("eth")); // Sender creates stealth address from receiver's stealth meta address. console.log( diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index b170852..e71629c 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -11,7 +11,6 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; -// TODO: Represent point at infinity via zero point? /** * @notice Point is a secp256k1 point in affine coordinates * @@ -199,7 +198,7 @@ library Secp256k1Arithmetic { //---------------------------------- // Arithmetic - /// @dev Returns a new point being the sum of points `point` and `other`. + /// @dev Returns the sum of points `point` and `other` as new point. /// /// @dev TODO Note about performance. intoPoint() conversion is expensive. /// Also created new point struct in memory. @@ -422,7 +421,7 @@ library Secp256k1Arithmetic { // Private Functions //---------------------------------- - // Affine Point + // Jacobian Point // // Functionality stolen from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). diff --git a/src/stealth-addresses/ERC5564Announcer.sol b/src/stealth-addresses/ERC5564Announcer.sol index 3a47e29..82b5440 100644 --- a/src/stealth-addresses/ERC5564Announcer.sol +++ b/src/stealth-addresses/ERC5564Announcer.sol @@ -17,20 +17,20 @@ pragma solidity ^0.8.16; * * Index | Description | Length in bytes * ----------------------------------------------------------------------------- - * [0x00] | View tag | 1 - * [0x01:0x04] | `0xeeeeeeee` | 4 - * [0x05:0x24] | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | 20 - * [0x18:0x38] | Amount in wei | 32 + * [0x00] | View tag | 1 + * [0x01:0x04] | `0xeeeeeeee` | 4 + * [0x05:0x24] | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | 20 + * [0x18:0x38] | Amount in wei | 32 * * - Tx involving a contract call with a single argument, eg ERC-20/ERC-721 * transfers: * * Index | Description | Length in bytes * ----------------------------------------------------------------------------- - * [0x00] | View tag | 1 - * [0x01:0x04] | Solidity function selector | 4 - * [0x05:0x24] | Contract address | 20 - * [0x18:0x38] | One word argument, eg token amount | 32 + * [0x00] | View tag | 1 + * [0x01:0x04] | Solidity function selector | 4 + * [0x05:0x24] | Contract address | 20 + * [0x18:0x38] | One word argument, eg token amount | 32 * * @custom:references * - [EIP-5564]: https://eips.ethereum.org/EIPS/eip-5564 @@ -41,7 +41,7 @@ interface IERC5564Announcer { /// /// @param schemeId Scheme id based on [EIP-5564 Scheme Registry] registry. /// @param stealthAddress The stealth address. - /// @param caller The address who announced the tx. + /// @param caller The address announcing the tx. /// @param ephemeralPubKey The ephemeral public key created during the /// stealth address generation. /// @param metadata Bytes blob providing the view tag and arbitrary diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 050f780..8b42ccb 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -52,12 +52,21 @@ library StealthSecp256k1 { // Stealth Meta Addresses // TODO: See https://eips.ethereum.org/EIPS/eip-5564#stealth-meta-address-format. - function toBytes(StealthAddress memory stealthMetaAddress) - internal - pure - returns (bytes memory) - { - return bytes(""); + // + // st:eth:0x + function toBytes( + StealthMetaAddress memory stealthMetaAddress, + string memory chainShortName + ) internal pure returns (bytes memory) { + bytes memory prefix = + abi.encodePacked(bytes("st:"), bytes(chainShortName), bytes(":0x")); + + bytes memory pubKeys = abi.encodePacked( + stealthMetaAddress.spendingPubKey.toBytes(), + stealthMetaAddress.viewingPubKey.toBytes() + ); + + return abi.encodePacked(prefix, pubKeys); } // Stealth Address @@ -67,9 +76,6 @@ library StealthSecp256k1 { internal returns (StealthAddress memory) { - // TODO: Functionality missing in Secp256k1(Arithmetic): - // - PublicKey + PublicKey - // Create ephemeral key pair. PrivateKey ephemeralPrivKey = Secp256k1.newPrivateKey(); PublicKey memory ephemeralPubKey = ephemeralPrivKey.toPublicKey(); From 8cfa39aed430b74279031fbb72a8d0239d4d9115 Mon Sep 17 00:00:00 2001 From: pascal Date: Thu, 14 Dec 2023 19:49:54 +0100 Subject: [PATCH 08/11] hacking hard --- .../stealth-addresses/StealthSecp256k1.sol | 2 + src/curves/Secp256k1Arithmetic.sol | 252 ++++-------------- src/signatures/Schnorr.sol | 3 +- src/signatures/utils/Nonce.sol | 2 +- src/stealth-addresses/ERC5564Announcer.sol | 12 +- src/stealth-addresses/StealthSecp256k1.sol | 89 ++++++- test/stealth-addresses/StealthSecp256k1.t.sol | 56 ++++ 7 files changed, 204 insertions(+), 212 deletions(-) create mode 100644 test/stealth-addresses/StealthSecp256k1.t.sol diff --git a/examples/stealth-addresses/StealthSecp256k1.sol b/examples/stealth-addresses/StealthSecp256k1.sol index 8eff5de..45fee71 100644 --- a/examples/stealth-addresses/StealthSecp256k1.sol +++ b/examples/stealth-addresses/StealthSecp256k1.sol @@ -57,6 +57,8 @@ contract StealthSecp256k1Example is Script { console.log( "Sender: Creates stealth address based on receiver's meta address" ); + // TODO: receiver's stealh address must be argument for function, not + // an object to call a function on. StealthAddress memory stealth = receiverStealthMeta.newStealthAddress(); // Sender sends ETH to stealth. diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index e71629c..2a0439d 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -25,19 +25,8 @@ struct Point { /** * @notice ProjectivePoint is a secp256k1 point in projective coordinates * -<<<<<<< HEAD * @dev A projective point represents an affine point (x, y) as (X, Y, Z) * satisfying the following equations: -||||||| parent of cc6390d (getting there...) - * @dev Jacobian point represents Affine point (x, y) as (X, Y, Z) satisfying - * the following equations: -======= - * @dev TODO Note to only use Jacobian Point if you know what you're doing. - * If not, use more user-friendly Point functions. - * - * @dev Jacobian point represents Affine point (x, y) as (X, Y, Z) satisfying - * the following equations: ->>>>>>> cc6390d (getting there...) * x = X / Z² * y = Y / Z³ */ @@ -115,20 +104,9 @@ library Secp256k1Arithmetic { /// /// @dev Note that the identity is represented via: /// point.x = point.y = type(uint).max -<<<<<<< HEAD /// /// @dev Note that the identity is also called point at infinity. function Identity() internal pure returns (Point memory) { -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - function PointAtInfinity() internal pure returns (Point memory) { -======= - /// - /// @dev Note that the point at infinity serves as identity element with - /// following rules: - /// ∀ p ∊ Point: p.isOnCurve() → (p + PointAtInfinity() = p) - /// ∀ x ∊ Uint: [s]PointAtInfinity() = PointAtInfinity() - function PointAtInfinity() internal pure returns (Point memory) { ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) return Point(type(uint).max, type(uint).max); } @@ -148,23 +126,10 @@ library Secp256k1Arithmetic { /// where: /// a = 0 /// b = 7 -<<<<<<< HEAD /// /// @dev Note that the identity is also on the curve. -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) -======= - /// - /// @dev Note that the point at infinity is also on the curve. ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) function isOnCurve(Point memory point) internal pure returns (bool) { -<<<<<<< HEAD if (point.isIdentity()) { -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - // TODO: Point at infinity on curve? - if (point.isPointAtInfinity()) { -======= - if (point.isPointAtInfinity()) { ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) return true; } @@ -192,7 +157,7 @@ library Secp256k1Arithmetic { pure returns (bool) { - return point.x == other.x && point.y == other.y; + return (point.x == other.x) && (point.y == other.y); } //---------------------------------- @@ -218,7 +183,8 @@ library Secp256k1Arithmetic { revert("PointNotOnCurve(other)"); } - // Catch addition with identity, ie point at infinity. + // Catch addition with identity. + // Note that point at infinity serves as identity. if (point.isPointAtInfinity()) { return other; } @@ -226,15 +192,20 @@ library Secp256k1Arithmetic { return point; } + // Perform addition in Jacobian coordinates. JacobianPoint memory jSum; JacobianPoint memory jPoint = point.toJacobianPoint(); if (point.equals(other)) { + // point = other → sum = point + point jSum = _jDouble(jPoint); } else { + // point != other → sum = point + other jSum = _jAdd(jPoint, other.toJacobianPoint()); } + // Return sum as Affine point. + // Note that conversion is expensive! return jSum.intoPoint(); } @@ -255,15 +226,19 @@ library Secp256k1Arithmetic { revert("PointNotOnCurve(point)"); } + // Catch muliplication with identity or scalar of zero. if (point.isPointAtInfinity() || scalar == 0) { return PointAtInfinity(); } + // Perform multiplicatino in Jacobian coordinates. JacobianPoint memory jProduct; JacobianPoint memory jPoint = point.toJacobianPoint(); jProduct = _jMul(jPoint, scalar); + // Return product as Affine point. + // Note that conversion is expensive! return jProduct.intoPoint(); } @@ -276,6 +251,7 @@ library Secp256k1Arithmetic { pure returns (JacobianPoint memory) { + // TODO: Point at infinity jacobian encoding = (maxUint maxUint, 1)? return JacobianPoint(point.x, point.y, 1); } @@ -287,6 +263,21 @@ library Secp256k1Arithmetic { // If you need Jacobian functionality exposed, eg for performance // gains, let me know! + /// @dev Returns whether Jacobian point `jPoint` is the point at infinity. + /// + /// @dev Note that point at infinity is represented via: + /// jPoint.x = jPoint.y = type(uint).max + /// jPoint.z = 1 + function isPointAtInfinity(JacobianPoint memory jPoint) + internal + pure + returns (bool) + { + bool xy = (jPoint.x & jPoint.y) == type(uint).max; + + return xy && (jPoint.z == 1); + } + //---------------------------------- // Type Conversion @@ -426,21 +417,17 @@ library Secp256k1Arithmetic { // Functionality stolen from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). /// @dev Assumptions: - /// - Each point is either on the curve or the zero point + /// - Each point is on the curve /// - No point is the point at infinity function _jAdd(JacobianPoint memory jPoint, JacobianPoint memory other) private pure returns (JacobianPoint memory) { - if ((jPoint.x | jPoint.y) == 0) { - return other; - } - if ((other.x | other.y) == 0) { - return jPoint; - } + assert(!jPoint.isPointAtInfinity() && !other.isPointAtInfinity()); JacobianPoint memory sum; + uint l; uint lz; uint da; @@ -482,7 +469,7 @@ library Secp256k1Arithmetic { /// - Point is on the curve /// - Point is not the point at infinity function _jDouble(JacobianPoint memory jPoint) - internal + private pure returns (JacobianPoint memory) { @@ -490,7 +477,6 @@ library Secp256k1Arithmetic { return _jAdd(jPoint, jPoint); } -<<<<<<< HEAD //-------------------------------------------------------------------------- // (De)Serialization @@ -500,39 +486,38 @@ library Secp256k1Arithmetic { /// @dev Returns point `point` as projective point. function toProjectivePoint(Point memory point) internal -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - //-------------------------------------------------------------------------- - // (De)Serialization - - //---------------------------------- - // Point + pure + returns (ProjectivePoint memory) + { + return ProjectivePoint(point.x, point.y, 1); + } - /// @dev Returns point `point` as Jacobian point. - function toJacobianPoint(Point memory point) - internal -======= /// @dev Assumptions: /// - Point is on the curve /// - Point is not the point at infinity /// - Scalar is not zero - function _jMul(JacobianPoint memory jPoint, uint scalar) + function _jMul(ProjectivePoint memory jPoint, uint scalar) private ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) pure returns (ProjectivePoint memory) { -<<<<<<< HEAD - return ProjectivePoint(point.x, point.y, 1); - } -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - return JacobianPoint(point.x, point.y, 1); + assert(!jPoint.isIdentity()); + assert(scalar != 0); + + ProjectivePoint memory copy = jPoint; + ProjectivePoint memory product = ZeroPoint().toProjectivePoint(); + + while (scalar != 0) { + if (scalar & 1 == 1) { + product = _jAdd(product, copy); + } + scalar = scalar >> 1; // Divide by 2. + copy = _jDouble(copy); + } + + return product; } -======= - JacobianPoint memory copy = jPoint; - JacobianPoint memory product = ZeroPoint().toJacobianPoint(); ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) -<<<<<<< HEAD //---------------------------------- // Projective Point @@ -634,122 +619,11 @@ library Secp256k1Arithmetic { tmp := r r := x x := sub(tmp, mul(q, x)) -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - //---------------------------------- - // Jacobian Point - - /// @dev Mutates Jacobian point `jPoint` to Affine point. - function intoPoint(JacobianPoint memory jPoint) - internal - pure - returns (Point memory) - { - // Compute z⁻¹, i.e. the modular inverse of jPoint.z. - uint zInv = modularInverseOf(jPoint.z); - - // Compute (z⁻¹)² (mod P) - uint zInv_2 = mulmod(zInv, zInv, P); - - // Compute jPoint.x * (z⁻¹)² (mod P), i.e. the x coordinate of given - // Jacobian point in Affine representation. - uint x = mulmod(jPoint.x, zInv_2, P); - - // Compute jPoint.y * (z⁻¹)³ (mod P), i.e. the y coordinate of given - // Jacobian point in Affine representation. - uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); - - // Store x and y in jPoint. - assembly ("memory-safe") { - mstore(jPoint, x) - mstore(add(jPoint, 0x20), y) - } - - // Return as Point(jPoint.x, jPoint.y). - // Note that jPoint.z is from now on dirty memory! - Point memory point; - assembly ("memory-safe") { - point := jPoint - } - return point; - } - - //-------------------------------------------------------------------------- - // Utils - - // @todo Use Fermats Little Theorem. While generally less performant, it is - // cheaper on EVM due to the modexp precompile. - // See "Speeding up Elliptic Curve Computations for Ethereum Account Abstraction" page 4. - - /// @dev Returns the modular inverse of `x` for modulo `P`. - /// - /// The modular inverse of `x` is x⁻¹ such that x * x⁻¹ ≡ 1 (mod P). - /// - /// @dev Reverts if: - /// - x not in [1, P) - /// - /// @dev Uses the Extended Euclidean Algorithm. - /// - /// @custom:invariant Terminates in finite time. - function modularInverseOf(uint x) internal pure returns (uint) { - if (x == 0) { - revert("Modular inverse of zero does not exist"); - } - if (x >= P) { - revert("TODO(modularInverse: x >= P)"); - } - - uint t; - uint q; - uint newT = 1; - uint r = P; - - assembly ("memory-safe") { - // Implemented in assembly to circumvent division-by-zero - // and over-/underflow protection. - // - // Functionally equivalent Solidity code: - // while (x != 0) { - // q = r / x; - // (t, newT) = (newT, addmod(t, (P - mulmod(q, newT, P)), P)); - // (r, x) = (x, r - (q * x)); - // } - // - // For the division r / x, x is guaranteed to not be zero via the - // loop condition. - // - // The subtraction of form P - mulmod(_, _, P) is guaranteed to not - // underflow due to the subtrahend being a (mod P) result, - // i.e. the subtrahend being guaranteed to be less than P. - // - // The subterm q * x is guaranteed to not overflow because - // q * x ≤ r due to q = ⎣r / x⎦. - // - // The term r - (q * x) is guaranteed to not underflow because - // q * x ≤ r and therefore r - (q * x) ≥ 0. - for {} x {} { - q := div(r, x) - - let tmp := t - t := newT - newT := addmod(tmp, sub(P, mulmod(q, newT, P)), P) - - tmp := r - r := x - x := sub(tmp, mul(q, x)) -======= - while (scalar != 0) { - if (scalar & 1 == 1) { - product = _jAdd(product, copy); ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) - } - scalar /= 2; - copy = _jDouble(copy); } return product; } -<<<<<<< HEAD /// @dev Returns whether `xInv` is the modular inverse of `x`. function areModularInverse(uint x, uint xInv) internal @@ -766,30 +640,12 @@ library Secp256k1Arithmetic { return mulmod(x, xInv, P) == 1; } - //-------------------------------------------------------------------------- - // Private Functions -||||||| parent of 8b86dc3 (Finishing up arithmetic - still untested) - function areModularInverse(uint x, uint xInv) - internal - pure - returns (bool) - { - if (x == 0 || xInv == 0) { - revert("Modular inverse of zero does not exist"); - } - if (x >= P || xInv >= P) { - revert("TODO(modularInverse: x >= P)"); - } - - return mulmod(x, xInv, P) == 1; - } //-------------------------------------------------------------------------- // Private Functions -======= + //---------------------------------- // Helpers ->>>>>>> 8b86dc3 (Finishing up arithmetic - still untested) function _add(uint x1, uint z1, uint x2, uint z2) private diff --git a/src/signatures/Schnorr.sol b/src/signatures/Schnorr.sol index d955976..8ddd9cc 100644 --- a/src/signatures/Schnorr.sol +++ b/src/signatures/Schnorr.sol @@ -304,6 +304,5 @@ library Schnorr { //-------------------------------------------------------------------------- // (De)Serialization // - // TODO: Schnorr Serde - // Any other standard except BIP-340? + // TODO: Schnorr Serde defined via BIP-340. } diff --git a/src/signatures/utils/Nonce.sol b/src/signatures/utils/Nonce.sol index 4398e93..b5bd21c 100644 --- a/src/signatures/utils/Nonce.sol +++ b/src/signatures/utils/Nonce.sol @@ -53,7 +53,7 @@ library Nonce { /// @dev Derives a deterministic nonce from secret key `sk` and message /// `message`. /// - /// @dev Note that a nonce is of type uint and not bounded by any field! + /// @dev Note that a nonce is of type uint and not bounded to any field! /// /// @custom:invariant Keccak256 image is never zero /// ∀ (sk, digest) ∊ (SecretKey, bytes32): keccak256(sk ‖ digest) != 0 diff --git a/src/stealth-addresses/ERC5564Announcer.sol b/src/stealth-addresses/ERC5564Announcer.sol index 82b5440..9b68144 100644 --- a/src/stealth-addresses/ERC5564Announcer.sol +++ b/src/stealth-addresses/ERC5564Announcer.sol @@ -8,8 +8,8 @@ pragma solidity ^0.8.16; * * @dev Metadata Specification and Recommendations * - * The first byte of the metadata MUST be the view tag. The view tag provides - * a probabilistic filter to skip computations when checking announcements. + * The first byte of the metadata MUST be the view tag. The view tag is a + * probabilistic filter to skip computations when checking announcements. * * The following recommendations are given in [EIP-5564]: * @@ -22,15 +22,15 @@ pragma solidity ^0.8.16; * [0x05:0x24] | `0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE` | 20 * [0x18:0x38] | Amount in wei | 32 * - * - Tx involving a contract call with a single argument, eg ERC-20/ERC-721 + * - Tx involving a contract call with a single argument, eg ERC-20 and ERC-721 * transfers: * * Index | Description | Length in bytes * ----------------------------------------------------------------------------- - * [0x00] | View tag | 1 - * [0x01:0x04] | Solidity function selector | 4 + * [0x00] | View tag | 1 + * [0x01:0x04] | Solidity function selector | 4 * [0x05:0x24] | Contract address | 20 - * [0x18:0x38] | One word argument, eg token amount | 32 + * [0x18:0x38] | One word argument, eg token amount in wei | 32 * * @custom:references * - [EIP-5564]: https://eips.ethereum.org/EIPS/eip-5564 diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 8b42ccb..7fe4204 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -11,6 +11,9 @@ // SPDX-License-Identifier: MIT pragma solidity ^0.8.16; +// TODO: During dev: +import {console2 as console} from "forge-std/console2.sol"; + import {Vm} from "forge-std/Vm.sol"; import {Secp256k1, PrivateKey, PublicKey} from "../curves/Secp256k1.sol"; @@ -49,24 +52,94 @@ library StealthSecp256k1 { using Secp256k1 for Point; using Secp256k1Arithmetic for Point; + // ~~~~~~~ Prelude ~~~~~~~ + // forgefmt: disable-start + Vm private constant vm = Vm(address(uint160(uint(keccak256("hevm cheat code"))))); + modifier vmed() { + if (block.chainid != 31337) revert("requireVm"); + _; + } + // forgefmt: disable-end + // ~~~~~~~~~~~~~~~~~~~~~~~ + // Stealth Meta Addresses // TODO: See https://eips.ethereum.org/EIPS/eip-5564#stealth-meta-address-format. // // st:eth:0x + + /// @dev Returns the string representation of stealth meta address + /// `stealthMetaAddress` for chain `chain`. + /// + /// @dev Note that `chain` should be the chain's short name as defined via + /// https://github.com/ethereum-lists/chains. + /// + /// @dev A stealth meta address' string representation is defined as: + /// `st::0x` + /// + /// @custom:vm vm.toString(bytes) + function toString( + StealthMetaAddress memory stealthMetaAddress, + string memory chain + ) internal vmed returns (string memory) { + string memory prefix = string.concat("st:", chain, ":0x"); + + bytes memory spendingKey; + bytes memory viewingKey; + + string memory key; + key = vm.toString(abi.encodePacked(stealthMetaAddress.spendingPubKey.x, stealthMetaAddress.spendingPubKey.y)); + spendingKey = new bytes(bytes(key).length - 2); + for (uint i = 2; i < bytes(key).length; i++) { + spendingKey[i-2] = bytes(key)[i]; + } + + key = vm.toString(abi.encodePacked(stealthMetaAddress.viewingPubKey.x, stealthMetaAddress.viewingPubKey.y)); + viewingKey = new bytes(bytes(key).length - 2); + for (uint i = 2; i < bytes(key).length; i++) { + viewingKey[i-2] = bytes(key)[i]; + } + + return string.concat(prefix, string(spendingKey), string(viewingKey)); + } + + /// @dev Returns stealth meta address `stealthMetaAddress` for chain `chain` + /// as bytes. + /// + /// @dev Note that `chain` should be the chain's short name as defined via + /// https://github.com/ethereum-lists/chains. + /// + /// @dev Provides following encoding: + /// `st::0x` function toBytes( StealthMetaAddress memory stealthMetaAddress, - string memory chainShortName + string memory chain ) internal pure returns (bytes memory) { - bytes memory prefix = - abi.encodePacked(bytes("st:"), bytes(chainShortName), bytes(":0x")); + return bytes.concat( + bytes("st:"), + bytes(chain), + bytes(":0x") + ); + - bytes memory pubKeys = abi.encodePacked( + bytes memory prefix = bytes(string.concat("st:", chain, ":0x")); + + bytes memory keys = bytes.concat( stealthMetaAddress.spendingPubKey.toBytes(), stealthMetaAddress.viewingPubKey.toBytes() ); - return abi.encodePacked(prefix, pubKeys); + return bytes.concat(prefix, keys); + //bytes.concat(bytes("st:"), bytes(chain)); + //bytes memory prefix = + // abi.encodePacked(bytes("st:"), bytes(chain), bytes(":0x")); + + //bytes memory pubKeys = abi.encodePacked( + // stealthMetaAddress.spendingPubKey.toBytes(), + // stealthMetaAddress.viewingPubKey.toBytes() + //); + + //return abi.encodePacked(prefix, pubKeys); } // Stealth Address @@ -80,6 +153,8 @@ library StealthSecp256k1 { PrivateKey ephemeralPrivKey = Secp256k1.newPrivateKey(); PublicKey memory ephemeralPubKey = ephemeralPrivKey.toPublicKey(); + console.log("[internal] newStealthAddress: Ephemeral key pair created"); + // Compute shared secret. // forgefmt: disable-next-item PublicKey memory sharedPubKey = sma.viewingPubKey @@ -87,6 +162,10 @@ library StealthSecp256k1 { .mul(ephemeralPrivKey.asUint()) .intoPublicKey(); + console.log( + "[internal] newStealthAddress: Shared secret based public key computed" + ); + // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. diff --git a/test/stealth-addresses/StealthSecp256k1.t.sol b/test/stealth-addresses/StealthSecp256k1.t.sol new file mode 100644 index 0000000..31f9cff --- /dev/null +++ b/test/stealth-addresses/StealthSecp256k1.t.sol @@ -0,0 +1,56 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {Secp256k1, PrivateKey, PublicKey} from "src/curves/Secp256k1.sol"; + +import { + StealthSecp256k1, + StealthMetaAddress, + StealthAddress +} from "src/stealth-addresses/StealthSecp256k1.sol"; + +/** + * @notice StealthSecp256k1 Unit Tests + */ +contract StealthSecp256k1Test is Test { + using Secp256k1 for PrivateKey; + + StealthSecp256k1Wrapper wrapper; + + function setUp() public { + wrapper = new StealthSecp256k1Wrapper(); + } + + function test_StealthMetaAddress_toString() public { + PrivateKey spendPrivKey = Secp256k1.privateKeyFromUint(uint(0x5a21e92ba5784ad9e94c9d670d3b21baff82c1668aa9ef9bd039674c7d4589f8)); + PrivateKey viewPrivKey = Secp256k1.privateKeyFromUint(uint(0xf6956ed1c1488982a7a80be72fa0ec8cc978d2c957b431e8b363557e552dbb75)); + + StealthMetaAddress memory stealthMetaAddress = StealthMetaAddress({ + spendingPubKey: spendPrivKey.toPublicKey(), + viewingPubKey: viewPrivKey.toPublicKey() + }); + + string memory chain = "eth"; + + string memory got = wrapper.toString(stealthMetaAddress, chain); + + console.log(got); + } +} + +/** + * @notice Library wrapper to enable forge coverage reporting + * + * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. + */ +contract StealthSecp256k1Wrapper { + function toString( + StealthMetaAddress memory stealthMetaAddress, + string memory chain + ) public returns (string memory) { + return StealthSecp256k1.toString(stealthMetaAddress, chain); + } +} From c0107f86a8c371d3ab19f9da0f7d75441be6cc55 Mon Sep 17 00:00:00 2001 From: pascal Date: Sun, 17 Dec 2023 12:59:38 +0100 Subject: [PATCH 09/11] compiles again ^^ --- src/curves/Secp256k1Arithmetic.sol | 239 ++++-------------- src/stealth-addresses/StealthSecp256k1.sol | 170 ++++++------- .../secp256k1/Secp256k1Arithmetic.t.sol | 24 +- test/stealth-addresses/StealthSecp256k1.t.sol | 24 +- 4 files changed, 151 insertions(+), 306 deletions(-) diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index 2a0439d..14bc4cf 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -184,26 +184,26 @@ library Secp256k1Arithmetic { } // Catch addition with identity. - // Note that point at infinity serves as identity. - if (point.isPointAtInfinity()) { + if (point.isIdentity()) { return other; } - if (other.isPointAtInfinity()) { + if (other.isIdentity()) { return point; } - // Perform addition in Jacobian coordinates. - JacobianPoint memory jSum; - JacobianPoint memory jPoint = point.toJacobianPoint(); + // Perform addition in projective coordinates. + ProjectivePoint memory jSum; + ProjectivePoint memory jPoint = point.toProjectivePoint(); if (point.equals(other)) { // point = other → sum = point + point jSum = _jDouble(jPoint); } else { // point != other → sum = point + other - jSum = _jAdd(jPoint, other.toJacobianPoint()); + jSum = _jAdd(jPoint, other.toProjectivePoint()); } + // TODO: Don't convert. MUST be done by user! // Return sum as Affine point. // Note that conversion is expensive! return jSum.intoPoint(); @@ -227,62 +227,50 @@ library Secp256k1Arithmetic { } // Catch muliplication with identity or scalar of zero. - if (point.isPointAtInfinity() || scalar == 0) { - return PointAtInfinity(); + if (point.isIdentity() || scalar == 0) { + return Identity(); } - // Perform multiplicatino in Jacobian coordinates. - JacobianPoint memory jProduct; - JacobianPoint memory jPoint = point.toJacobianPoint(); + // Perform multiplicatino in projective coordinates. + ProjectivePoint memory jProduct; + ProjectivePoint memory jPoint = point.toProjectivePoint(); jProduct = _jMul(jPoint, scalar); - // Return product as Affine point. + // TODO: Don't convert. Must be done by user! + // Return product as affine point. // Note that conversion is expensive! return jProduct.intoPoint(); } - //---------------------------------- - // Type Conversion - - /// @dev Returns point `point` as Jacobian point. - function toJacobianPoint(Point memory point) - internal - pure - returns (JacobianPoint memory) - { - // TODO: Point at infinity jacobian encoding = (maxUint maxUint, 1)? - return JacobianPoint(point.x, point.y, 1); - } - //-------------------------------------------------------------------------- // Projective Point // - // TODO: Jacobian arithmetic is private for now as they provide less + // TODO: Projective arithmetic is private for now as they provide less // security guarantees. // If you need Jacobian functionality exposed, eg for performance // gains, let me know! - /// @dev Returns whether Jacobian point `jPoint` is the point at infinity. - /// - /// @dev Note that point at infinity is represented via: - /// jPoint.x = jPoint.y = type(uint).max - /// jPoint.z = 1 - function isPointAtInfinity(JacobianPoint memory jPoint) + //-------------------------------------------------------------------------- + // (De)Serialization + + //---------------------------------- + // Point + + /// @dev Returns point `point` as projective point. + function toProjectivePoint(Point memory point) internal pure - returns (bool) + returns (ProjectivePoint memory) { - bool xy = (jPoint.x & jPoint.y) == type(uint).max; - - return xy && (jPoint.z == 1); + return ProjectivePoint(point.x, point.y, 1); } //---------------------------------- - // Type Conversion + // Projective Point - /// @dev Mutates Jacobian point `jPoint` to Affine point. - function intoPoint(JacobianPoint memory jPoint) + /// @dev Mutates projective point `jPoint` to affine point. + function intoPoint(ProjectivePoint memory jPoint) internal pure returns (Point memory) @@ -290,15 +278,15 @@ library Secp256k1Arithmetic { // Compute z⁻¹, i.e. the modular inverse of jPoint.z. uint zInv = modularInverseOf(jPoint.z); - // Compute (z⁻¹)² (mod P) + // Compute (z⁻¹)² (mod p) uint zInv_2 = mulmod(zInv, zInv, P); - // Compute jPoint.x * (z⁻¹)² (mod P), i.e. the x coordinate of given - // Jacobian point in Affine representation. + // Compute jPoint.x * (z⁻¹)² (mod p), i.e. the x coordinate of given + // projective point in affine representation. uint x = mulmod(jPoint.x, zInv_2, P); - // Compute jPoint.y * (z⁻¹)³ (mod P), i.e. the y coordinate of given - // Jacobian point in Affine representation. + // Compute jPoint.y * (z⁻¹)³ (mod p), i.e. the y coordinate of given + // projective point in affine representation. uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); // Store x and y in jPoint. @@ -308,7 +296,7 @@ library Secp256k1Arithmetic { } // Return as Point(jPoint.x, jPoint.y). - // Note that jPoint.z is from now on dirty memory! + // Note that from this moment, jPoint.z is dirty memory! Point memory point; assembly ("memory-safe") { point := jPoint @@ -412,21 +400,22 @@ library Secp256k1Arithmetic { // Private Functions //---------------------------------- - // Jacobian Point + // Projective Point // // Functionality stolen from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). /// @dev Assumptions: /// - Each point is on the curve /// - No point is the point at infinity - function _jAdd(JacobianPoint memory jPoint, JacobianPoint memory other) + function _jAdd(ProjectivePoint memory jPoint, ProjectivePoint memory other) private pure - returns (JacobianPoint memory) + returns (ProjectivePoint memory) { - assert(!jPoint.isPointAtInfinity() && !other.isPointAtInfinity()); + // TODO: Define identity for ProjectPoint. + // assert(!jPoint.isIdentity() && !other.isIdentity()); - JacobianPoint memory sum; + ProjectivePoint memory sum; uint l; uint lz; @@ -468,30 +457,15 @@ library Secp256k1Arithmetic { /// @dev Assumptions: /// - Point is on the curve /// - Point is not the point at infinity - function _jDouble(JacobianPoint memory jPoint) + function _jDouble(ProjectivePoint memory jPoint) private pure - returns (JacobianPoint memory) + returns (ProjectivePoint memory) { // TODO: There are faster doubling formulas. return _jAdd(jPoint, jPoint); } - //-------------------------------------------------------------------------- - // (De)Serialization - - //---------------------------------- - // Point - - /// @dev Returns point `point` as projective point. - function toProjectivePoint(Point memory point) - internal - pure - returns (ProjectivePoint memory) - { - return ProjectivePoint(point.x, point.y, 1); - } - /// @dev Assumptions: /// - Point is on the curve /// - Point is not the point at infinity @@ -501,7 +475,8 @@ library Secp256k1Arithmetic { pure returns (ProjectivePoint memory) { - assert(!jPoint.isIdentity()); + // TODO: Define identity for ProjectPoint. + // assert(!jPoint.isIdentity()); assert(scalar != 0); ProjectivePoint memory copy = jPoint; @@ -518,132 +493,6 @@ library Secp256k1Arithmetic { return product; } - //---------------------------------- - // Projective Point - - /// @dev Mutates projective point `jPoint` to affine point. - function intoPoint(ProjectivePoint memory jPoint) - internal - pure - returns (Point memory) - { - // Compute z⁻¹, i.e. the modular inverse of jPoint.z. - uint zInv = modularInverseOf(jPoint.z); - - // Compute (z⁻¹)² (mod p) - uint zInv_2 = mulmod(zInv, zInv, P); - - // Compute jPoint.x * (z⁻¹)² (mod p), i.e. the x coordinate of given - // projective point in affine representation. - uint x = mulmod(jPoint.x, zInv_2, P); - - // Compute jPoint.y * (z⁻¹)³ (mod p), i.e. the y coordinate of given - // projective point in affine representation. - uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); - - // Store x and y in jPoint. - assembly ("memory-safe") { - mstore(jPoint, x) - mstore(add(jPoint, 0x20), y) - } - - // Return as Point(jPoint.x, jPoint.y). - // Note that from this moment, jPoint.z is dirty memory! - Point memory point; - assembly ("memory-safe") { - point := jPoint - } - return point; - } - - //-------------------------------------------------------------------------- - // Utils - - // @todo Use Fermats Little Theorem. While generally less performant, it is - // cheaper on EVM due to the modexp precompile. - // See "Speeding up Elliptic Curve Computations for Ethereum Account Abstraction" page 4. - - /// @dev Returns the modular inverse of `x` for modulo `P`. - /// - /// The modular inverse of `x` is x⁻¹ such that x * x⁻¹ ≡ 1 (mod p). - /// - /// @dev Reverts if: - /// - x not in [1, P) - /// - /// @dev Uses the Extended Euclidean Algorithm. - /// - /// @custom:invariant Terminates in finite time. - function modularInverseOf(uint x) internal pure returns (uint) { - if (x == 0) { - revert("Modular inverse of zero does not exist"); - } - if (x >= P) { - revert("TODO(modularInverse: x >= P)"); - } - - uint t; - uint q; - uint newT = 1; - uint r = P; - - assembly ("memory-safe") { - // Implemented in assembly to circumvent division-by-zero - // and over-/underflow protection. - // - // Functionally equivalent Solidity code: - // while (x != 0) { - // q = r / x; - // (t, newT) = (newT, addmod(t, (P - mulmod(q, newT, P)), P)); - // (r, x) = (x, r - (q * x)); - // } - // - // For the division r / x, x is guaranteed to not be zero via the - // loop condition. - // - // The subtraction of form P - mulmod(_, _, P) is guaranteed to not - // underflow due to the subtrahend being a (mod P) result, - // i.e. the subtrahend being guaranteed to be less than P. - // - // The subterm q * x is guaranteed to not overflow because - // q * x ≤ r due to q = ⎣r / x⎦. - // - // The term r - (q * x) is guaranteed to not underflow because - // q * x ≤ r and therefore r - (q * x) ≥ 0. - for {} x {} { - q := div(r, x) - - let tmp := t - t := newT - newT := addmod(tmp, sub(P, mulmod(q, newT, P)), P) - - tmp := r - r := x - x := sub(tmp, mul(q, x)) - } - - return product; - } - - /// @dev Returns whether `xInv` is the modular inverse of `x`. - function areModularInverse(uint x, uint xInv) - internal - pure - returns (bool) - { - if (x == 0 || xInv == 0) { - revert("Modular inverse of zero does not exist"); - } - if (x >= P || xInv >= P) { - revert("TODO(modularInverse: x >= P)"); - } - - return mulmod(x, xInv, P) == 1; - } - - - //-------------------------------------------------------------------------- - // Private Functions - //---------------------------------- // Helpers diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 7fe4204..77bd13f 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -16,19 +16,24 @@ import {console2 as console} from "forge-std/console2.sol"; import {Vm} from "forge-std/Vm.sol"; -import {Secp256k1, PrivateKey, PublicKey} from "../curves/Secp256k1.sol"; +import {Secp256k1, SecretKey, PublicKey} from "../curves/Secp256k1.sol"; import {Secp256k1Arithmetic, Point} from "../curves/Secp256k1Arithmetic.sol"; uint constant SCHEME_ID = 1; +/// @custom:field spendPk The spending public key +/// @custom:field viewPk The viewing public key struct StealthMetaAddress { - PublicKey spendingPubKey; - PublicKey viewingPubKey; + PublicKey spendPk; + PublicKey viewPk; } +/// @custom:field recipient +/// @custom:field ephPk The ephemeral public key +/// @custom:field viewTag struct StealthAddress { address recipient; - PublicKey ephemeralPubKey; + PublicKey ephPk; uint8 viewTag; } @@ -47,7 +52,7 @@ struct StealthAddress { * @author crysol (https://github.com/pmerkleplant/crysol) */ library StealthSecp256k1 { - using Secp256k1 for PrivateKey; + using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; using Secp256k1 for Point; using Secp256k1Arithmetic for Point; @@ -68,8 +73,8 @@ library StealthSecp256k1 { // // st:eth:0x - /// @dev Returns the string representation of stealth meta address - /// `stealthMetaAddress` for chain `chain`. + /// @dev Returns the string representation of stealth meta address `sma` for + /// chain `chain`. /// /// @dev Note that `chain` should be the chain's short name as defined via /// https://github.com/ethereum-lists/chains. @@ -78,26 +83,27 @@ library StealthSecp256k1 { /// `st::0x` /// /// @custom:vm vm.toString(bytes) - function toString( - StealthMetaAddress memory stealthMetaAddress, - string memory chain - ) internal vmed returns (string memory) { + function toString(StealthMetaAddress memory sma, string memory chain) + internal + vmed + returns (string memory) + { string memory prefix = string.concat("st:", chain, ":0x"); bytes memory spendingKey; bytes memory viewingKey; string memory key; - key = vm.toString(abi.encodePacked(stealthMetaAddress.spendingPubKey.x, stealthMetaAddress.spendingPubKey.y)); + key = vm.toString(abi.encodePacked(sma.spendPk.x, sma.spendPk.y)); spendingKey = new bytes(bytes(key).length - 2); for (uint i = 2; i < bytes(key).length; i++) { - spendingKey[i-2] = bytes(key)[i]; + spendingKey[i - 2] = bytes(key)[i]; } - key = vm.toString(abi.encodePacked(stealthMetaAddress.viewingPubKey.x, stealthMetaAddress.viewingPubKey.y)); + key = vm.toString(abi.encodePacked(sma.viewPk.x, sma.viewPk.y)); viewingKey = new bytes(bytes(key).length - 2); for (uint i = 2; i < bytes(key).length; i++) { - viewingKey[i-2] = bytes(key)[i]; + viewingKey[i - 2] = bytes(key)[i]; } return string.concat(prefix, string(spendingKey), string(viewingKey)); @@ -111,23 +117,17 @@ library StealthSecp256k1 { /// /// @dev Provides following encoding: /// `st::0x` - function toBytes( - StealthMetaAddress memory stealthMetaAddress, - string memory chain - ) internal pure returns (bytes memory) { - return bytes.concat( - bytes("st:"), - bytes(chain), - bytes(":0x") - ); - + function toBytes(StealthMetaAddress memory sma, string memory chain) + internal + pure + returns (bytes memory) + { + return bytes.concat(bytes("st:"), bytes(chain), bytes(":0x")); bytes memory prefix = bytes(string.concat("st:", chain, ":0x")); - bytes memory keys = bytes.concat( - stealthMetaAddress.spendingPubKey.toBytes(), - stealthMetaAddress.viewingPubKey.toBytes() - ); + bytes memory keys = + bytes.concat(sma.spendPk.toBytes(), sma.viewPk.toBytes()); return bytes.concat(prefix, keys); //bytes.concat(bytes("st:"), bytes(chain)); @@ -150,17 +150,16 @@ library StealthSecp256k1 { returns (StealthAddress memory) { // Create ephemeral key pair. - PrivateKey ephemeralPrivKey = Secp256k1.newPrivateKey(); - PublicKey memory ephemeralPubKey = ephemeralPrivKey.toPublicKey(); + SecretKey ephSk = Secp256k1.newSecretKey(); + PublicKey memory ephPk = ephSk.toPublicKey(); console.log("[internal] newStealthAddress: Ephemeral key pair created"); // Compute shared secret. // forgefmt: disable-next-item - PublicKey memory sharedPubKey = sma.viewingPubKey - .intoPoint() - .mul(ephemeralPrivKey.asUint()) - .intoPublicKey(); + PublicKey memory sharedPk = sma.viewPk.intoPoint() + .mul(ephSk.asUint()) + .intoPublicKey(); console.log( "[internal] newStealthAddress: Shared secret based public key computed" @@ -169,107 +168,96 @@ library StealthSecp256k1 { // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. - PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( - uint(sharedPubKey.toHash()) % Secp256k1.Q - ); + SecretKey sharedSecretSk = + Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); // Extract view tag from shared secret. - uint8 viewTag = uint8(sharedSecretPrivKey.asUint() >> 152); + uint8 viewTag = uint8(sharedSecretSk.asUint() >> 152); - // Compute public key from shared secret private key. - PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); + // Compute public key from shared secret secret key. + PublicKey memory sharedSecretPk = sharedSecretSk.toPublicKey(); // Compute recipients public key. // forgefmt: disable-next-item - PublicKey memory recipientPubKey = sma.spendingPubKey - .intoPoint() - .add(sharedSecretPubKey - .intoPoint()) - .intoPublicKey(); + PublicKey memory recipientPk = sma.spendPk + .intoPoint() + .add(sharedSecretPk.intoPoint()) + .intoPublicKey(); // Derive recipients address from their public key. - address recipientAddr = recipientPubKey.toAddress(); + address recipientAddr = recipientPk.toAddress(); - return StealthAddress(recipientAddr, ephemeralPubKey, viewTag); + return StealthAddress(recipientAddr, ephPk, viewTag); } /// @custom:invariant Shared secret private key is not zero. - /// ∀ (viewPrivKey, ephPubKey) ∊ (PrivateKey, PublicKey): - /// ([viewPrivKey]ephPubKey).toHash() != 0 (mod Q) + /// ∀ (viewSk, ephPk) ∊ (SecretKey, PublicKey): + /// ([viewSk]ephPk).toHash() != 0 (mod Q) function checkStealthAddress( - PrivateKey viewingPrivKey, - PublicKey memory spendingPubKey, - StealthAddress memory stealthAddress + SecretKey viewSk, + PublicKey memory spendPk, + StealthAddress memory sa ) internal returns (bool) { - // Compute shared secret. + // Compute shared public key. // forgefmt: disable-next-item - PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey - .intoPoint() - .mul(viewingPrivKey.asUint()) - .intoPublicKey(); + PublicKey memory sharedPk = sa.ephPk + .intoPoint() + .mul(viewSk.asUint()) + .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bound to field. - PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( - uint(sharedPubKey.toHash()) % Secp256k1.Q - ); + SecretKey sharedSecretSk = + Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); // Extract view tag from shared secret. - uint8 viewTag = uint8(sharedSecretPrivKey.asUint() >> 152); + uint8 viewTag = uint8(sharedSecretSk.asUint() >> 152); // Return early if view tags do not match. - if (viewTag != stealthAddress.viewTag) { + if (viewTag != sa.viewTag) { return false; } - // Compute public key from shared secret private key. - PublicKey memory sharedSecretPubKey = sharedSecretPrivKey.toPublicKey(); + // Compute public key from shared secret secret key. + PublicKey memory sharedSecretPk = sharedSecretSk.toPublicKey(); // Compute recipients public key. // forgefmt: disable-next-item - PublicKey memory recipientPubKey = spendingPubKey - .intoPoint() - .add(sharedSecretPubKey - .intoPoint()) - .intoPublicKey(); + PublicKey memory recipientPk = spendPk.intoPoint() + .add(sharedSecretPk.intoPoint()) + .intoPublicKey(); // Derive recipients address from their public key. - address recipientAddr = recipientPubKey.toAddress(); + address recipientAddr = recipientPk.toAddress(); // Return true if stealth address' address matches computed recipients // address. - return recipientAddr == stealthAddress.recipient; + return recipientAddr == sa.recipient; } // Private Key - function computeStealthPrivateKey( - PrivateKey spendingPrivKey, - PrivateKey viewingPrivKey, - StealthAddress memory stealthAddress - ) internal returns (PrivateKey) { - // Compute shared secret. + function computeStealthSecretKey( + SecretKey spendSk, + SecretKey viewSk, + StealthAddress memory sa + ) internal returns (SecretKey) { + // Compute shared secret public key. // forgefmt: disable-next-item - PublicKey memory sharedPubKey = stealthAddress.ephemeralPubKey - .intoPoint() - .mul(viewingPrivKey.asUint()) + PublicKey memory sharedPk = sa.ephPk.intoPoint() + .mul(viewSk.asUint()) .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. - PrivateKey sharedSecretPrivKey = Secp256k1.privateKeyFromUint( - uint(sharedPubKey.toHash()) % Secp256k1.Q - ); + SecretKey sharedSecretSk = + Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); // Compute stealth private key. - PrivateKey stealthPrivKey = Secp256k1.privateKeyFromUint( - addmod( - spendingPrivKey.asUint(), - sharedSecretPrivKey.asUint(), - Secp256k1.Q - ) + SecretKey stealthSk = Secp256k1.secretKeyFromUint( + addmod(spendSk.asUint(), sharedSecretSk.asUint(), Secp256k1.Q) ); - return stealthPrivKey; + return stealthSk; } } diff --git a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol index 2cc511c..d4763c5 100644 --- a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol @@ -78,8 +78,8 @@ contract Secp256k1ArithmeticTest is Test { assertTrue(wrapper.isOnCurve(point)); } - function test_Point_isOnCurve_PointAtInfinity() public { - assertTrue(wrapper.isOnCurve(Secp256k1Arithmetic.PointAtInfinity())); + function test_Point_isOnCurve_Identity() public { + assertTrue(wrapper.isOnCurve(Secp256k1Arithmetic.Identity())); } // -- yParity @@ -94,24 +94,24 @@ contract Secp256k1ArithmeticTest is Test { // -- equals - function testFuzz_Point_equals(PrivateKey privKey) public { - vm.assume(privKey.isValid()); + function testFuzz_Point_equals(SecretKey sk) public { + vm.assume(sk.isValid()); - Point memory point = privKey.toPublicKey().intoPoint(); + Point memory point = sk.toPublicKey().intoPoint(); assertTrue(wrapper.equals(point, point)); } function testFuzz_Point_equals_FailsIfPointsDoNotEqual( - PrivateKey privKey1, - PrivateKey privKey2 + SecretKey sk1, + SecretKey sk2 ) public { - vm.assume(privKey1.asUint() != privKey2.asUint()); - vm.assume(privKey1.isValid()); - vm.assume(privKey2.isValid()); + vm.assume(sk1.asUint() != sk2.asUint()); + vm.assume(sk1.isValid()); + vm.assume(sk2.isValid()); - Point memory point1 = privKey1.toPublicKey().intoPoint(); - Point memory point2 = privKey2.toPublicKey().intoPoint(); + Point memory point1 = sk1.toPublicKey().intoPoint(); + Point memory point2 = sk2.toPublicKey().intoPoint(); assertFalse(wrapper.equals(point1, point2)); } diff --git a/test/stealth-addresses/StealthSecp256k1.t.sol b/test/stealth-addresses/StealthSecp256k1.t.sol index 31f9cff..ecd1e93 100644 --- a/test/stealth-addresses/StealthSecp256k1.t.sol +++ b/test/stealth-addresses/StealthSecp256k1.t.sol @@ -4,7 +4,7 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Secp256k1, PrivateKey, PublicKey} from "src/curves/Secp256k1.sol"; +import {Secp256k1, SecretKey, PublicKey} from "src/curves/Secp256k1.sol"; import { StealthSecp256k1, @@ -16,7 +16,7 @@ import { * @notice StealthSecp256k1 Unit Tests */ contract StealthSecp256k1Test is Test { - using Secp256k1 for PrivateKey; + using Secp256k1 for SecretKey; StealthSecp256k1Wrapper wrapper; @@ -25,17 +25,25 @@ contract StealthSecp256k1Test is Test { } function test_StealthMetaAddress_toString() public { - PrivateKey spendPrivKey = Secp256k1.privateKeyFromUint(uint(0x5a21e92ba5784ad9e94c9d670d3b21baff82c1668aa9ef9bd039674c7d4589f8)); - PrivateKey viewPrivKey = Secp256k1.privateKeyFromUint(uint(0xf6956ed1c1488982a7a80be72fa0ec8cc978d2c957b431e8b363557e552dbb75)); + SecretKey spendSk = Secp256k1.secretKeyFromUint( + uint( + 0x5a21e92ba5784ad9e94c9d670d3b21baff82c1668aa9ef9bd039674c7d4589f8 + ) + ); + SecretKey viewSk = Secp256k1.secretKeyFromUint( + uint( + 0xf6956ed1c1488982a7a80be72fa0ec8cc978d2c957b431e8b363557e552dbb75 + ) + ); - StealthMetaAddress memory stealthMetaAddress = StealthMetaAddress({ - spendingPubKey: spendPrivKey.toPublicKey(), - viewingPubKey: viewPrivKey.toPublicKey() + StealthMetaAddress memory sma = StealthMetaAddress({ + spendPk: spendSk.toPublicKey(), + viewPk: viewSk.toPublicKey() }); string memory chain = "eth"; - string memory got = wrapper.toString(stealthMetaAddress, chain); + string memory got = wrapper.toString(sma, chain); console.log(got); } From 434be0a4baf0f086bc3198a12ded71b1b9e08ed6 Mon Sep 17 00:00:00 2001 From: pascal Date: Tue, 19 Dec 2023 00:55:46 +0100 Subject: [PATCH 10/11] -\_O? --- README.md | 8 +- docs/Intro.md | 19 ++- .../stealth-addresses/StealthSecp256k1.sol | 85 +++++----- foundry.toml | 2 +- src/curves/Secp256k1.sol | 2 +- src/curves/Secp256k1Arithmetic.sol | 149 +++++++++++++++--- src/signatures/Schnorr.sol | 1 + src/signatures/utils/Nonce.sol | 11 +- src/stealth-addresses/StealthSecp256k1.sol | 144 ++++++++++------- .../secp256k1/Secp256k1Arithmetic.t.sol | 25 ++- 10 files changed, 314 insertions(+), 132 deletions(-) diff --git a/README.md b/README.md index b48535e..cf8f4e7 100644 --- a/README.md +++ b/README.md @@ -10,12 +10,14 @@ > **Warning** > -> Still in early development. Don't use yet except you know what your doing! +> Still in early development. +> Don't use yet except you know what your doing! +> > Expect breaking changes! -`crysol` is a set of Solidity libraries providing **clean** and **well-tested** implementations of cryptographic algorithms. +`crysol` is a collection of pure Solidity libraries providing clean +implementations of cryptographic algorithms for on- and offchain usage. -Differentiating to other projects, `crysol` also provides functionality to create cryptographic objects. ## Installation diff --git a/docs/Intro.md b/docs/Intro.md index 3c167e5..ea902ff 100644 --- a/docs/Intro.md +++ b/docs/Intro.md @@ -2,19 +2,26 @@ ## What are `vmed` functions? -Traditionally, Solidity has been primarily used for verifying cryptographic objects and rarely for creating them, eg we verify ECDSA signatures in Solidity via `ecrecover` and create them via our non-Solidity based wallet libraries. +Traditionally, Solidity has been primarily used for verifying cryptographic +objects and rarely for creating them, eg we verify ECDSA signatures in Solidity +via `ecrecover` and create them via our non-Solidity based wallet libraries. -`crysol` takes a more comprehensive approach and also provides functionality to create cryptographic objects, allowing developers to test and experiment with cryptographic systems from within their Solidity environment. +`crysol` takes a more comprehensive approach and also provides functionality to +create cryptographic objects, allowing developers to test and research +cryptographic systems in a pure EVM environment. -However, most Solidity code is run on public blockchains - the last place one should perform operations requiring a private key as input. +However, most Solidity code is run on public blockchains - the last place one +should perform operations requiring a secret key as input. -To ensure operations using sensitive data are never run on non-local blockchains such functions are "`vmed`", meaning they revert whenever the blockchain's chain id is not `31337`. +To ensure operations using sensitive data are never run on non-local blockchains +such functions are "`vmed`", meaning they revert whenever the chain id is not `31337`. ## The Prelude -Many libraries include a code block called _prelude_ providing common internal functionality. -It provides the `vmed` modifier which protects certain functions from being called in non-local environments. +Many libraries include a code block called _prelude_ providing common internal +functionality such as the `vmed` modifier which protects certain functions from +being called in non-local environments. The _prelude_ code is: diff --git a/examples/stealth-addresses/StealthSecp256k1.sol b/examples/stealth-addresses/StealthSecp256k1.sol index 45fee71..f8f7f8d 100644 --- a/examples/stealth-addresses/StealthSecp256k1.sol +++ b/examples/stealth-addresses/StealthSecp256k1.sol @@ -2,8 +2,11 @@ pragma solidity ^0.8.16; import {Script} from "forge-std/Script.sol"; +import {StdStyle} from "forge-std/StdStyle.sol"; import {console2 as console} from "forge-std/console2.sol"; +import {Secp256k1, SecretKey, PublicKey} from "src/curves/Secp256k1.sol"; + import { StealthSecp256k1, StealthAddress, @@ -15,95 +18,101 @@ import { ERC5564Announcer } from "src/stealth-addresses/ERC5564Announcer.sol"; -import {Secp256k1, PrivateKey, PublicKey} from "src/curves/Secp256k1.sol"; - contract StealthSecp256k1Example is Script { - using StealthSecp256k1 for PrivateKey; + using StealthSecp256k1 for SecretKey; using StealthSecp256k1 for StealthAddress; using StealthSecp256k1 for StealthMetaAddress; - using Secp256k1 for PrivateKey; + using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; function run() public { // Sender key pair. - console.log("Sender: Creates key pair"); - PrivateKey senderPrivKey = Secp256k1.newPrivateKey(); - PublicKey memory senderPubKey = senderPrivKey.toPublicKey(); + SecretKey senderSk = Secp256k1.newSecretKey(); + PublicKey memory senderPk = senderSk.toPublicKey(); + logSender("Created key pair"); // Receiver key pairs consist of spending and viewing key pairs. - console.log("Receiver: Creates key pairs"); - PrivateKey receiverSpendingPrivKey = Secp256k1.newPrivateKey(); - PublicKey memory receiverSpendingPubKey = - receiverSpendingPrivKey.toPublicKey(); - PrivateKey receiverViewPrivKey = Secp256k1.newPrivateKey(); - PublicKey memory receiverViewPubKey = receiverViewPrivKey.toPublicKey(); + SecretKey receiverSpendSk = Secp256k1.newSecretKey(); + PublicKey memory receiverSpendPk = receiverSpendSk.toPublicKey(); + SecretKey receiverViewSk = Secp256k1.newSecretKey(); + PublicKey memory receiverViewPk = receiverViewSk.toPublicKey(); + logReceiver("Created key pair"); // There exists an ERC5564Announcer instance. IERC5564Announcer announcer = IERC5564Announcer(new ERC5564Announcer()); // Receiver creates their stealth meta address. // TODO: Note that these addresses need to be published somehow. - console.log("Receiver: Creates stealth meta address"); - StealthMetaAddress memory receiverStealthMeta; - receiverStealthMeta = StealthMetaAddress({ - spendingPubKey: receiverSpendingPubKey, - viewingPubKey: receiverViewPubKey + StealthMetaAddress memory receiverSma = StealthMetaAddress({ + spendPk: receiverSpendPk, + viewPk: receiverViewPk }); - console.log("Stealth Meta Address: "); - console.logBytes(receiverStealthMeta.toBytes("eth")); + logReceiver( + string.concat( + "Created Ethereum stealth meta address: ", receiverSma.toString("eth") + ) + ); // Sender creates stealth address from receiver's stealth meta address. - console.log( - "Sender: Creates stealth address based on receiver's meta address" - ); // TODO: receiver's stealh address must be argument for function, not // an object to call a function on. - StealthAddress memory stealth = receiverStealthMeta.newStealthAddress(); + StealthAddress memory stealth = receiverSma.newStealthAddress(); + logSender("Created stealth address from receiver's stealth meta address"); // Sender sends ETH to stealth. - console.log("Sender: Sends ETH to receiver's stealth address"); - vm.deal(senderPubKey.toAddress(), 1 ether); - vm.prank(senderPubKey.toAddress()); + vm.deal(senderPk.toAddress(), 1 ether); + vm.prank(senderPk.toAddress()); (bool ok, ) = stealth.recipient.call{value: 1 ether}(""); require(ok, "Sender: ETH transfer failed"); + logSender("Send 1 ETH to stealth address"); // Sender announces tx via ERC5564Announcer. - console.log("Sender: Announces tx via ERC5564Announcer"); - vm.prank(senderPubKey.toAddress()); + vm.prank(senderPk.toAddress()); announcer.announce({ schemeId: SCHEME_ID, stealthAddress: stealth.recipient, - ephemeralPubKey: stealth.ephemeralPubKey.toBytes(), + ephemeralPubKey: stealth.ephPk.toBytes(), // See ERC5564Announcer.sol for more info. metadata: abi.encodePacked( stealth.viewTag, hex"eeeeeeee", hex"eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", uint(1 ether) - ) + ) }); + logSender("Announced tx via ERC-5564 announcer"); // Receiver checks announces stealth address. - console.log("Receiver: Verifies tx is based on own meta address"); require( - receiverViewPrivKey.checkStealthAddress( - receiverSpendingPubKey, stealth - ), + receiverViewSk.checkStealthAddress(receiverSpendPk, stealth), "Check failed" ); + logReceiver("Verfied tx is based on own stealth meta address"); // Receiver computed stealth's private key. console.log("Receiver: Computes private key for stealth address"); - PrivateKey stealthPrivKey = receiverSpendingPrivKey - .computeStealthPrivateKey(receiverViewPrivKey, stealth); + SecretKey stealthSk = receiverSpendSk + .computeStealthSecretKey(receiverViewSk, stealth); // Verify computed private key's address matches stealth's recipient // address. console.log("Receiver: Verifies access on stealth address"); require( - stealthPrivKey.toPublicKey().toAddress() == stealth.recipient, + stealthSk.toPublicKey().toAddress() == stealth.recipient, "Private key computation failed" ); } + + function logSender(string memory message) internal { + console.log( + string.concat(StdStyle.yellow("[SENDER] "), message) + ); + } + + function logReceiver(string memory message) internal { + console.log( + string.concat(StdStyle.blue("[RECEIVER] "), message) + ); + } } diff --git a/foundry.toml b/foundry.toml index 405d846..099616c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -7,7 +7,7 @@ ffi = true # Compilation evm_version = "shanghai" solc_version = "0.8.23" -via_ir = false +via_ir = true optimizer = false extra_output_files = ["irOptimized"] diff --git a/src/curves/Secp256k1.sol b/src/curves/Secp256k1.sol index 64162fd..507dc60 100644 --- a/src/curves/Secp256k1.sol +++ b/src/curves/Secp256k1.sol @@ -33,7 +33,7 @@ import { import {Random} from "../Random.sol"; /** - * @notice Secret key is an secp256k1 secret key + * @notice SecretKey is an secp256k1 secret key * * @dev Note that a secret key MUST be a field element, ie sk ∊ [1, Q). * diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index 14bc4cf..d635589 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -163,6 +163,16 @@ library Secp256k1Arithmetic { //---------------------------------- // Arithmetic + // TODO: Provide add(Projective, Projective) -> Projective for simple one-time additions. + // Users needs to convert intoPoint() to realize expense. + // Use complete addition formula Alg 7 from https://eprint.iacr.org/2015/1060.pdf. + // + // TODO: Provide add(Projective, Affine) -> Projective for sequential additions. + // Users needs to convert intoPoint() to realize expense. + // Use complete addition formula Alg 8 from https://eprint.iacr.org/2015/1060.pdf. + // => Only if cheaper though, otherwise same as Affine->Projective is constant operation. + // => Therefore not important for now. Only optimization issue -> After initial release. + /// @dev Returns the sum of points `point` and `other` as new point. /// /// @dev TODO Note about performance. intoPoint() conversion is expensive. @@ -245,11 +255,99 @@ library Secp256k1Arithmetic { //-------------------------------------------------------------------------- // Projective Point - // - // TODO: Projective arithmetic is private for now as they provide less - // security guarantees. - // If you need Jacobian functionality exposed, eg for performance - // gains, let me know! + + /// @dev Returns whether point `point` is the identity. + /// + /// @dev Note that the identity is represented via: + /// point.x = 0, point.y = 1, point.z = 0 + /// + /// @dev Note that the identity is also called point at infinity. + function isIdentity(ProjectivePoint memory point) + internal + pure + returns (bool) + { + return point.x == 0 && point.y == 1 && point.z == 0; + // return point.y == 1 && (point.x | point.y) == 0; + } + + //---------------------------------- + // Arithmetic + + /// @dev + function add(ProjectivePoint memory point, ProjectivePoint memory other) + internal + pure + returns (ProjectivePoint memory) + { + // Uses complete addition formula from Renes-Costello-Batina 2015. + // See https://eprint.iacr.org/2015/1060.pdf Alg 7. + + // forgefmt: disable-start + + if (point.isIdentity()) { + return other; + } + if (other.isIdentity()) { + return point; + } + + // Inputs: P = (x1, y1, z1), Q = (x2, y2, z2) + uint x1 = point.x; uint x2 = other.x; + uint y1 = point.y; uint y2 = other.y; + uint z1 = point.z; uint z2 = other.z; + + // Output: P + Q = (x3, y3, z3) + uint x3; + uint y3; + uint z3; + + // Constants: TODO Make constant + uint b3 = mulmod(3, B, P); + + // Variables: + uint t0; uint t1; uint t2; uint t3; uint t4; + + // Alg: + // Note that x - y = (Q - y) + x (mod P) + t0 = mulmod(x1, x2, P); + t1 = mulmod(y1, y2, P); + t2 = mulmod(z1, z2, P); + t3 = addmod(x1, y1, P); + t4 = addmod(x2, y2, P); + t3 = mulmod(t3, t4, P); + t4 = addmod(t0, t1, P); + unchecked { t3 = addmod(Q - t4, t3, P); } + t4 = addmod(y1, z1, P); + x3 = addmod(y2, z2, P); + t4 = mulmod(t4, x3, P); + x3 = addmod(t1, t2, P); + unchecked { t4 = addmod(Q - x3, t4, P); } + x3 = addmod(x1, z1, P); + y3 = addmod(x2, z2, P); + x3 = mulmod(x3, y3, P); + y3 = addmod(t0, t2, P); + unchecked { y3 = addmod(Q - y3, x3, P); } + x3 = addmod(t0, t0, P); + t0 = addmod(x3, t0, P); + t2 = mulmod(b3, t2, P); + z3 = addmod(t1, t2, P); + unchecked { t1 = addmod(Q - t2, t1, P); } + y3 = mulmod(b3, y3, P); + x3 = mulmod(t4, y3, P); + t2 = mulmod(t3, t1, P); + unchecked { x3 = addmod(Q - x3, t2, P); } + y3 = mulmod(y3, t0, P); + t1 = mulmod(t1, z3, P); + y3 = addmod(t1, y3, P); + t0 = mulmod(t0, t2, P); + z3 = mulmod(z3, t4, P); + z3 = addmod(z3, t0, P); + // forgefmt: disable-end + + // Return as ProjectivePoint. + return ProjectivePoint(x3, y3, z3); + } //-------------------------------------------------------------------------- // (De)Serialization @@ -263,45 +361,52 @@ library Secp256k1Arithmetic { pure returns (ProjectivePoint memory) { - return ProjectivePoint(point.x, point.y, 1); + // TODO: Comment about projective identity representation. + return point.isIdentity() + ? ProjectivePoint(0, 1, 0) + : ProjectivePoint(point.x, point.y, 1); } //---------------------------------- // Projective Point - /// @dev Mutates projective point `jPoint` to affine point. - function intoPoint(ProjectivePoint memory jPoint) + /// @dev Mutates projective point `point` to affine point. + function intoPoint(ProjectivePoint memory point) internal pure returns (Point memory) { - // Compute z⁻¹, i.e. the modular inverse of jPoint.z. - uint zInv = modularInverseOf(jPoint.z); + if (point.isIdentity()) { + return Identity(); + } + + // Compute z⁻¹, i.e. the modular inverse of point.z. + uint zInv = modularInverseOf(point.z); // Compute (z⁻¹)² (mod p) uint zInv_2 = mulmod(zInv, zInv, P); - // Compute jPoint.x * (z⁻¹)² (mod p), i.e. the x coordinate of given + // Compute point.x * (z⁻¹)² (mod p), i.e. the x coordinate of given // projective point in affine representation. - uint x = mulmod(jPoint.x, zInv_2, P); + uint x = mulmod(point.x, zInv_2, P); - // Compute jPoint.y * (z⁻¹)³ (mod p), i.e. the y coordinate of given + // Compute point.y * (z⁻¹)³ (mod p), i.e. the y coordinate of given // projective point in affine representation. - uint y = mulmod(jPoint.y, mulmod(zInv, zInv_2, P), P); + uint y = mulmod(point.y, mulmod(zInv, zInv_2, P), P); - // Store x and y in jPoint. + // Store x and y in point. assembly ("memory-safe") { - mstore(jPoint, x) - mstore(add(jPoint, 0x20), y) + mstore(point, x) + mstore(add(point, 0x20), y) } - // Return as Point(jPoint.x, jPoint.y). - // Note that from this moment, jPoint.z is dirty memory! - Point memory point; + // Return as Point(point.x, point.y). + // Note that from this moment point.z is dirty memory! + Point memory p; assembly ("memory-safe") { - point := jPoint + p := point } - return point; + return p; } //-------------------------------------------------------------------------- diff --git a/src/signatures/Schnorr.sol b/src/signatures/Schnorr.sol index 8ddd9cc..a9bdbd8 100644 --- a/src/signatures/Schnorr.sol +++ b/src/signatures/Schnorr.sol @@ -197,6 +197,7 @@ library Schnorr { vmed returns (Signature memory) { + // Note that public key derivation fails if secret key invalid. PublicKey memory pk = sk.toPublicKey(); // Derive deterministic nonce ∊ [1, Q). diff --git a/src/signatures/utils/Nonce.sol b/src/signatures/utils/Nonce.sol index b5bd21c..2ffe43d 100644 --- a/src/signatures/utils/Nonce.sol +++ b/src/signatures/utils/Nonce.sol @@ -13,8 +13,7 @@ pragma solidity ^0.8.16; import {Secp256k1, SecretKey} from "../../curves/Secp256k1.sol"; -// TODO: Goal: Library to derive deterministic nonces following RFC 6979. -// +// TODO: Derive deterministic nonces following RFC 6979. // For Rust implementation (used by foundry), see: // - https://github.com/RustCrypto/signatures/blob/master/rfc6979/src/lib.rs#L77 // - https://github.com/RustCrypto/signatures/blob/master/rfc6979/src/lib.rs#L135 @@ -33,8 +32,8 @@ library Nonce { using Nonce for SecretKey; - /// @dev Derives a deterministic nonce from secret key `sk` and message - /// `message`. + /// @dev Derives a deterministic non-zero nonce from secret key `sk` and + /// message `message`. /// /// @dev Note that a nonce is of type uint and not bounded by any field! /// @@ -50,8 +49,8 @@ library Nonce { return sk.deriveNonce(digest); } - /// @dev Derives a deterministic nonce from secret key `sk` and message - /// `message`. + /// @dev Derives a deterministic non-zero nonce from secret key `sk` and + /// hash digest `digest`. /// /// @dev Note that a nonce is of type uint and not bounded to any field! /// diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index 77bd13f..b278018 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -17,20 +17,79 @@ import {console2 as console} from "forge-std/console2.sol"; import {Vm} from "forge-std/Vm.sol"; import {Secp256k1, SecretKey, PublicKey} from "../curves/Secp256k1.sol"; -import {Secp256k1Arithmetic, Point} from "../curves/Secp256k1Arithmetic.sol"; +import { + Secp256k1Arithmetic, + Point, + ProjectivePoint +} from "../curves/Secp256k1Arithmetic.sol"; uint constant SCHEME_ID = 1; +/// @notice +/// /// @custom:field spendPk The spending public key /// @custom:field viewPk The viewing public key + +/** + * @notice StealthMetaAddress encapsulates a receiver's spending and viewing + * public keys from which a [StealthAddress] can be computed. + * + * @dev Stealth meta addresses offer TODO... + * + * @dev Secret keys for stealth addresses derived from a stealth meta address + * can be computed via the spending secret key. The viewing secret key + * can be used to determine whether a tx belongs to the stealth meta + * address. + * + * @custom:example Generate a stealth meta address: + * + * ```solidity + * import {Secp256k1, SecretKey, PublicKey} from "crysol/curves/Secp256k1.sol"; + * import {StealthSecp256k1, StealthMetaAddress} from "crysol/stealth-addresses/StealthSecp256k1.sol"; + * contract Example { + * using Secp256k1 for SecretKey; + * + * // Create spending and viewing secret keys. + * SecretKey spendSk = Secp256k1.newSecretKey(); + * SecretKey viewSk = Secp256k1.newSecretKey(); + * + * // Stealth meta address is their set of public keys. + * StealthMetaAddress memory sma = StealthMetaAddress({ + * spendPk: spendSk.toPublicKey(), + * viewPk: viewSk.toPublicKey() + * }) + * } + * ``` + */ struct StealthMetaAddress { PublicKey spendPk; PublicKey viewPk; } -/// @custom:field recipient -/// @custom:field ephPk The ephemeral public key -/// @custom:field viewTag +/** + * @notice StealthAddress + * + * + * @custom:example Generate a stealth meta address: + * + * ```solidity + * import {Secp256k1, SecretKey, PublicKey} from "crysol/curves/Secp256k1.sol"; + * import {StealthSecp256k1, StealthMetaAddress} from "crysol/stealth-addresses/StealthSecp256k1.sol"; + * contract Example { + * using Secp256k1 for SecretKey; + * + * // Create spending and viewing secret keys. + * SecretKey spendSk = Secp256k1.newSecretKey(); + * SecretKey viewSk = Secp256k1.newSecretKey(); + * + * // Stealth meta address is their set of public keys. + * StealthMetaAddress memory sma = StealthMetaAddress({ + * spendPk: spendSk.toPublicKey(), + * viewPk: viewSk.toPublicKey() + * }) + * } + * ``` + */ struct StealthAddress { address recipient; PublicKey ephPk; @@ -56,6 +115,7 @@ library StealthSecp256k1 { using Secp256k1 for PublicKey; using Secp256k1 for Point; using Secp256k1Arithmetic for Point; + using Secp256k1Arithmetic for ProjectivePoint; // ~~~~~~~ Prelude ~~~~~~~ // forgefmt: disable-start @@ -80,7 +140,7 @@ library StealthSecp256k1 { /// https://github.com/ethereum-lists/chains. /// /// @dev A stealth meta address' string representation is defined as: - /// `st::0x` + /// `st::0x` /// /// @custom:vm vm.toString(bytes) function toString(StealthMetaAddress memory sma, string memory chain) @@ -90,61 +150,32 @@ library StealthSecp256k1 { { string memory prefix = string.concat("st:", chain, ":0x"); - bytes memory spendingKey; - bytes memory viewingKey; + bytes memory spendPkBytes; + bytes memory viewPkBytes; - string memory key; - key = vm.toString(abi.encodePacked(sma.spendPk.x, sma.spendPk.y)); - spendingKey = new bytes(bytes(key).length - 2); - for (uint i = 2; i < bytes(key).length; i++) { - spendingKey[i - 2] = bytes(key)[i]; - } + string memory buffer; - key = vm.toString(abi.encodePacked(sma.viewPk.x, sma.viewPk.y)); - viewingKey = new bytes(bytes(key).length - 2); - for (uint i = 2; i < bytes(key).length; i++) { - viewingKey[i - 2] = bytes(key)[i]; + // Note to remove "0x" prefix. + buffer = vm.toString(sma.spendPk.toBytes()); + spendPkBytes = new bytes(bytes(buffer).length - 2); + for (uint i = 2; i < bytes(buffer).length; i++) { + spendPkBytes[i - 2] = bytes(buffer)[i]; } - return string.concat(prefix, string(spendingKey), string(viewingKey)); - } - - /// @dev Returns stealth meta address `stealthMetaAddress` for chain `chain` - /// as bytes. - /// - /// @dev Note that `chain` should be the chain's short name as defined via - /// https://github.com/ethereum-lists/chains. - /// - /// @dev Provides following encoding: - /// `st::0x` - function toBytes(StealthMetaAddress memory sma, string memory chain) - internal - pure - returns (bytes memory) - { - return bytes.concat(bytes("st:"), bytes(chain), bytes(":0x")); - - bytes memory prefix = bytes(string.concat("st:", chain, ":0x")); - - bytes memory keys = - bytes.concat(sma.spendPk.toBytes(), sma.viewPk.toBytes()); - - return bytes.concat(prefix, keys); - //bytes.concat(bytes("st:"), bytes(chain)); - //bytes memory prefix = - // abi.encodePacked(bytes("st:"), bytes(chain), bytes(":0x")); - - //bytes memory pubKeys = abi.encodePacked( - // stealthMetaAddress.spendingPubKey.toBytes(), - // stealthMetaAddress.viewingPubKey.toBytes() - //); + // Note to remove "0x" prefix. + buffer = vm.toString(sma.viewPk.toBytes()); + viewPkBytes = new bytes(bytes(buffer).length - 2); + for (uint i = 2; i < bytes(buffer).length; i++) { + viewPkBytes[i - 2] = bytes(buffer)[i]; + } - //return abi.encodePacked(prefix, pubKeys); + return string.concat(prefix, string(spendPkBytes), string(viewPkBytes)); } // Stealth Address // TODO: See https://eips.ethereum.org/EIPS/eip-5564#generation---generate-stealth-address-from-stealth-meta-address. + // TODO: Rename to derive? function newStealthAddress(StealthMetaAddress memory sma) internal returns (StealthAddress memory) @@ -153,21 +184,25 @@ library StealthSecp256k1 { SecretKey ephSk = Secp256k1.newSecretKey(); PublicKey memory ephPk = ephSk.toPublicKey(); - console.log("[internal] newStealthAddress: Ephemeral key pair created"); + console.log("[INTERNAL] newStealthAddress: Created ephemeral key pair"); + + // TODO: Move sharedPk stuff into own function? + // Otherwise naming overload. - // Compute shared secret. + // Compute shared secret = [ephSk]viewPk. // forgefmt: disable-next-item PublicKey memory sharedPk = sma.viewPk.intoPoint() .mul(ephSk.asUint()) .intoPublicKey(); console.log( - "[internal] newStealthAddress: Shared secret based public key computed" + "[INTERNAL] newStealthAddress: Computed shared secret's public key" ); // TODO: EIP not exact: sharedSecret must be bounded to field. // TODO: If sharedSecret is zero, loop with new ephemeral key! // Currently reverts. + // => Should be negligible propability though. SecretKey sharedSecretSk = Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); @@ -180,8 +215,9 @@ library StealthSecp256k1 { // Compute recipients public key. // forgefmt: disable-next-item PublicKey memory recipientPk = sma.spendPk + .toProjectivePoint() + .add(sharedSecretPk.toProjectivePoint()) .intoPoint() - .add(sharedSecretPk.intoPoint()) .intoPublicKey(); // Derive recipients address from their public key. diff --git a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol index d4763c5..89cd44b 100644 --- a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol @@ -119,7 +119,7 @@ contract Secp256k1ArithmeticTest is Test { function test_Point_equals_DoesNotRevert_IfPointsNotOnCurve( Point memory point1, Point memory point2 - ) public { + ) public view { wrapper.equals(point1, point2); } @@ -144,6 +144,29 @@ contract Secp256k1ArithmeticTest is Test { //-------------------------------------------------------------------------- // Test: Projective Point + //---------------------------------- + // Test: Arithmetic + + // TODO: Get vectors from Paul Miller's noble curve projects. + function test_ProjectivePoint_add() public {} + + // TODO: Needs to use wrapper :( + function test_ProjectivePoint_add_Identity() public { + ProjectivePoint memory g = Secp256k1.G().toProjectivePoint(); + ProjectivePoint memory id = + Secp256k1Arithmetic.Identity().toProjectivePoint(); + + Point memory sum; + + sum = g.add(id).intoPoint(); + assertEq(sum.x, g.x); + assertEq(sum.y, g.y); + + sum = id.add(g).intoPoint(); + assertEq(sum.x, g.x); + assertEq(sum.y, g.y); + } + //---------------------------------- // Test: Type Conversion From 4509af7c507c0554cabf5d3bba37e4a8f98c21c3 Mon Sep 17 00:00:00 2001 From: pascal Date: Fri, 5 Jan 2024 16:25:49 +0100 Subject: [PATCH 11/11] ... --- src/curves/Secp256k1.sol | 2 +- src/curves/Secp256k1Arithmetic.sol | 417 +++++++----------- src/stealth-addresses/StealthSecp256k1.sol | 33 +- test/curves/secp256k1/Secp256k1.t.sol | 40 ++ .../secp256k1/Secp256k1Arithmetic.t.sol | 211 ++++++++- .../curves/Secp256k1Arithmetic/addtion.json | 11 + 6 files changed, 422 insertions(+), 292 deletions(-) create mode 100644 test/test-vectors/curves/Secp256k1Arithmetic/addtion.json diff --git a/src/curves/Secp256k1.sol b/src/curves/Secp256k1.sol index 507dc60..ac9c391 100644 --- a/src/curves/Secp256k1.sol +++ b/src/curves/Secp256k1.sol @@ -177,7 +177,7 @@ library Secp256k1 { /// - Scalar not in [1, Q) function secretKeyFromUint(uint scalar) internal pure returns (SecretKey) { if (scalar == 0 || scalar >= Q) { - revert("InvalidScalar()"); + revert("ScalarInvalid()"); } return SecretKey.wrap(scalar); diff --git a/src/curves/Secp256k1Arithmetic.sol b/src/curves/Secp256k1Arithmetic.sol index d635589..cc25ce0 100644 --- a/src/curves/Secp256k1Arithmetic.sol +++ b/src/curves/Secp256k1Arithmetic.sol @@ -53,6 +53,11 @@ library Secp256k1Arithmetic { using Secp256k1Arithmetic for Point; using Secp256k1Arithmetic for ProjectivePoint; + //-------------------------------------------------------------------------- + // Optimization Constants + + uint private constant B3 = mulmod(B, 3, P); + //-------------------------------------------------------------------------- // Secp256k1 Constants // @@ -157,102 +162,10 @@ library Secp256k1Arithmetic { pure returns (bool) { + // TODO: (point.x ^ other.y) | (point.y ^ other.y) ? return (point.x == other.x) && (point.y == other.y); } - //---------------------------------- - // Arithmetic - - // TODO: Provide add(Projective, Projective) -> Projective for simple one-time additions. - // Users needs to convert intoPoint() to realize expense. - // Use complete addition formula Alg 7 from https://eprint.iacr.org/2015/1060.pdf. - // - // TODO: Provide add(Projective, Affine) -> Projective for sequential additions. - // Users needs to convert intoPoint() to realize expense. - // Use complete addition formula Alg 8 from https://eprint.iacr.org/2015/1060.pdf. - // => Only if cheaper though, otherwise same as Affine->Projective is constant operation. - // => Therefore not important for now. Only optimization issue -> After initial release. - - /// @dev Returns the sum of points `point` and `other` as new point. - /// - /// @dev TODO Note about performance. intoPoint() conversion is expensive. - /// Also created new point struct in memory. - /// - /// @dev Reverts if: - /// - Point not on curve - function add(Point memory point, Point memory other) - internal - pure - returns (Point memory) - { - // Revert if any point not on curve. - if (!point.isOnCurve()) { - revert("PointNotOnCurve(point)"); - } - if (!other.isOnCurve()) { - revert("PointNotOnCurve(other)"); - } - - // Catch addition with identity. - if (point.isIdentity()) { - return other; - } - if (other.isIdentity()) { - return point; - } - - // Perform addition in projective coordinates. - ProjectivePoint memory jSum; - ProjectivePoint memory jPoint = point.toProjectivePoint(); - - if (point.equals(other)) { - // point = other → sum = point + point - jSum = _jDouble(jPoint); - } else { - // point != other → sum = point + other - jSum = _jAdd(jPoint, other.toProjectivePoint()); - } - - // TODO: Don't convert. MUST be done by user! - // Return sum as Affine point. - // Note that conversion is expensive! - return jSum.intoPoint(); - } - - /// @dev Returns a new point being the product of point `point` and scalar - /// `scalar`. - /// - /// @dev TODO Note about performance. intoPoint() conversion is expensive. - /// Also created new point struct in memory. - /// - /// @dev Reverts if: - /// - Point not on curve - function mul(Point memory point, uint scalar) - internal - pure - returns (Point memory) - { - if (!point.isOnCurve()) { - revert("PointNotOnCurve(point)"); - } - - // Catch muliplication with identity or scalar of zero. - if (point.isIdentity() || scalar == 0) { - return Identity(); - } - - // Perform multiplicatino in projective coordinates. - ProjectivePoint memory jProduct; - ProjectivePoint memory jPoint = point.toProjectivePoint(); - - jProduct = _jMul(jPoint, scalar); - - // TODO: Don't convert. Must be done by user! - // Return product as affine point. - // Note that conversion is expensive! - return jProduct.intoPoint(); - } - //-------------------------------------------------------------------------- // Projective Point @@ -268,22 +181,28 @@ library Secp256k1Arithmetic { returns (bool) { return point.x == 0 && point.y == 1 && point.z == 0; - // return point.y == 1 && (point.x | point.y) == 0; + // TODO: return point.y == 1 && (point.x | point.y) == 0; } //---------------------------------- // Arithmetic - /// @dev + // TODO: Add negate() function. + + // TODO: What about intoAdd()? Saves memory allocations... + /// @dev Returns the sum of projective points `point` and `other` as new + /// projective point. + /// + /// @dev Assumes: + /// - Points are on curve function add(ProjectivePoint memory point, ProjectivePoint memory other) internal pure returns (ProjectivePoint memory) { + /* // Uses complete addition formula from Renes-Costello-Batina 2015. - // See https://eprint.iacr.org/2015/1060.pdf Alg 7. - - // forgefmt: disable-start + // See https://eprint.iacr.org/2015/1060.pdf Alg 1. if (point.isIdentity()) { return other; @@ -292,9 +211,11 @@ library Secp256k1Arithmetic { return point; } + // forgefmt: disable-start + // Inputs: P = (x1, y1, z1), Q = (x2, y2, z2) - uint x1 = point.x; uint x2 = other.x; - uint y1 = point.y; uint y2 = other.y; + uint x1 = point.x; uint x2 = other.x; + uint y1 = point.y; uint y2 = other.y; uint z1 = point.z; uint z2 = other.z; // Output: P + Q = (x3, y3, z3) @@ -302,53 +223,163 @@ library Secp256k1Arithmetic { uint y3; uint z3; - // Constants: TODO Make constant - uint b3 = mulmod(3, B, P); + // Constants used: + // - B3 = mulmod(B, 3, P) // Variables: - uint t0; uint t1; uint t2; uint t3; uint t4; + { + uint t0; uint t1; uint t2; uint t3; uint t4; uint t5; // Alg: - // Note that x - y = (Q - y) + x (mod P) t0 = mulmod(x1, x2, P); t1 = mulmod(y1, y2, P); t2 = mulmod(z1, z2, P); t3 = addmod(x1, y1, P); - t4 = addmod(x2, y2, P); + t4 = mulmod(x2, y2, P); // step 5 + t3 = mulmod(t3, t4, P); + t4 = addmod(t0, t1, P); + t3 = addmod(t3, P - t4, P); + t4 = addmod(x1, z1, P); + t5 = addmod(x2, z2, P); // step 10 + t4 = mulmod(t4, t5, P); + t5 = addmod(t0, t2, P); + t4 = addmod(t4, P - t5, P); + t5 = addmod(y1, z1, P); + x3 = addmod(y2, z2, P); // step 15 + t5 = mulmod(t5, x3, P); + x3 = addmod(t1, t2, P); + t5 = addmod(t5, P - x3, P); + z3 = mulmod(A, t4, P); + x3 = mulmod(B3, t2, P); // step 20 + z3 = addmod(x3, z3, P); + x3 = addmod(t1, P - z3, P); + z3 = addmod(t1, z3, P); + y3 = mulmod(x3, z3, P); + t1 = addmod(t0, t0, P); // step 25 + t1 = addmod(t1, t0, P); + t2 = mulmod(A, t2, P); + t4 = mulmod(B3, t4, P); + t1 = addmod(t1, t2, P); + t2 = addmod(t0, P - t2, P); // step 30 + t2 = mulmod(A, t2, P); + t4 = addmod(t4, t2, P); + t0 = mulmod(t1, t4, P); + y3 = addmod(y3, t0, P); + t0 = mulmod(t5, t4, P); // step 35 + x3 = mulmod(t3, x3, P); + x3 = addmod(x3, P - t0, P); + t0 = mulmod(t3, t1, P); + z3 = mulmod(t5, z3, P); + z3 = addmod(z3, t0, P); // step 40 + } + // forgefmt: disable-end + + return ProjectivePoint(x3, y3, z3); + */ + + // Uses complete addition formula from Renes-Costello-Batina 2015. + // See https://eprint.iacr.org/2015/1060.pdf Alg 7. + // + // TODO: This implementation can be optimized. + // See for example https://github.com/RustCrypto/elliptic-curves/blob/master/k256/src/arithmetic/projective.rs#L96. + + // TODO: Can be removed... Should be? + //if (point.isIdentity()) { + // return other; + //} + //if (other.isIdentity()) { + // return point; + //} + + // forgefmt: disable-start + + // Inputs: P = (x1, y1, z1), Q = (x2, y2, z2) + uint x1 = point.x; uint x2 = other.x; + uint y1 = point.y; uint y2 = other.y; + uint z1 = point.z; uint z2 = other.z; + + // Output: (x3, y3, z3) = P + Q + uint x3; + uint y3; + uint z3; + + // Constants used: + // - B3 = mulmod(B, 3, P) + + // Variables: + uint t0; uint t1; uint t2; uint t3; uint t4; + + // Alg: + // Note that x - y = x + (P - y) (mod P) + t0 = mulmod(x1, x2, P); // Step 1 + t1 = mulmod(y1, y2, P); + t2 = mulmod(z1, z2, P); + t3 = addmod(x1, y1, P); + t4 = addmod(x2, y2, P); // Step 5 t3 = mulmod(t3, t4, P); t4 = addmod(t0, t1, P); - unchecked { t3 = addmod(Q - t4, t3, P); } + unchecked { t3 = addmod(t3, P - t4, P); } t4 = addmod(y1, z1, P); - x3 = addmod(y2, z2, P); + x3 = addmod(y2, z2, P); // Step 10 t4 = mulmod(t4, x3, P); x3 = addmod(t1, t2, P); - unchecked { t4 = addmod(Q - x3, t4, P); } + unchecked { t4 = addmod(t4, P - x3, P); } x3 = addmod(x1, z1, P); - y3 = addmod(x2, z2, P); + y3 = addmod(x2, z2, P); // Step 15 x3 = mulmod(x3, y3, P); y3 = addmod(t0, t2, P); - unchecked { y3 = addmod(Q - y3, x3, P); } + unchecked { y3 = addmod(x3, P - y3, P); } x3 = addmod(t0, t0, P); - t0 = addmod(x3, t0, P); - t2 = mulmod(b3, t2, P); + t0 = addmod(x3, t0, P); // Step 20 + t2 = mulmod(B3, t2, P); z3 = addmod(t1, t2, P); - unchecked { t1 = addmod(Q - t2, t1, P); } - y3 = mulmod(b3, y3, P); - x3 = mulmod(t4, y3, P); + unchecked { t1 = addmod(t1, P - t2, P); } + y3 = mulmod(B3, y3, P); + x3 = mulmod(t4, y3, P); // Step 25 t2 = mulmod(t3, t1, P); - unchecked { x3 = addmod(Q - x3, t2, P); } + unchecked { x3 = addmod(t2, P - x3, P); } y3 = mulmod(y3, t0, P); t1 = mulmod(t1, z3, P); - y3 = addmod(t1, y3, P); - t0 = mulmod(t0, t2, P); + y3 = addmod(t1, y3, P); // Step 30 + t0 = mulmod(t0, t3, P); z3 = mulmod(z3, t4, P); z3 = addmod(z3, t0, P); // forgefmt: disable-end - // Return as ProjectivePoint. return ProjectivePoint(x3, y3, z3); } + /// @dev Returns the product of projective point `point` and scalar `scalar`. + /// + /// @dev Assumes: + /// - Points are on curve + function mul(ProjectivePoint memory point, uint scalar) + internal + pure + returns (ProjectivePoint memory) + { + // TODO: Should revert if scalar not in [0, Q)? + + // Catch multiplication with identity or scalar of zero. + if (point.isIdentity() || scalar == 0) { + // TODO: Need Identity()(ProjectivePoint) function. + return ProjectivePoint(0, 1, 0); + } + + ProjectivePoint memory copy = point; + ProjectivePoint memory product = ProjectivePoint(0, 0, 0); + + while (scalar != 0) { + if (scalar & 1 == 1) { + product = product.add(copy); + } + scalar >>= 1; // Divide by 2. + copy = copy.add(copy); + } + + return product; + } + //-------------------------------------------------------------------------- // (De)Serialization @@ -484,8 +515,8 @@ library Secp256k1Arithmetic { /// @dev Note that there is no modular inverse for zero. /// /// @dev Reverts if: - /// - x not in [1, P) - /// - xInv not in [1, P) + /// - x not in [0, P) + /// - xInv not in [0, P) function areModularInverse(uint x, uint xInv) internal pure @@ -500,148 +531,4 @@ library Secp256k1Arithmetic { return mulmod(x, xInv, P) == 1; } - - //-------------------------------------------------------------------------- - // Private Functions - - //---------------------------------- - // Projective Point - // - // Functionality stolen from Jordi Baylina's [ecsol](https://github.com/jbaylina/ecsol/blob/c2256afad126b7500e6f879a9369b100e47d435d/ec.sol). - - /// @dev Assumptions: - /// - Each point is on the curve - /// - No point is the point at infinity - function _jAdd(ProjectivePoint memory jPoint, ProjectivePoint memory other) - private - pure - returns (ProjectivePoint memory) - { - // TODO: Define identity for ProjectPoint. - // assert(!jPoint.isIdentity() && !other.isIdentity()); - - ProjectivePoint memory sum; - - uint l; - uint lz; - uint da; - uint db; - - if (jPoint.x == other.x && jPoint.y == other.y) { - (l, lz) = _mul(jPoint.x, jPoint.z, jPoint.x, jPoint.z); - (l, lz) = _mul(l, lz, 3, 1); - (l, lz) = _add(l, lz, A, 1); - - (da, db) = _mul(jPoint.y, jPoint.z, 2, 1); - } else { - (l, lz) = _sub(other.y, other.z, jPoint.y, jPoint.z); - (da, db) = _sub(other.x, other.z, jPoint.x, jPoint.z); - } - - (l, lz) = _div(l, lz, da, db); - - (sum.x, da) = _mul(l, lz, l, lz); - (sum.x, da) = _sub(sum.x, da, jPoint.x, jPoint.z); - (sum.x, da) = _sub(sum.x, da, other.x, other.z); - - (sum.y, db) = _sub(jPoint.x, jPoint.z, sum.x, da); - (sum.y, db) = _mul(sum.y, db, l, lz); - (sum.y, db) = _sub(sum.y, db, jPoint.y, jPoint.z); - - if (da != db) { - sum.x = mulmod(sum.x, db, P); - sum.y = mulmod(sum.y, da, P); - sum.z = mulmod(da, db, P); - } else { - sum.z = da; - } - - return sum; - } - - /// @dev Assumptions: - /// - Point is on the curve - /// - Point is not the point at infinity - function _jDouble(ProjectivePoint memory jPoint) - private - pure - returns (ProjectivePoint memory) - { - // TODO: There are faster doubling formulas. - return _jAdd(jPoint, jPoint); - } - - /// @dev Assumptions: - /// - Point is on the curve - /// - Point is not the point at infinity - /// - Scalar is not zero - function _jMul(ProjectivePoint memory jPoint, uint scalar) - private - pure - returns (ProjectivePoint memory) - { - // TODO: Define identity for ProjectPoint. - // assert(!jPoint.isIdentity()); - assert(scalar != 0); - - ProjectivePoint memory copy = jPoint; - ProjectivePoint memory product = ZeroPoint().toProjectivePoint(); - - while (scalar != 0) { - if (scalar & 1 == 1) { - product = _jAdd(product, copy); - } - scalar = scalar >> 1; // Divide by 2. - copy = _jDouble(copy); - } - - return product; - } - - //---------------------------------- - // Helpers - - function _add(uint x1, uint z1, uint x2, uint z2) - private - pure - returns (uint, uint) - { - uint x3 = addmod(mulmod(z2, x1, P), mulmod(x2, z1, P), P); - uint z3 = mulmod(z1, z2, P); - - return (x3, z3); - } - - function _sub(uint x1, uint z1, uint x2, uint z2) - private - pure - returns (uint, uint) - { - uint x3 = addmod(mulmod(z2, x1, P), mulmod(P - x2, z1, P), P); - uint z3 = mulmod(z1, z2, P); - - return (x3, z3); - } - - function _mul(uint x1, uint z1, uint x2, uint z2) - private - pure - returns (uint, uint) - { - uint x3 = mulmod(x1, x2, P); - uint z3 = mulmod(z1, z2, P); - - return (x3, z3); - } - - function _div(uint x1, uint z1, uint x2, uint z2) - private - pure - returns (uint, uint) - { - uint x3 = mulmod(x1, z2, P); - uint z3 = mulmod(z1, x2, P); - - return (x3, z3); - } } diff --git a/src/stealth-addresses/StealthSecp256k1.sol b/src/stealth-addresses/StealthSecp256k1.sol index b278018..4e1df5f 100644 --- a/src/stealth-addresses/StealthSecp256k1.sol +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -25,11 +25,6 @@ import { uint constant SCHEME_ID = 1; -/// @notice -/// -/// @custom:field spendPk The spending public key -/// @custom:field viewPk The viewing public key - /** * @notice StealthMetaAddress encapsulates a receiver's spending and viewing * public keys from which a [StealthAddress] can be computed. @@ -57,7 +52,7 @@ uint constant SCHEME_ID = 1; * StealthMetaAddress memory sma = StealthMetaAddress({ * spendPk: spendSk.toPublicKey(), * viewPk: viewSk.toPublicKey() - * }) + * }); * } * ``` */ @@ -142,7 +137,7 @@ library StealthSecp256k1 { /// @dev A stealth meta address' string representation is defined as: /// `st::0x` /// - /// @custom:vm vm.toString(bytes) + /// @custom:vm vm.toString(bytes)(string) function toString(StealthMetaAddress memory sma, string memory chain) internal vmed @@ -191,9 +186,11 @@ library StealthSecp256k1 { // Compute shared secret = [ephSk]viewPk. // forgefmt: disable-next-item - PublicKey memory sharedPk = sma.viewPk.intoPoint() - .mul(ephSk.asUint()) - .intoPublicKey(); + PublicKey memory sharedPk = sma.viewPk + .toProjectivePoint() + .mul(ephSk.asUint()) + .intoPoint() + .intoPublicKey(); console.log( "[INTERNAL] newStealthAddress: Computed shared secret's public key" @@ -236,10 +233,10 @@ library StealthSecp256k1 { ) internal returns (bool) { // Compute shared public key. // forgefmt: disable-next-item - PublicKey memory sharedPk = sa.ephPk - .intoPoint() - .mul(viewSk.asUint()) - .intoPublicKey(); + PublicKey memory sharedPk = sa.ephPk.toProjectivePoint() + .mul(viewSk.asUint()) + .intoPoint() + .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bound to field. SecretKey sharedSecretSk = @@ -258,8 +255,9 @@ library StealthSecp256k1 { // Compute recipients public key. // forgefmt: disable-next-item - PublicKey memory recipientPk = spendPk.intoPoint() - .add(sharedSecretPk.intoPoint()) + PublicKey memory recipientPk = spendPk.toProjectivePoint() + .add(sharedSecretPk.toProjectivePoint()) + .intoPoint() .intoPublicKey(); // Derive recipients address from their public key. @@ -279,8 +277,9 @@ library StealthSecp256k1 { ) internal returns (SecretKey) { // Compute shared secret public key. // forgefmt: disable-next-item - PublicKey memory sharedPk = sa.ephPk.intoPoint() + PublicKey memory sharedPk = sa.ephPk.toProjectivePoint() .mul(viewSk.asUint()) + .intoPoint() .intoPublicKey(); // TODO: EIP not exact: sharedSecret must be bounded to field. diff --git a/test/curves/secp256k1/Secp256k1.t.sol b/test/curves/secp256k1/Secp256k1.t.sol index 9e98a77..3f1d98f 100644 --- a/test/curves/secp256k1/Secp256k1.t.sol +++ b/test/curves/secp256k1/Secp256k1.t.sol @@ -19,6 +19,9 @@ contract Secp256k1Test is Test { using Secp256k1 for PublicKey; using Secp256k1 for Point; + using Secp256k1Arithmetic for Point; + using Secp256k1Arithmetic for ProjectivePoint; + // Uncompressed Generator G. // Copied from [Sec 2 v2]. bytes constant GENERATOR_ENCODED_UNCOMPRESSED = @@ -97,6 +100,33 @@ contract Secp256k1Test is Test { wrapper.toPublicKey(sk); } + // -- secretKeyFromUint + + function testFuzz_SecretKey_secretKeyFromUint(uint scalar) public { + vm.assume(scalar != 0 && scalar < Secp256k1.Q); + + SecretKey sk = wrapper.secretKeyFromUint(scalar); + + assertEq(sk.asUint(), scalar); + } + + function testFuzz_SecretKey_secretKeyFromUint_RevertsIf_ScalarInvalid( + uint scalar + ) public { + vm.assume(scalar == 0 || scalar >= Secp256k1.Q); + + vm.expectRevert("ScalarInvalid()"); + wrapper.secretKeyFromUint(scalar); + } + + // -- asUint + + function testFuzz_SecretKey_asUint(SecretKey sk) public { + uint scalar = SecretKey.unwrap(sk); + + assertEq(sk.asUint(), scalar); + } + //-------------------------------------------------------------------------- // Test: Public Key @@ -184,6 +214,8 @@ contract Secp256k1Test is Test { } function testFuzz_PublicKey_toProjectivePoint(PublicKey memory pk) public { + vm.assume(!pk.intoPoint().isIdentity()); + ProjectivePoint memory jPoint = wrapper.toProjectivePoint(pk); assertEq(jPoint.x, pk.x); @@ -191,6 +223,14 @@ contract Secp256k1Test is Test { assertEq(jPoint.z, 1); } + function test_PublicKey_toProjectivePoint_Identity() public { + PublicKey memory pk = Secp256k1Arithmetic.Identity().intoPublicKey(); + + ProjectivePoint memory point = wrapper.toProjectivePoint(pk); + + assertTrue(point.isIdentity()); + } + //-------------------------------------------------------------------------- // Test: (De)Serialization diff --git a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol index 89cd44b..aa807de 100644 --- a/test/curves/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/curves/secp256k1/Secp256k1Arithmetic.t.sol @@ -123,9 +123,6 @@ contract Secp256k1ArithmeticTest is Test { wrapper.equals(point1, point2); } - //---------------------------------- - // TODO: Test: Arithmetic - //---------------------------------- // Test: Type Conversion @@ -144,25 +141,184 @@ contract Secp256k1ArithmeticTest is Test { //-------------------------------------------------------------------------- // Test: Projective Point + // -- isIdentity + + function testFuzz_ProjectivePoint_isIdentity(ProjectivePoint memory point) + public + { + if (point.x == 0 && point.y == 1 && point.z == 0) { + assertTrue(wrapper.isIdentity(point)); + } else { + assertFalse(wrapper.isIdentity(point)); + } + } + //---------------------------------- // Test: Arithmetic - // TODO: Get vectors from Paul Miller's noble curve projects. - function test_ProjectivePoint_add() public {} + /* + function test_ProjectivePoint_add() public { + ( + hex!("79BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798"), + hex!("483ADA7726A3C4655DA4FBFC0E1108A8FD17B448A68554199C47D08FFB10D4B8"), + ), + ( + hex!("C6047F9441ED7D6D3045406E95C07CD85C778E4B8CEF3CA7ABAC09B95C709EE5"), + hex!("1AE168FEA63DC339A3C58419466CEAEEF7F632653266D0E1236431A950CFE52A"), + ), + ( + hex!("F9308A019258C31049344F85F89D5229B531C845836F99B08601F113BCE036F9"), + hex!("388F7B0F632DE8140FE337E62A37F3566500A99934C2231B6CB9FD7584B8E672"), + ), + ( + hex!("E493DBF1C10D80F3581E4904930B1404CC6C13900EE0758474FA94ABE8C4CD13"), + hex!("51ED993EA0D455B75642E2098EA51448D967AE33BFBDFE40CFE97BDC47739922"), + ), + ( + hex!("2F8BDE4D1A07209355B4A7250A5C5128E88B84BDDC619AB7CBA8D569B240EFE4"), + hex!("D8AC222636E5E3D6D4DBA9DDA6C9C426F788271BAB0D6840DCA87D3AA6AC62D6"), + ), + ( + hex!("FFF97BD5755EEEA420453A14355235D382F6472F8568A18B2F057A1460297556"), + hex!("AE12777AACFBB620F3BE96017F45C560DE80F0F6518FE4A03C870C36B075F297"), + ), + ( + hex!("5CBDF0646E5DB4EAA398F365F2EA7A0E3D419B7E0330E39CE92BDDEDCAC4F9BC"), + hex!("6AEBCA40BA255960A3178D6D861A54DBA813D0B813FDE7B5A5082628087264DA"), + ), + ( + hex!("2F01E5E15CCA351DAFF3843FB70F3C2F0A1BDD05E5AF888A67784EF3E10A2A01"), + hex!("5C4DA8A741539949293D082A132D13B4C2E213D6BA5B7617B5DA2CB76CBDE904"), + ), + ( + hex!("ACD484E2F0C7F65309AD178A9F559ABDE09796974C57E714C35F110DFC27CCBE"), + hex!("CC338921B0A7D9FD64380971763B61E9ADD888A4375F8E0F05CC262AC64F9C37"), + ), + ( + hex!("A0434D9E47F3C86235477C7B1AE6AE5D3442D49B1943C2B752A68E2A47E247C7"), + hex!("893ABA425419BC27A3B6C7E693A24C696F794C2ED877A1593CBEE53B037368D7"), + ), + ( + hex!("774AE7F858A9411E5EF4246B70C65AAC5649980BE5C17891BBEC17895DA008CB"), + hex!("D984A032EB6B5E190243DD56D7B7B365372DB1E2DFF9D6A8301D74C9C953C61B"), + ), + ( + hex!("D01115D548E7561B15C38F004D734633687CF4419620095BC5B0F47070AFE85A"), + hex!("A9F34FFDC815E0D7A8B64537E17BD81579238C5DD9A86D526B051B13F4062327"), + ), + ( + hex!("F28773C2D975288BC7D1D205C3748651B075FBC6610E58CDDEEDDF8F19405AA8"), + hex!("0AB0902E8D880A89758212EB65CDAF473A1A06DA521FA91F29B5CB52DB03ED81"), + ), + ( + hex!("499FDF9E895E719CFD64E67F07D38E3226AA7B63678949E6E49B241A60E823E4"), + hex!("CAC2F6C4B54E855190F044E4A7B3D464464279C27A3F95BCC65F40D403A13F5B"), + ), + ( + hex!("D7924D4F7D43EA965A465AE3095FF41131E5946F3C85F79E44ADBCF8E27E080E"), + hex!("581E2872A86C72A683842EC228CC6DEFEA40AF2BD896D3A5C504DC9FF6A26B58"), + ), + ( + hex!("E60FCE93B59E9EC53011AABC21C23E97B2A31369B87A5AE9C44EE89E2A6DEC0A"), + hex!("F7E3507399E595929DB99F34F57937101296891E44D23F0BE1F32CCE69616821"), + ), + ( + hex!("DEFDEA4CDB677750A420FEE807EACF21EB9898AE79B9768766E4FAA04A2D4A34"), + hex!("4211AB0694635168E997B0EAD2A93DAECED1F4A04A95C0F6CFB199F69E56EB77"), + ), + ( + hex!("5601570CB47F238D2B0286DB4A990FA0F3BA28D1A319F5E7CF55C2A2444DA7CC"), + hex!("C136C1DC0CBEB930E9E298043589351D81D8E0BC736AE2A1F5192E5E8B061D58"), + ), + ( + hex!("2B4EA0A797A443D293EF5CFF444F4979F06ACFEBD7E86D277475656138385B6C"), + hex!("85E89BC037945D93B343083B5A1C86131A01F60C50269763B570C854E5C09B7A"), + ), + ( + hex!("4CE119C96E2FA357200B559B2F7DD5A5F02D5290AFF74B03F3E471B273211C97"), + hex!("12BA26DCB10EC1625DA61FA10A844C676162948271D96967450288EE9233DC3A"), + ), + } + */ + + function test_ProjectivePoint_add() public { + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); + + // want = [2]G + SecretKey sk = Secp256k1.secretKeyFromUint(2); + Point memory want = sk.toPublicKey().intoPoint(); + + // got: G + G + ProjectivePoint memory jPoint = wrapper.add(g, g); + console.log("jPoint.x", jPoint.x); + console.log("jPoint.y", jPoint.y); + console.log("jPoint.z", jPoint.z); + + Point memory got = wrapper.add(g, g).intoPoint(); + + // Want: + // x: 89565891926547004231252920425935692360644145829622209833684329913297188986597 + // y: 12158399299693830322967808612713398636155367887041628176798871954788371653930 + + // Alg 1: + // x: 10962303011661563909760120580420572844268442539981607763544518030530584249896 + // y: 73466120800335599957096923879044237072732953459294722407059852471027259622578 + // + // jPoint.x 13181156486935683610805726302064329274717629062639299519950901153253244018254 + // jPoint.y 39155707150128334349216371677407456506802956851096117747929288260567018884059 < + // jPoint.z 93461060555196532511955904293955655567833845947013025069247287831448311466323 + + // Alg 7: + // x: 87391808355972582617912962196687600089218617032645942978517463571946182934760 + // y: 59798459239490663731683313163756213699029087070685955441295759628293051219517 + // + // jPoint.x 110383685576993659245168857245613307344564578195757623090394588386385391034312 + // jPoint.y 39155707150128334349216371677407456506802956851096117747929288260567018884059 < + // jPoint.z 112386024462437979217642839804619380985487717678471186887195924032703633398313 + + assertEq(want.x, got.x); + assertEq(want.y, got.y); + } + + /* + function testFuzz_ProjectivePoint_add_Generator(SecretKey sk) { + public + { + //vm.assume(sk.isValid()); + //vm.assume(sk.asUint() < 100); + + SecretKey sk = Secp256k1.secretKeyFromUint(2); + + Point memory want = sk.toPublicKey().intoPoint(); + + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); + ProjectivePoint memory id = + Secp256k1Arithmetic.Identity().toProjectivePoint(); + + ProjectivePoint memory sum = id; + for (uint i; i < sk.asUint(); i++) { + sum = sum.add(g); + } + + Point memory got = sum.intoPoint(); + + assertEq(want.x, got.x); + assertEq(want.y, got.y); + } + */ - // TODO: Needs to use wrapper :( function test_ProjectivePoint_add_Identity() public { - ProjectivePoint memory g = Secp256k1.G().toProjectivePoint(); + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); ProjectivePoint memory id = Secp256k1Arithmetic.Identity().toProjectivePoint(); Point memory sum; - sum = g.add(id).intoPoint(); + sum = wrapper.add(g, id).intoPoint(); assertEq(sum.x, g.x); assertEq(sum.y, g.y); - sum = id.add(g).intoPoint(); + sum = wrapper.add(id, g).intoPoint(); assertEq(sum.x, g.x); assertEq(sum.y, g.y); } @@ -182,6 +338,13 @@ contract Secp256k1ArithmeticTest is Test { assertEq(want.y, got.y); } + function test_ProjectivePoint_intoPoint_IsIdentityIfIdentity() public { + // TODO: Make Identity()(ProjectivePoint) function! + ProjectivePoint memory id = ProjectivePoint(0, 1, 0); + + assertTrue(wrapper.intoPoint(id).isIdentity()); + } + //-------------------------------------------------------------------------- // Test: Utils @@ -307,6 +470,36 @@ contract Secp256k1ArithmeticWrapper { return point.equals(other); } + //-------------------------------------------------------------------------- + // Projective Point + + function isIdentity(ProjectivePoint memory point) + public + pure + returns (bool) + { + return point.isIdentity(); + } + + //---------------------------------- + // Arithmetic + + function add(ProjectivePoint memory point, ProjectivePoint memory other) + public + pure + returns (ProjectivePoint memory) + { + return point.add(other); + } + + function mul(ProjectivePoint memory point, uint scalar) + public + pure + returns (ProjectivePoint memory) + { + return point.mul(scalar); + } + //-------------------------------------------------------------------------- // (De)Serialization diff --git a/test/test-vectors/curves/Secp256k1Arithmetic/addtion.json b/test/test-vectors/curves/Secp256k1Arithmetic/addtion.json new file mode 100644 index 0000000..64ee0df --- /dev/null +++ b/test/test-vectors/curves/Secp256k1Arithmetic/addtion.json @@ -0,0 +1,11 @@ +{ + "title": "Secp256k1 addition test vectors", + "description": "Provides the result of repeated addition of the generator", + "source": "Stolen from RustCrypto's k256 crate", + "vectors": [ + { + "x": "55066263022277343669578718895168534326250603453777594175500187360389116729240", + "y": "" + } + ] +}