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 new file mode 100644 index 0000000..f8f7f8d --- /dev/null +++ b/examples/stealth-addresses/StealthSecp256k1.sol @@ -0,0 +1,118 @@ +// SPDX-License-Identifier: MIT +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, + StealthMetaAddress, + SCHEME_ID +} from "src/stealth-addresses/StealthSecp256k1.sol"; +import { + IERC5564Announcer, + ERC5564Announcer +} from "src/stealth-addresses/ERC5564Announcer.sol"; + +contract StealthSecp256k1Example is Script { + using StealthSecp256k1 for SecretKey; + using StealthSecp256k1 for StealthAddress; + using StealthSecp256k1 for StealthMetaAddress; + + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + + function run() public { + // Sender key pair. + SecretKey senderSk = Secp256k1.newSecretKey(); + PublicKey memory senderPk = senderSk.toPublicKey(); + logSender("Created key pair"); + + // Receiver key pairs consist of spending and viewing key pairs. + 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. + StealthMetaAddress memory receiverSma = StealthMetaAddress({ + spendPk: receiverSpendPk, + viewPk: receiverViewPk + }); + logReceiver( + string.concat( + "Created Ethereum stealth meta address: ", receiverSma.toString("eth") + ) + ); + + // Sender creates stealth address from receiver's stealth meta address. + // TODO: receiver's stealh address must be argument for function, not + // an object to call a function on. + StealthAddress memory stealth = receiverSma.newStealthAddress(); + logSender("Created stealth address from receiver's stealth meta address"); + + // Sender sends ETH to stealth. + 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. + vm.prank(senderPk.toAddress()); + announcer.announce({ + schemeId: SCHEME_ID, + stealthAddress: stealth.recipient, + 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. + require( + 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"); + 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( + 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 e58136b..099616c 100644 --- a/foundry.toml +++ b/foundry.toml @@ -1,13 +1,13 @@ [profile.default] src = 'src' out = 'out' -libs = ['lib', 'spec', 'examples', 'unsafe'] +libs = ['lib', 'examples', 'unsafe'] 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..ac9c391 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). * @@ -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 bfe52cb..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 // @@ -134,7 +139,7 @@ library Secp256k1Arithmetic { } 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); @@ -151,10 +156,229 @@ library Secp256k1Arithmetic { return point.y & 1; } + /// @dev Returns whether point `point` equals point `other`. + function equals(Point memory point, Point memory other) + internal + pure + returns (bool) + { + // TODO: (point.x ^ other.y) | (point.y ^ other.y) ? + return (point.x == other.x) && (point.y == other.y); + } + //-------------------------------------------------------------------------- // Projective Point - // - // Coming soon... + + /// @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; + // TODO: return point.y == 1 && (point.x | point.y) == 0; + } + + //---------------------------------- + // Arithmetic + + // 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 1. + + 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: P + Q = (x3, y3, z3) + uint x3; + uint y3; + uint z3; + + // Constants used: + // - B3 = mulmod(B, 3, P) + + // Variables: + { + uint t0; uint t1; uint t2; uint t3; uint t4; uint t5; + + // Alg: + t0 = mulmod(x1, x2, P); + t1 = mulmod(y1, y2, P); + t2 = mulmod(z1, z2, P); + t3 = addmod(x1, y1, 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(t3, P - t4, P); } + t4 = addmod(y1, z1, P); + x3 = addmod(y2, z2, P); // Step 10 + t4 = mulmod(t4, x3, P); + x3 = addmod(t1, t2, P); + unchecked { t4 = addmod(t4, P - x3, P); } + x3 = addmod(x1, z1, P); + y3 = addmod(x2, z2, P); // Step 15 + x3 = mulmod(x3, y3, P); + y3 = addmod(t0, t2, P); + unchecked { y3 = addmod(x3, P - y3, P); } + x3 = addmod(t0, t0, P); + t0 = addmod(x3, t0, P); // Step 20 + t2 = mulmod(B3, t2, P); + z3 = addmod(t1, t2, 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(t2, P - x3, P); } + y3 = mulmod(y3, t0, P); + t1 = mulmod(t1, z3, 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 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 @@ -168,57 +392,60 @@ 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; } //-------------------------------------------------------------------------- // 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). + /// The modular inverse of `x` is x⁻¹ such that x * x⁻¹ ≡ 1 (mod P). /// /// @dev Reverts if: /// - x not in [1, P) @@ -227,11 +454,16 @@ library Secp256k1Arithmetic { /// /// @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("TODO(modularInverse: x >= P)"); + revert("NotAFieldElement(x)"); } uint t; @@ -279,16 +511,22 @@ library Secp256k1Arithmetic { } /// @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 [0, P) + /// - xInv not in [0, P) 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) { + revert("NotAFieldElement(x)"); } - if (x >= P || xInv >= P) { - revert("TODO(modularInverse: x >= P)"); + if (xInv >= P) { + revert("NotAFieldElement(xInv)"); } return mulmod(x, xInv, P) == 1; diff --git a/src/signatures/Schnorr.sol b/src/signatures/Schnorr.sol index d955976..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). @@ -304,6 +305,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..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,10 +49,10 @@ 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 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 new file mode 100644 index 0000000..9b68144 --- /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 is 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 and 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 in wei | 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 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 + /// 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 new file mode 100644 index 0000000..4e1df5f --- /dev/null +++ b/src/stealth-addresses/StealthSecp256k1.sol @@ -0,0 +1,298 @@ +/* + + ██████ ██████  ██  ██ ███████  ██████  ██ +██      ██   ██  ██  ██  ██      ██    ██ ██ +██  ██████    ████   ███████ ██  ██ ██ +██  ██   ██   ██        ██ ██  ██ ██ + ██████ ██  ██  ██  ███████  ██████  ███████ + +*/ + +// 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, SecretKey, PublicKey} from "../curves/Secp256k1.sol"; +import { + Secp256k1Arithmetic, + Point, + ProjectivePoint +} from "../curves/Secp256k1Arithmetic.sol"; + +uint constant SCHEME_ID = 1; + +/** + * @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; +} + +/** + * @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; + 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 SecretKey; + using Secp256k1 for PublicKey; + using Secp256k1 for Point; + using Secp256k1Arithmetic for Point; + using Secp256k1Arithmetic for ProjectivePoint; + + // ~~~~~~~ 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 `sma` 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)(string) + function toString(StealthMetaAddress memory sma, string memory chain) + internal + vmed + returns (string memory) + { + string memory prefix = string.concat("st:", chain, ":0x"); + + bytes memory spendPkBytes; + bytes memory viewPkBytes; + + string memory buffer; + + // 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]; + } + + // 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 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) + { + // Create ephemeral key pair. + SecretKey ephSk = Secp256k1.newSecretKey(); + PublicKey memory ephPk = ephSk.toPublicKey(); + + console.log("[INTERNAL] newStealthAddress: Created ephemeral key pair"); + + // TODO: Move sharedPk stuff into own function? + // Otherwise naming overload. + + // Compute shared secret = [ephSk]viewPk. + // forgefmt: disable-next-item + PublicKey memory sharedPk = sma.viewPk + .toProjectivePoint() + .mul(ephSk.asUint()) + .intoPoint() + .intoPublicKey(); + + console.log( + "[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); + + // Extract view tag from shared secret. + uint8 viewTag = uint8(sharedSecretSk.asUint() >> 152); + + // Compute public key from shared secret secret key. + PublicKey memory sharedSecretPk = sharedSecretSk.toPublicKey(); + + // Compute recipients public key. + // forgefmt: disable-next-item + PublicKey memory recipientPk = sma.spendPk + .toProjectivePoint() + .add(sharedSecretPk.toProjectivePoint()) + .intoPoint() + .intoPublicKey(); + + // Derive recipients address from their public key. + address recipientAddr = recipientPk.toAddress(); + + return StealthAddress(recipientAddr, ephPk, viewTag); + } + + /// @custom:invariant Shared secret private key is not zero. + /// ∀ (viewSk, ephPk) ∊ (SecretKey, PublicKey): + /// ([viewSk]ephPk).toHash() != 0 (mod Q) + function checkStealthAddress( + SecretKey viewSk, + PublicKey memory spendPk, + StealthAddress memory sa + ) internal returns (bool) { + // Compute shared public key. + // forgefmt: disable-next-item + PublicKey memory sharedPk = sa.ephPk.toProjectivePoint() + .mul(viewSk.asUint()) + .intoPoint() + .intoPublicKey(); + + // TODO: EIP not exact: sharedSecret must be bound to field. + SecretKey sharedSecretSk = + Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); + + // Extract view tag from shared secret. + uint8 viewTag = uint8(sharedSecretSk.asUint() >> 152); + + // Return early if view tags do not match. + if (viewTag != sa.viewTag) { + return false; + } + + // Compute public key from shared secret secret key. + PublicKey memory sharedSecretPk = sharedSecretSk.toPublicKey(); + + // Compute recipients public key. + // forgefmt: disable-next-item + PublicKey memory recipientPk = spendPk.toProjectivePoint() + .add(sharedSecretPk.toProjectivePoint()) + .intoPoint() + .intoPublicKey(); + + // Derive recipients address from their public key. + address recipientAddr = recipientPk.toAddress(); + + // Return true if stealth address' address matches computed recipients + // address. + return recipientAddr == sa.recipient; + } + + // Private Key + + function computeStealthSecretKey( + SecretKey spendSk, + SecretKey viewSk, + StealthAddress memory sa + ) internal returns (SecretKey) { + // Compute shared secret public key. + // forgefmt: disable-next-item + PublicKey memory sharedPk = sa.ephPk.toProjectivePoint() + .mul(viewSk.asUint()) + .intoPoint() + .intoPublicKey(); + + // TODO: EIP not exact: sharedSecret must be bounded to field. + // TODO: If sharedSecret is zero, loop with new ephemeral key! + // Currently reverts. + SecretKey sharedSecretSk = + Secp256k1.secretKeyFromUint(uint(sharedPk.toHash()) % Secp256k1.Q); + + // Compute stealth private key. + SecretKey stealthSk = Secp256k1.secretKeyFromUint( + addmod(spendSk.asUint(), sharedSecretSk.asUint(), Secp256k1.Q) + ); + + return stealthSk; + } +} 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 a7194e3..aa807de 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_Identity() public { + assertTrue(wrapper.isOnCurve(Secp256k1Arithmetic.Identity())); + } + // -- yParity function testFuzz_Point_yParity(uint x, uint y) public { @@ -88,6 +92,40 @@ contract Secp256k1ArithmeticTest is Test { assertEq(want, got); } + // -- equals + + function testFuzz_Point_equals(SecretKey sk) public { + vm.assume(sk.isValid()); + + Point memory point = sk.toPublicKey().intoPoint(); + + assertTrue(wrapper.equals(point, point)); + } + + function testFuzz_Point_equals_FailsIfPointsDoNotEqual( + SecretKey sk1, + SecretKey sk2 + ) public { + vm.assume(sk1.asUint() != sk2.asUint()); + vm.assume(sk1.isValid()); + vm.assume(sk2.isValid()); + + Point memory point1 = sk1.toPublicKey().intoPoint(); + Point memory point2 = sk2.toPublicKey().intoPoint(); + + assertFalse(wrapper.equals(point1, point2)); + } + + function test_Point_equals_DoesNotRevert_IfPointsNotOnCurve( + Point memory point1, + Point memory point2 + ) public view { + wrapper.equals(point1, point2); + } + + //---------------------------------- + // Test: Type Conversion + // -- toProjectivePoint function testFuzz_Point_toProjectivePoint(SecretKey sk) public { @@ -103,6 +141,191 @@ 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 + + /* + 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); + } + */ + + function test_ProjectivePoint_add_Identity() public { + ProjectivePoint memory g = Secp256k1Arithmetic.G().toProjectivePoint(); + ProjectivePoint memory id = + Secp256k1Arithmetic.Identity().toProjectivePoint(); + + Point memory sum; + + sum = wrapper.add(g, id).intoPoint(); + assertEq(sum.x, g.x); + assertEq(sum.y, g.y); + + sum = wrapper.add(id, g).intoPoint(); + assertEq(sum.x, g.x); + assertEq(sum.y, g.y); + } + + //---------------------------------- + // 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 { @@ -115,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 @@ -141,8 +371,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 +392,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 +400,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 +414,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 +462,44 @@ contract Secp256k1ArithmeticWrapper { return point.yParity(); } + function equals(Point memory point, Point memory other) + public + pure + returns (bool) + { + 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/stealth-addresses/StealthSecp256k1.t.sol b/test/stealth-addresses/StealthSecp256k1.t.sol new file mode 100644 index 0000000..ecd1e93 --- /dev/null +++ b/test/stealth-addresses/StealthSecp256k1.t.sol @@ -0,0 +1,64 @@ +// 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, SecretKey, 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 SecretKey; + + StealthSecp256k1Wrapper wrapper; + + function setUp() public { + wrapper = new StealthSecp256k1Wrapper(); + } + + function test_StealthMetaAddress_toString() public { + SecretKey spendSk = Secp256k1.secretKeyFromUint( + uint( + 0x5a21e92ba5784ad9e94c9d670d3b21baff82c1668aa9ef9bd039674c7d4589f8 + ) + ); + SecretKey viewSk = Secp256k1.secretKeyFromUint( + uint( + 0xf6956ed1c1488982a7a80be72fa0ec8cc978d2c957b431e8b363557e552dbb75 + ) + ); + + StealthMetaAddress memory sma = StealthMetaAddress({ + spendPk: spendSk.toPublicKey(), + viewPk: viewSk.toPublicKey() + }); + + string memory chain = "eth"; + + string memory got = wrapper.toString(sma, 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); + } +} 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": "" + } + ] +}