diff --git a/README.md b/README.md index 749b870..2520b7a 100644 --- a/README.md +++ b/README.md @@ -14,21 +14,28 @@ ```ml src -├─ common -│ ├─ Random - "Access to cryptographically secure randomness" -│ ├─ Message - "Functionality for constructing Ethereum Signed Message Hashes" -│ └─ Nonce - "Deterministic nonce derivation" -├─ secp256k1 -│ ├─ Secp256k1 - "Cryptography-related functionality for the secp256k1 elliptic curve" -│ ├─ Secp256k1Arithmetic — "Arithmetic-related functionality for the secp256k1 elliptic curve" -│ ├─ signatures -│ │ ├─ ECDSA — "ECDSA signature functionality for secp256k1" -│ │ └─ Schnorr — "Schnorr signature functionality for secp256k1" -│ └─ stealth-addresses -│ └─ ERC5564 - "ERC-5564 conforming stealth addresses for secp256k1" -└─ interfaces - ├─ IERC5564Announcer - "ERC-5564 stealth address announcement interface" - └─ IERC5564Registry - "ERC-5564 stealth meta address registry interface" +├─ onchain +│ ├─ common +│ │ ├─ Message - "Functionality for constructing Ethereum Signed Message Hashes" +│ │ └─ Nonce - "Deterministic nonce derivation" +│ └─ secp256k1 +│ ├─ Secp256k1 - "Cryptography-related functionality for the secp256k1 elliptic curve" +│ ├─ Secp256k1Arithmetic — "Arithmetic-related functionality for the secp256k1 elliptic curve" +│ └─ signatures +│ ├─ ECDSA — "ECDSA signature functionality for secp256k1" +│ └─ Schnorr — "Schnorr signature functionality for secp256k1" +├─ offchain +│ ├─ common +│ │ └─ RandomOffchain - "Access to cryptographically secure randomness" +│ └─ secp256k1 +│ ├─ Secp256k1Offchain - "Cryptography-related functionality for the secp256k1 elliptic curve" +│ └─ signatures +│ ├─ ECDSAOffchain — "ECDSA signature functionality for secp256k1" +│ └─ SchnorrOffchain — "Schnorr signature functionality for secp256k1" +└─ unsafe + └─ secp256k1 + └─ signatures + └─ ECDSAUnsafe — "Unsafe ECDSA signature functionality for secp256k1" ``` ## Installation @@ -45,7 +52,6 @@ Several examples are provided in [`examples/`](./examples), such as: - secure key pair and Ethereum address creation - secp256k1 point arithmetic - Schnorr and ECDSA signature creation and verification -- private ETH transfer via stealth addresses ## Contributing diff --git a/docs/secp256k1/signatures/Schnorr.md b/docs/secp256k1/signatures/Schnorr.md index b0626c7..1f70a82 100644 --- a/docs/secp256k1/signatures/Schnorr.md +++ b/docs/secp256k1/signatures/Schnorr.md @@ -34,7 +34,6 @@ Schnorr signatures provide a number of advantages compared to ECDSA signatures: - `Pk :: Secp256k1::PublicKey` - The signer's public key, ie `[sk]G` - `m :: bytes32` - The keccak256 hash digest to sign - ## Signature Creation 1. Derive a cryptographically secure nonce from `m` and `sk`. diff --git a/examples/common/Random.sol b/examples/common/Random.sol new file mode 100644 index 0000000..6118d9e --- /dev/null +++ b/examples/common/Random.sol @@ -0,0 +1,28 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Script} from "forge-std/Script.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {RandomOffchain} from "src/offchain/common/RandomOffchain.sol"; + +/** + * @title RandomExample + * + * @dev Run via: + * + * ```bash + * $ forge script examples/common/Random.sol:RandomExample -vvvv + * ``` + */ +contract RandomExample is Script { + function run() public { + // Create random uint. + uint rand = RandomOffchain.readUint(); + console.log("Random uint: ", rand); + + // Bound to smaller type via discarding higher-order bits. + uint8 randByte = uint8(rand); + console.log("Random byte: ", randByte); + } +} diff --git a/examples/secp256k1/Secp256k1.sol b/examples/secp256k1/Secp256k1.sol index b94c3b7..24b452d 100644 --- a/examples/secp256k1/Secp256k1.sol +++ b/examples/secp256k1/Secp256k1.sol @@ -4,12 +4,17 @@ pragma solidity ^0.8.16; import {Script} from "forge-std/Script.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; import { Secp256k1Arithmetic, Point, ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; +} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; /** * @title Secp256k1Example @@ -24,22 +29,23 @@ import { * regarding unused variables. */ contract Secp256k1Example is Script { + using Secp256k1Offchain for SecretKey; + using Secp256k1Offchain for PublicKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; - using Secp256k1Arithmetic for Point; function run() public { // Create new cryptographically sound secret key. - SecretKey sk = Secp256k1.newSecretKey(); - assert(sk.isValid()); + SecretKey sk = Secp256k1Offchain.newSecretKey(); + // assert(sk.isValid()); console.log("Created new secret key:"); console.log(sk.asUint()); console.log(""); // Derive public key. PublicKey memory pk = sk.toPublicKey(); - assert(pk.isValid()); + // assert(pk.isValid()); console.log("Derived public key:"); console.log(pk.toString()); console.log(""); diff --git a/examples/secp256k1/signatures/ECDSA.sol b/examples/secp256k1/signatures/ECDSA.sol index 3248599..5233727 100644 --- a/examples/secp256k1/signatures/ECDSA.sol +++ b/examples/secp256k1/signatures/ECDSA.sol @@ -4,9 +4,16 @@ pragma solidity ^0.8.16; import {Script} from "forge-std/Script.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; -import {ECDSA, Signature} from "src/secp256k1/signatures/ECDSA.sol"; +import {ECDSAOffchain} from + "src/offchain/secp256k1/signatures/ECDSAOffchain.sol"; +import {ECDSA, Signature} from "src/onchain/secp256k1/signatures/ECDSA.sol"; /** * @title ECDSAExample @@ -18,9 +25,13 @@ import {ECDSA, Signature} from "src/secp256k1/signatures/ECDSA.sol"; * ``` */ contract ECDSAExample is Script { + using Secp256k1Offchain for SecretKey; + using Secp256k1Offchain for PublicKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; + using ECDSAOffchain for SecretKey; + using ECDSAOffchain for Signature; using ECDSA for address; using ECDSA for SecretKey; using ECDSA for PublicKey; @@ -30,8 +41,8 @@ contract ECDSAExample is Script { bytes memory message = bytes("crysol <3"); // Create new cryptographically sound secret key. - SecretKey sk = Secp256k1.newSecretKey(); - assert(sk.isValid()); + SecretKey sk = Secp256k1Offchain.newSecretKey(); + // assert(sk.isValid()); // Sign message via ECDSA. Signature memory sig = sk.sign(message); @@ -40,10 +51,8 @@ contract ECDSAExample is Script { console.log(""); // Verify signature via public key or address. - PublicKey memory pk = sk.toPublicKey(); - require(sk.toPublicKey().verify(message, sig), "Signature invalid"); - address addr = pk.toAddress(); - require(addr.verify(message, sig), "Signature invalid"); + // assert(sk.toPublicKey().verify(message, sig)); + // assert(sk.toPublicKey().toAddress().verify(message, sig)); // Default serialization (65 bytes). console.log("Default encoded signature:"); diff --git a/examples/secp256k1/signatures/Schnorr.sol b/examples/secp256k1/signatures/Schnorr.sol index 9e4d414..529c5e7 100644 --- a/examples/secp256k1/signatures/Schnorr.sol +++ b/examples/secp256k1/signatures/Schnorr.sol @@ -4,9 +4,18 @@ pragma solidity ^0.8.16; import {Script} from "forge-std/Script.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; - -import {Schnorr, Signature} from "src/secp256k1/signatures/Schnorr.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; + +import {SchnorrOffchain} from + "src/offchain/secp256k1/signatures/SchnorrOffchain.sol"; +import { + Schnorr, Signature +} from "src/onchain/secp256k1/signatures/Schnorr.sol"; /** * @title SchnorrExample @@ -18,9 +27,14 @@ import {Schnorr, Signature} from "src/secp256k1/signatures/Schnorr.sol"; * ``` */ contract SchnorrExample is Script { + using Secp256k1Offchain for SecretKey; + using Secp256k1Offchain for PublicKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; + using SchnorrOffchain for Signature; + using SchnorrOffchain for SecretKey; + using SchnorrOffchain for PublicKey; using Schnorr for SecretKey; using Schnorr for PublicKey; using Schnorr for Signature; @@ -29,18 +43,17 @@ contract SchnorrExample is Script { bytes memory message = bytes("crysol <3"); // Create a cryptographically secure secret key. - SecretKey sk = Secp256k1.newSecretKey(); - assert(sk.isValid()); + SecretKey sk = Secp256k1Offchain.newSecretKey(); + // assert(sk.isValid()); // Create Schnorr signature. Signature memory sig = sk.sign(message); - assert(!sig.isMalleable()); + // assert(!sig.isMalleable()); console.log("Signed message via Schnorr, signature:"); console.log(sig.toString()); console.log(""); // Verify signature. - PublicKey memory pk = sk.toPublicKey(); - require(pk.verify(message, sig), "Could not verify own signature"); + // assert(sk.toPublicKey().verify(message, sig)); } } diff --git a/examples/secp256k1/stealth-addresses/ERC5564.sol b/examples/secp256k1/stealth-addresses/ERC5564.sol deleted file mode 100644 index a22bc0f..0000000 --- a/examples/secp256k1/stealth-addresses/ERC5564.sol +++ /dev/null @@ -1,99 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.16; - -import {Script} from "forge-std/Script.sol"; -import {console2 as console} from "forge-std/console2.sol"; -import {StdStyle} from "forge-std/StdStyle.sol"; - -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; -import { - Secp256k1Arithmetic, - Point, - ProjectivePoint -} from "src/secp256k1/Secp256k1.sol"; - -import { - ERC5564, - StealthMetaAddress, - StealthAddress -} from "src/secp256k1/stealth-addresses/ERC5564.sol"; - -/** - * @title ERC5564Example - * - * @dev Run via: - * - * ```bash - * $ forge script examples/secp256k1/stealth-addresses/ERC5564.sol:ERC5564Example -vvvv - * ``` - */ -contract ERC5564Example is Script { - using Secp256k1 for SecretKey; - using Secp256k1 for PublicKey; - - using ERC5564 for SecretKey; - using ERC5564 for StealthMetaAddress; - - function run() public { - // Alice creates a key pair funded with 1 ETH. - SecretKey aliceSk = Secp256k1.newSecretKey(); - PublicKey memory alicePk = aliceSk.toPublicKey(); - vm.deal(alicePk.toAddress(), 1 ether); - logAlice("Created new address funded with 1 ETH"); - - // Bob creates two key pairs for their stealth meta address, - // the spend key pair and the view key pair. - SecretKey bobSpendSk = Secp256k1.newSecretKey(); - PublicKey memory bobSpendPk = bobSpendSk.toPublicKey(); - SecretKey bobViewSk = Secp256k1.newSecretKey(); - PublicKey memory bobViewPk = bobViewSk.toPublicKey(); - logBob("Created two key pairs for their stealth meta address"); - - // Bob creates their stealth meta address and publishes it via some - // known channel, eg an ERC-5564 Registry contract. - StealthMetaAddress memory bobStealthMeta = - StealthMetaAddress({spendPk: bobSpendPk, viewPk: bobViewPk}); - logBob( - string.concat( - "Created and published stealth meta address: ", - bobStealthMeta.toString("eth") - ) - ); - - // Alice creates a new stealth address from Bob's publicly known stealth - // meta address. - StealthAddress memory stealth = bobStealthMeta.generateStealthAddress(); - logAlice("Generated stealth address from Bob stealth meta address"); - - // Alice sends 1 ETH to the stealth address only accessible by Bob and - // publishes the stealth address via some know channel, eg an ERC-5564 - // Announcer contract. - vm.prank(alicePk.toAddress()); - (bool ok,) = stealth.addr.call{value: 1 ether}(""); - assert(ok); - logAlice( - "Send 1 ETH to stealth address and published the stealth address" - ); - - // Bob checks whether the announced stealth address belongs to them. - bool found = bobViewSk.checkStealthAddress(bobSpendPk, stealth); - assert(found); - logBob("Found out the stealth address belongs to them"); - - // Bob computes the stealth address' secret key. - SecretKey stealthSk = - bobSpendSk.computeStealthSecretKey(bobViewSk, stealth); - logBob("Computed the stealth address' secret key"); - - // Verify secret key is correct: - assert(stealthSk.toPublicKey().toAddress() == stealth.addr); - } - - function logAlice(string memory message) internal pure { - console.log(string.concat(StdStyle.yellow("[ALICE] "), message)); - } - - function logBob(string memory message) internal pure { - console.log(string.concat(StdStyle.blue("[BOB] "), message)); - } -} diff --git a/foundry.toml b/foundry.toml index f005497..70ecd3e 100644 --- a/foundry.toml +++ b/foundry.toml @@ -9,7 +9,6 @@ evm_version = "shanghai" solc_version = "0.8.24" via_ir = false optimizer = false -optimizer_runs = 100_000 extra_output_files = ["irOptimized"] [fmt] @@ -20,12 +19,6 @@ number_underscore = "preserve" [doc] out = "docs_generated" # Note to not overwrite own docs -# Profile to compile without --via-ir and optimizations -# Run via `FOUNDRY_PROFILE=light forge ...` -[profile.light] -via_ir = false -optimizer = false - # Profile to compile with --via-ir and optimizations # Run via `FOUNDRY_PROFILE=release forge ...` [profile.release] @@ -46,5 +39,6 @@ optimizer = true optimizer_runs = 100_000 [profile.ci.fuzz] runs = 10_000 +max_test_rejects = 4_294_967_295 [profile.ci.invariant] runs = 10_000 diff --git a/src/interfaces/IERC5564Announcer.sol b/src/interfaces/IERC5564Announcer.sol deleted file mode 100644 index 7daff03..0000000 --- a/src/interfaces/IERC5564Announcer.sol +++ /dev/null @@ -1,85 +0,0 @@ -/* - - ██████ ██████  ██  ██ ███████  ██████  ██ -██      ██   ██  ██  ██  ██      ██    ██ ██ -██  ██████    ████   ███████ ██  ██ ██ -██  ██   ██   ██        ██ ██  ██ ██ - ██████ ██  ██  ██  ███████  ██████  ███████ - -*/ - -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.16; - -/** - * @title IERC5561Announcer - * - * @notice Interface to announce a tx to an [ERC-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 [ERC-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 - * - [ERC-5564]: https://eips.ethereum.org/EIPS/eip-5564 - * - [ERC-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 The scheme id based on [ERC-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 [ERC-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 [ERC-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 [ERC-5564] provides - /// recommendations. - function announce( - uint schemeId, - address stealthAddress, - bytes memory ephemeralPubKey, - bytes memory metadata - ) external; -} diff --git a/src/interfaces/IERC5564Registry.sol b/src/interfaces/IERC5564Registry.sol deleted file mode 100644 index b161bfd..0000000 --- a/src/interfaces/IERC5564Registry.sol +++ /dev/null @@ -1,51 +0,0 @@ -/* - - ██████ ██████  ██  ██ ███████  ██████  ██ -██      ██   ██  ██  ██  ██      ██    ██ ██ -██  ██████    ████   ███████ ██  ██ ██ -██  ██   ██   ██        ██ ██  ██ ██ - ██████ ██  ██  ██  ███████  ██████  ███████ - -*/ - -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.16; - -/** - * @title IERC5564Registry - * - * @notice Interface for an [ERC-5564 Stealth Meta-Address Registry] - * - * @dev The stealth meta-address registry maps recipient identifiers, such as - * eg. Ethereum addresses or ENS domains, to their stealth meta-address. - * - * @custom:references - * - [ERC-5564]: https://eips.ethereum.org/EIPS/eip-5564 - * - [ERC-5564 Stealth Meta-Address Registry]: https://eips.ethereum.org/EIPS/eip-6538 - */ -interface IERC5564Registry { - /// @notice Emitted when a registrant updates their stealth meta-address. - /// - /// @param registrant The registrant's identifier. - /// @param schemeId The scheme id based on [ERC-5564 Scheme Registry]. - /// @param stealthMetaAddress The registrant's stealth meta address. - event StealthMetaAddressSet( - bytes indexed registrant, - uint indexed schemeId, - bytes stealthMetaAddress - ); - - /// @notice Returns the stealth meta address of recipient `recipient` for - /// scheme id `schemeId` or zero if not registered. - /// - /// @param recipient The recipient's identifier. - /// @param schemeId The scheme id based on [ERC-5564 Scheme Registry]. - /// @return stealthMetaAddress The stealth meta-address if recipient - /// registered for given scheme id, zero otherwise. - function stealthMetaAddressOf(bytes memory recipient, uint schemeId) - external - view - returns (bytes memory stealthMetaAddress); - - // TODO: Mutating functions not included yet. -} diff --git a/src/common/Random.sol b/src/offchain/common/RandomOffchain.sol similarity index 89% rename from src/common/Random.sol rename to src/offchain/common/RandomOffchain.sol index eca9589..69100ee 100644 --- a/src/common/Random.sol +++ b/src/offchain/common/RandomOffchain.sol @@ -14,15 +14,16 @@ pragma solidity ^0.8.16; import {Vm} from "forge-std/Vm.sol"; /** - * @title Random + * @title RandomOffchain * - * @notice Provides access to cryptographically secure randomness + * @notice Provides offchain access to cryptographically secure randomness * * @dev Randomness is sourced from cast's `wallet new` command. * - * @author crysol (https://github.com/pmerkleplant/crysol) + * @author verklegarden + * @custom:repository github.com/verklegarden/crysol */ -library Random { +library RandomOffchain { // ~~~~~~~ Prelude ~~~~~~~ // forgefmt: disable-start Vm private constant vm = Vm(address(uint160(uint(keccak256("hevm cheat code"))))); diff --git a/src/offchain/secp256k1/Secp256k1Offchain.sol b/src/offchain/secp256k1/Secp256k1Offchain.sol new file mode 100644 index 0000000..6988064 --- /dev/null +++ b/src/offchain/secp256k1/Secp256k1Offchain.sol @@ -0,0 +1,107 @@ +/* + + ██████ ██████  ██  ██ ███████  ██████  ██ +██      ██   ██  ██  ██  ██      ██    ██ ██ +██  ██████    ████   ███████ ██  ██ ██ +██  ██   ██   ██        ██ ██  ██ ██ + ██████ ██  ██  ██  ███████  ██████  ███████ + +*/ + +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Vm} from "forge-std/Vm.sol"; + +import {RandomOffchain} from "../common/RandomOffchain.sol"; + +import { + Secp256k1, + SecretKey, + PublicKey +} from "../../onchain/secp256k1/Secp256k1.sol"; +import { + Secp256k1Arithmetic, + Point, + ProjectivePoint +} from "../../onchain/secp256k1/Secp256k1Arithmetic.sol"; + +/** + * @title Secp256k1Offchain + * + * @notice Providing offchain cryptography-related functionality for the secp256k1 + * elliptic curve + * + * @author crysol (https://github.com/pmerkleplant/crysol) + */ +library Secp256k1Offchain { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + 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 + // ~~~~~~~~~~~~~~~~~~~~~~~ + + //-------------------------------------------------------------------------- + // Secret Key + + /// @dev Returns a new cryptographically secure secret key. + /// + /// @custom:vm RandomOffchain::readUint()(uint) + function newSecretKey() internal vmed returns (SecretKey) { + uint scalar; + while (scalar == 0 || scalar >= Secp256k1Arithmetic.Q) { + // Note to not introduce potential bias via bounding operation. + scalar = RandomOffchain.readUint(); + } + return Secp256k1.secretKeyFromUint(scalar); + } + + /// @dev Returns the public key of secret key `sk`. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm vm.createWallet(uint) + function toPublicKey(SecretKey sk) + internal + vmed + returns (PublicKey memory) + { + if (!sk.isValid()) { + revert("SecretKeyInvalid()"); + } + + // Use vm to compute pk = [sk]G. + Vm.Wallet memory wallet = vm.createWallet(sk.asUint()); + return PublicKey(wallet.publicKeyX, wallet.publicKeyY); + } + + //-------------------------------------------------------------------------- + // Public Key + + /// @dev Returns a string representation of public key `pk`. + /// + /// @custom:vm vm.toString(uint) + function toString(PublicKey memory pk) + internal + view + vmed + returns (string memory) + { + string memory str = "Secp256k1::PublicKey({"; + str = string.concat(str, " x: ", vm.toString(pk.x), ","); + str = string.concat(str, " y: ", vm.toString(pk.y)); + str = string.concat(str, " })"); + return str; + } +} diff --git a/src/offchain/secp256k1/signatures/ECDSAOffchain.sol b/src/offchain/secp256k1/signatures/ECDSAOffchain.sol new file mode 100644 index 0000000..16b347f --- /dev/null +++ b/src/offchain/secp256k1/signatures/ECDSAOffchain.sol @@ -0,0 +1,163 @@ +/* + + ██████ ██████  ██  ██ ███████  ██████  ██ +██      ██   ██  ██  ██  ██      ██    ██ ██ +██  ██████    ████   ███████ ██  ██ ██ +██  ██   ██   ██        ██ ██  ██ ██ + ██████ ██  ██  ██  ███████  ██████  ███████ + +*/ + +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Vm} from "forge-std/Vm.sol"; + +import {Message} from "../../../onchain/common/Message.sol"; + +import {Secp256k1Offchain} from "../Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "../../../onchain/secp256k1/Secp256k1.sol"; + +import { + ECDSA, Signature +} from "../../..//onchain/secp256k1/signatures/ECDSA.sol"; + +/** + * @title ECDSAOffchain + * + * @notice Provides offchain ECDSA signature functionality + * + * @author crysol (https://github.com/pmerkleplant/crysol) + */ +library ECDSAOffchain { + using Secp256k1 for SecretKey; + + using ECDSAOffchain for SecretKey; + + // ~~~~~~~ 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 + // ~~~~~~~~~~~~~~~~~~~~~~~ + + //-------------------------------------------------------------------------- + // Signature Creation + + /// @dev Returns an ECDSA signature signed by secret key `sk` signing + /// message `message`. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm vm.sign(uint,bytes32) + /// @custom:invariant Created signature is non-malleable. + function sign(SecretKey sk, bytes memory message) + internal + view + vmed + returns (Signature memory) + { + bytes32 digest = keccak256(message); + + return sk.sign(digest); + } + + /// @dev Returns an ECDSA signature signed by secret key `sk` signing hash + /// digest `digest`. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm vm.sign(uint,bytes32) + /// @custom:invariant Created signature is non-malleable. + function sign(SecretKey sk, bytes32 digest) + internal + view + vmed + returns (Signature memory) + { + if (!sk.isValid()) { + revert("SecretKeyInvalid()"); + } + + uint8 v; + bytes32 r; + bytes32 s; + (v, r, s) = vm.sign(sk.asUint(), digest); + + Signature memory sig = Signature(v, r, s); + // assert(!sig.isMalleable()); + + return sig; + } + + /// @dev Returns an ECDSA signature signed by secret key `sk` singing + /// message `message`'s keccak256 digest as Ethereum Signed Message. + /// + /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm vm.sign(uint,bytes32) + /// @custom:invariant Created signature is non-malleable. + function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) + internal + view + vmed + returns (Signature memory) + { + bytes32 digest = Message.deriveEthereumSignedMessageHash(message); + + return sk.sign(digest); + } + + /// @dev Returns an ECDSA signature signed by secret key `sk` singing hash + /// digest `digest` as Ethereum Signed Message. + /// + /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm vm.sign(uint,bytes32) + /// @custom:invariant Created signature is non-malleable. + function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) + internal + view + vmed + returns (Signature memory) + { + bytes32 digest2 = Message.deriveEthereumSignedMessageHash(digest); + + return sk.sign(digest2); + } + + //-------------------------------------------------------------------------- + // Utils + + /// @dev Returns a string representation of signature `sig`. + /// + /// @custom:vm vm.toString(uint) + function toString(Signature memory sig) + internal + view + vmed + returns (string memory) + { + string memory str = "ECDSA::Signature({"; + str = string.concat(str, " v: ", vm.toString(sig.v), ","); + str = string.concat(str, " r: ", vm.toString(sig.r), ","); + str = string.concat(str, " s: ", vm.toString(sig.s)); + str = string.concat(str, " })"); + return str; + } +} diff --git a/src/offchain/secp256k1/signatures/SchnorrOffchain.sol b/src/offchain/secp256k1/signatures/SchnorrOffchain.sol new file mode 100644 index 0000000..4564285 --- /dev/null +++ b/src/offchain/secp256k1/signatures/SchnorrOffchain.sol @@ -0,0 +1,184 @@ +/* + + ██████ ██████  ██  ██ ███████  ██████  ██ +██      ██   ██  ██  ██  ██      ██    ██ ██ +██  ██████    ████   ███████ ██  ██ ██ +██  ██   ██   ██        ██ ██  ██ ██ + ██████ ██  ██  ██  ███████  ██████  ███████ + +*/ + +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Vm} from "forge-std/Vm.sol"; + +import {Message} from "../../../onchain/common/Message.sol"; +import {Nonce} from "../../../onchain/common/Nonce.sol"; + +import {Secp256k1Offchain} from "../Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "../../../onchain/secp256k1/Secp256k1.sol"; + +import { + Schnorr, + Signature +} from "../../../onchain/secp256k1/signatures/Schnorr.sol"; + +/** + * @title SchnorrOffchain + * + * @notice Provides offchain Schnorr signature functionality + * + * @custom:docs docs/signatures/Schnorr.md + * + * @author crysol (https://github.com/pmerkleplant/crysol) + */ +library SchnorrOffchain { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + + using SchnorrOffchain for SecretKey; + + // ~~~~~~~ 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 + // ~~~~~~~~~~~~~~~~~~~~~~~ + + //-------------------------------------------------------------------------- + // Signature Creation + + /// @dev Returns a Schnorr signature signed by secret key `sk` signing + /// message `message`. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm sign(SecretKey, bytes32) + function sign(SecretKey sk, bytes memory message) + internal + vmed + returns (Signature memory) + { + bytes32 digest = keccak256(message); + + return sk.sign(digest); + } + + /// @dev Returns a Schnorr signature signed by secret key `sk` signing + /// hash digest `digest`. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm curves/Secp256k1::toPublicKey(SecretKey)(PublicKey) + function sign(SecretKey sk, bytes32 digest) + internal + vmed + returns (Signature memory) + { + // Note that public key derivation fails if secret key is invalid. + PublicKey memory pk = sk.toPublicKey(); + + // Derive deterministic nonce ∊ [1, Q). + uint nonce = Nonce.deriveNonceFrom(sk.asUint(), digest) % Secp256k1.Q; + // assert(nonce != 0); // TODO: Revisit once nonce derived via RFC-6979. + + // Compute nonce's public key. + PublicKey memory noncePk = + Secp256k1.secretKeyFromUint(nonce).toPublicKey(); + + // Derive commitment from nonce's public key. + address commitment = noncePk.toAddress(); + + // Construct challenge = H(Pkₓ ‖ Pkₚ ‖ m ‖ Rₑ) (mod Q) + bytes32 challenge = bytes32( + uint( + keccak256( + abi.encodePacked( + pk.x, uint8(pk.yParity()), digest, commitment + ) + ) + ) % Secp256k1.Q + ); + + // Compute signature = k + (e * sk) (mod Q) + bytes32 signature = bytes32( + addmod( + nonce, + mulmod(uint(challenge), sk.asUint(), Secp256k1.Q), + Secp256k1.Q + ) + ); + + return Signature(signature, commitment); + } + + /// @dev Returns a Schnorr signature signed by secret key `sk` singing + /// message `message`'s keccak256 digest as Ethereum Signed Message. + /// + /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm sign(SecretKey,bytes32) + function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) + internal + vmed + returns (Signature memory) + { + bytes32 digest = Message.deriveEthereumSignedMessageHash(message); + + return sk.sign(digest); + } + + /// @dev Returns a Schnorr signature signed by secret key `sk` singing + /// hash digest `digest` as Ethereum Signed Message. + /// + /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. + /// + /// @dev Reverts if: + /// Secret key invalid + /// + /// @custom:vm sign(SecretKey,bytes32) + function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) + internal + vmed + returns (Signature memory) + { + bytes32 digest2 = Message.deriveEthereumSignedMessageHash(digest); + + return sk.sign(digest2); + } + + //-------------------------------------------------------------------------- + // Utils + + /// @dev Returns a string representation of signature `sig`. + /// + /// @custom:vm vm.toString(uint) + function toString(Signature memory sig) + internal + view + vmed + returns (string memory) + { + // forgefmt: disable-start + string memory str = "Schnorr::Signature({"; + str = string.concat(str, " signature: ", vm.toString(sig.signature), ","); + str = string.concat(str, " commitment: ", vm.toString(sig.commitment)); + str = string.concat(str, " })"); + return str; + // forgefmt: disable-end + } +} diff --git a/src/common/Message.sol b/src/onchain/common/Message.sol similarity index 100% rename from src/common/Message.sol rename to src/onchain/common/Message.sol diff --git a/src/common/Nonce.sol b/src/onchain/common/Nonce.sol similarity index 100% rename from src/common/Nonce.sol rename to src/onchain/common/Nonce.sol diff --git a/src/secp256k1/Secp256k1.sol b/src/onchain/secp256k1/Secp256k1.sol similarity index 84% rename from src/secp256k1/Secp256k1.sol rename to src/onchain/secp256k1/Secp256k1.sol index 3ed05ab..595da44 100644 --- a/src/secp256k1/Secp256k1.sol +++ b/src/onchain/secp256k1/Secp256k1.sol @@ -11,10 +11,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.16; -import {Vm} from "forge-std/Vm.sol"; - -import {Random} from "../common/Random.sol"; - import { Secp256k1Arithmetic, Point, @@ -90,16 +86,6 @@ library Secp256k1 { 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 - // ~~~~~~~~~~~~~~~~~~~~~~~ - //-------------------------------------------------------------------------- // Private Constants @@ -124,18 +110,6 @@ library Secp256k1 { //-------------------------------------------------------------------------- // Secret Key - /// @dev Returns a new cryptographically secure secret key. - /// - /// @custom:vm Random::readUint()(uint) - function newSecretKey() internal vmed returns (SecretKey) { - uint scalar; - while (scalar == 0 || scalar >= Q) { - // Note to not introduce potential bias via bounding operation. - scalar = Random.readUint(); - } - return secretKeyFromUint(scalar); - } - /// @dev Returns whether secret key `sk` is valid. /// /// @dev Note that a secret key MUST be a field element in order to be valid, @@ -146,26 +120,6 @@ library Secp256k1 { return scalar != 0 && scalar < Q; } - /// @dev Returns the public key of secret key `sk`. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm vm.createWallet(uint) - function toPublicKey(SecretKey sk) - internal - vmed - returns (PublicKey memory) - { - if (!sk.isValid()) { - revert("SecretKeyInvalid()"); - } - - // Use vm to compute pk = [sk]G. - Vm.Wallet memory wallet = vm.createWallet(sk.asUint()); - return PublicKey(wallet.publicKeyX, wallet.publicKeyY); - } - /// @dev Returns uint `scalar` as secret key. /// /// @dev Reverts if: @@ -212,20 +166,6 @@ library Secp256k1 { return digest; } - // TODO: docs PublicKey.toString() - function toString(PublicKey memory pk) - internal - view - vmed - returns (string memory) - { - string memory str = "Secp256k1::PublicKey({"; - str = string.concat(str, " x: ", vm.toString(pk.x), ","); - str = string.concat(str, " y: ", vm.toString(pk.x)); - str = string.concat(str, " })"); - return str; - } - /// @dev Returns whether public key `pk` is a valid secp256k1 public key. /// /// @dev Note that a public key is valid if its either on the curve or the diff --git a/src/secp256k1/Secp256k1Arithmetic.sol b/src/onchain/secp256k1/Secp256k1Arithmetic.sol similarity index 99% rename from src/secp256k1/Secp256k1Arithmetic.sol rename to src/onchain/secp256k1/Secp256k1Arithmetic.sol index 759c0c0..136cf61 100644 --- a/src/secp256k1/Secp256k1Arithmetic.sol +++ b/src/onchain/secp256k1/Secp256k1Arithmetic.sol @@ -307,6 +307,8 @@ library Secp256k1Arithmetic { return result; } + // TODO: Provide verifyMul(point,scalar,want) using ecrecover? + //-------------------------------------------------------------------------- // Type Conversions @@ -603,8 +605,8 @@ library Secp256k1Arithmetic { // The `modexp` precompile is at address 0x05. address target = address(5); - (bool ok, bytes memory result) = target.staticcall(payload); - assert(ok); // Precompile calls do not fail. + ( /*bool ok*/ , bytes memory result) = target.staticcall(payload); + // assert(ok); // Precompile calls do not fail. // Note that abi.decode() reverts if result is empty. // Result is empty iff the modexp computation failed due to insufficient diff --git a/src/secp256k1/signatures/ECDSA.sol b/src/onchain/secp256k1/signatures/ECDSA.sol similarity index 70% rename from src/secp256k1/signatures/ECDSA.sol rename to src/onchain/secp256k1/signatures/ECDSA.sol index a622d41..dfca0f3 100644 --- a/src/secp256k1/signatures/ECDSA.sol +++ b/src/onchain/secp256k1/signatures/ECDSA.sol @@ -11,8 +11,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.16; -import {Vm} from "forge-std/Vm.sol"; - import {Message} from "../../common/Message.sol"; import {Secp256k1, SecretKey, PublicKey} from "../Secp256k1.sol"; @@ -43,9 +41,9 @@ struct Signature { * secret key. This weakness has lead to numerous bugs in not just smart * contract systems. * - * Therefore, this library only creates and accepts signatures in one of - * the two possible representations. Signatures in the second representation - * are deemed invalid. + * Therefore, crysol only creates and accepts signatures in one of the two + * possible representations. Signatures in the second representation are + * deemed invalid. * For more info, see function `isMalleable(Signature)(bool)`. * * This behaviour is sync with the broader Ethereum ecosystem as a general @@ -68,15 +66,8 @@ library ECDSA { using ECDSA for SecretKey; using ECDSA for PublicKey; - // ~~~~~~~ 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 - // ~~~~~~~~~~~~~~~~~~~~~~~ + //-------------------------------------------------------------------------- + // Private Constants /// @dev Mask to receive an ECDSA's s value from an EIP-2098 compact /// signature representation. @@ -85,6 +76,12 @@ library ECDSA { bytes32 private constant _EIP2098_MASK = 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF; + /// @dev Used during malleability check. + /// + /// Note that ECDSA signatures are malleable with regards to their s + /// value and deemed invalid if s > secp256k1's order / 2. + uint private constant _SECP256K1_HALF = Secp256k1.Q / 2; + //-------------------------------------------------------------------------- // Signature Verification @@ -173,99 +170,6 @@ library ECDSA { return signer == ecrecover(digest, sig.v, sig.r, sig.s); } - //-------------------------------------------------------------------------- - // Signature Creation - - /// @dev Returns an ECDSA signature signed by secret key `sk` signing - /// message `message`. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm vm.sign(uint,bytes32) - /// @custom:invariant Created signature is non-malleable. - function sign(SecretKey sk, bytes memory message) - internal - view - vmed - returns (Signature memory) - { - bytes32 digest = keccak256(message); - - return sk.sign(digest); - } - - /// @dev Returns an ECDSA signature signed by secret key `sk` signing hash - /// digest `digest`. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm vm.sign(uint,bytes32) - /// @custom:invariant Created signature is non-malleable. - function sign(SecretKey sk, bytes32 digest) - internal - view - vmed - returns (Signature memory) - { - if (!sk.isValid()) { - revert("SecretKeyInvalid()"); - } - - uint8 v; - bytes32 r; - bytes32 s; - (v, r, s) = vm.sign(sk.asUint(), digest); - - Signature memory sig = Signature(v, r, s); - // assert(!sig.isMalleable()); - - return sig; - } - - /// @dev Returns an ECDSA signature signed by secret key `sk` singing - /// message `message`'s keccak256 digest as Ethereum Signed Message. - /// - /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm vm.sign(uint,bytes32) - /// @custom:invariant Created signature is non-malleable. - function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) - internal - view - vmed - returns (Signature memory) - { - bytes32 digest = Message.deriveEthereumSignedMessageHash(message); - - return sk.sign(digest); - } - - /// @dev Returns an ECDSA signature signed by secret key `sk` singing hash - /// digest `digest` as Ethereum Signed Message. - /// - /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm vm.sign(uint,bytes32) - /// @custom:invariant Created signature is non-malleable. - function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) - internal - view - vmed - returns (Signature memory) - { - bytes32 digest2 = Message.deriveEthereumSignedMessageHash(digest); - - return sk.sign(digest2); - } - //-------------------------------------------------------------------------- // Utils @@ -273,25 +177,7 @@ library ECDSA { /// /// @dev A signature is malleable if `sig.s > Secp256k1.Q / 2`. function isMalleable(Signature memory sig) internal pure returns (bool) { - return sig.s - > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0; - } - - /// @dev Returns a string representation of signature `sig`. - /// - /// @custom:vm vm.toString(uint) - function toString(Signature memory sig) - internal - view - vmed - returns (string memory) - { - string memory str = "ECDSA::Signature({"; - str = string.concat(str, " v: ", vm.toString(sig.v), ","); - str = string.concat(str, " r: ", vm.toString(sig.r), ","); - str = string.concat(str, " s: ", vm.toString(sig.s)); - str = string.concat(str, " })"); - return str; + return uint(sig.s) > _SECP256K1_HALF; } //-------------------------------------------------------------------------- diff --git a/src/secp256k1/signatures/Schnorr.sol b/src/onchain/secp256k1/signatures/Schnorr.sol similarity index 55% rename from src/secp256k1/signatures/Schnorr.sol rename to src/onchain/secp256k1/signatures/Schnorr.sol index c72ed87..6e11cac 100644 --- a/src/secp256k1/signatures/Schnorr.sol +++ b/src/onchain/secp256k1/signatures/Schnorr.sol @@ -11,11 +11,6 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.16; -import {Vm} from "forge-std/Vm.sol"; - -import {Message} from "../../common/Message.sol"; -import {Nonce} from "../../common/Nonce.sol"; - import {Secp256k1, SecretKey, PublicKey} from "../Secp256k1.sol"; /** @@ -43,23 +38,11 @@ library Schnorr { using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; - using Nonce for SecretKey; - using Schnorr for address; using Schnorr for Signature; using Schnorr for SecretKey; using Schnorr for PublicKey; - // ~~~~~~~ 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 - // ~~~~~~~~~~~~~~~~~~~~~~~ - //-------------------------------------------------------------------------- // Signature Verification @@ -96,6 +79,8 @@ library Schnorr { revert("PublicKeyInvalid()"); } + // TODO: What about digest of zero? + if (sig.signature == 0 || sig.commitment == address(0)) { revert("SignatureTrivial()"); } @@ -164,113 +149,6 @@ library Schnorr { return sig.commitment == recovered; } - //-------------------------------------------------------------------------- - // Signature Creation - - /// @dev Returns a Schnorr signature signed by secret key `sk` signing - /// message `message`. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm sign(SecretKey, bytes32) - function sign(SecretKey sk, bytes memory message) - internal - vmed - returns (Signature memory) - { - bytes32 digest = keccak256(message); - - return sk.sign(digest); - } - - /// @dev Returns a Schnorr signature signed by secret key `sk` signing - /// hash digest `digest`. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm curves/Secp256k1::toPublicKey(SecretKey)(PublicKey) - function sign(SecretKey sk, bytes32 digest) - internal - vmed - returns (Signature memory) - { - // Note that public key derivation fails if secret key is invalid. - PublicKey memory pk = sk.toPublicKey(); - - // Derive deterministic nonce ∊ [1, Q). - uint nonce = Nonce.deriveNonceFrom(sk.asUint(), digest) % Secp256k1.Q; - assert(nonce != 0); // TODO: Revisit once nonce derived via RFC 6979. - - // Compute nonce's public key. - PublicKey memory noncePk = - Secp256k1.secretKeyFromUint(nonce).toPublicKey(); - - // Derive commitment from nonce's public key. - address commitment = noncePk.toAddress(); - - // Construct challenge = H(Pkₓ ‖ Pkₚ ‖ m ‖ Rₑ) (mod Q) - bytes32 challenge = bytes32( - uint( - keccak256( - abi.encodePacked( - pk.x, uint8(pk.yParity()), digest, commitment - ) - ) - ) % Secp256k1.Q - ); - - // Compute signature = k + (e * sk) (mod Q) - bytes32 signature = bytes32( - addmod( - nonce, - mulmod(uint(challenge), sk.asUint(), Secp256k1.Q), - Secp256k1.Q - ) - ); - - return Signature(signature, commitment); - } - - /// @dev Returns a Schnorr signature signed by secret key `sk` singing - /// message `message`'s keccak256 digest as Ethereum Signed Message. - /// - /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm sign(SecretKey, bytes32) - function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) - internal - vmed - returns (Signature memory) - { - bytes32 digest = Message.deriveEthereumSignedMessageHash(message); - - return sk.sign(digest); - } - - /// @dev Returns a Schnorr signature signed by secret key `sk` singing - /// hash digest `digest` as Ethereum Signed Message. - /// - /// @dev For more info regarding Ethereum Signed Messages, see {Message.sol}. - /// - /// @dev Reverts if: - /// Secret key invalid - /// - /// @custom:vm sign(SecretKey, bytes32) - function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) - internal - vmed - returns (Signature memory) - { - bytes32 digest2 = Message.deriveEthereumSignedMessageHash(digest); - - return sk.sign(digest2); - } - //-------------------------------------------------------------------------- // Utils @@ -283,24 +161,6 @@ library Schnorr { return uint(sig.signature) >= Secp256k1.Q; } - /// @dev Returns a string representation of signature `sig`. - /// - /// @custom:vm vm.toString(uint) - function toString(Signature memory sig) - internal - view - vmed - returns (string memory) - { - // forgefmt: disable-start - string memory str = "Schnorr::Signature({"; - str = string.concat(str, " signature: ", vm.toString(sig.signature), ","); - str = string.concat(str, " commitment: ", vm.toString(sig.commitment)); - str = string.concat(str, " })"); - return str; - // forgefmt: disable-end - } - //-------------------------------------------------------------------------- // (De)Serialization // diff --git a/src/secp256k1/stealth-addresses/ERC5564.sol b/src/secp256k1/stealth-addresses/ERC5564.sol deleted file mode 100644 index bd0ea0a..0000000 --- a/src/secp256k1/stealth-addresses/ERC5564.sol +++ /dev/null @@ -1,317 +0,0 @@ -/* - - ██████ ██████  ██  ██ ███████  ██████  ██ -██      ██   ██  ██  ██  ██      ██    ██ ██ -██  ██████    ████   ███████ ██  ██ ██ -██  ██   ██   ██        ██ ██  ██ ██ - ██████ ██  ██  ██  ███████  ██████  ███████ - -*/ - -// SPDX-License-Identifier: MIT OR Apache-2.0 -pragma solidity ^0.8.16; - -import {Vm} from "forge-std/Vm.sol"; - -import {Secp256k1, SecretKey, PublicKey} from "../Secp256k1.sol"; -import { - Secp256k1Arithmetic, - Point, - ProjectivePoint -} from "../Secp256k1Arithmetic.sol"; - -// TODO: docs StealthMetaAddress -/** - * @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 A stealth address' secret key is computed via the spending secret key. - * The viewing secret key is used to determine whether a tx belongs to its - * stealth meta address. - * - * @custom:example Generate a stealth meta address: - * - * ```solidity - * import {Secp256k1, SecretKey, PublicKey} from "crysol/curves/Secp256k1.sol"; - * import {StealthAdressesSecp256k1, StealthMetaAddress} from "crysol/stealth-addresses/StealthAdressesSecp256k1.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 the tuple of public keys. - * StealthMetaAddress memory sma = StealthMetaAddress({ - * spendPk: spendSk.toPublicKey(), - * viewPk: viewSk.toPublicKey() - * }); - * } - * ``` - */ -struct StealthMetaAddress { - PublicKey spendPk; - PublicKey viewPk; -} - -// TODO: docs struct StealthAddress -/** - * @notice StealthAddress - */ -struct StealthAddress { - address addr; - PublicKey ephPk; - uint8 viewTag; -} - -/** - * @title StealthAddressesSecp256k1 - * - * @notice [ERC-5564] conforming stealth addresses for the secp256k1 curve - * - * @custom:references - * - [ERC-5564]: https://eips.ethereum.org/EIPS/eip-5564 - * - [ERC-5564 Scheme Ids]: https://eips.ethereum.org/assets/eip-5564/scheme_ids - * - * @author crysol (https://github.com/pmerkleplant/crysol) - */ -library ERC5564 { - 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 - // ~~~~~~~~~~~~~~~~~~~~~~~ - - //-------------------------------------------------------------------------- - // Constants - - /// @dev Identifies the stealth address scheme, see [ERC-5564 Scheme Ids]. - uint internal constant SCHEME_ID = 0; - - //-------------------------------------------------------------------------- - // Sender - - /// @dev Returns a randomly generated stealth address derived from stealth - /// meta address `stealthMeta`. - /// - /// @custom:vm Secp256k1::newSecretKey() - function generateStealthAddress(StealthMetaAddress memory stealthMeta) - internal - vmed - returns (StealthAddress memory) - { - // Create ephemeral secret key. - SecretKey ephSk = Secp256k1.newSecretKey(); - - return generateStealthAddress(stealthMeta, ephSk); - } - - /// @dev Returns a stealth address derived from stealth meta address - /// `stealthMeta` via ephemeral secret key `ephSk`. - /// - /// @dev Note that the ephemeral secret key MUST be kept private to not leak - /// the stealth address' owner! - /// - /// @dev Reverts if: - /// Ephemeral secret key invalid - /// - /// @custom:vm Secp256k1::SecretKey.toPublicKey() - /// @custom:invariant A public key's keccak256 image is never zero: - /// ∀ pk ∊ PublicKey: keccak256(pk) != 0 - function generateStealthAddress( - StealthMetaAddress memory stealthMeta, - SecretKey ephSk - ) internal vmed returns (StealthAddress memory) { - if (!ephSk.isValid()) { - revert("SecretKeyInvalid()"); - } - - PublicKey memory ephPk = ephSk.toPublicKey(); - - // Compute shared secret key from ephemeral secret key and stealthMeta's - // view public key. - SecretKey sharedSk = _deriveSharedSecret(ephSk, stealthMeta.viewPk); - - // Extract view tag from shared secret key. - uint8 viewTag = _extractViewTag(sharedSk); - - // Derive shared secret key's public key. - PublicKey memory sharedPk = sharedSk.toPublicKey(); - - // Compute stealth address' public key. - // forgefmt: disable-next-item - PublicKey memory stealthPk = stealthMeta.spendPk - .toProjectivePoint() - .add(sharedPk.toProjectivePoint()) - .intoPoint() - .intoPublicKey(); - - // Return stealth address. - return StealthAddress({ - addr: stealthPk.toAddress(), - ephPk: ephPk, - viewTag: viewTag - }); - } - - //-------------------------------------------------------------------------- - // Receiver - - /// @dev Returns whether stealth address `stealth` belongs to the view secret - /// key `viewSk` and spend public key `spendPk`. - /// - /// @dev Note that `stealth`'s view tag MUST be correct in order for the check - /// to succeed. - /// - /// @custom:vm Secp256k1::PublicKey.toPublicKey() - /// @custom:invariant A public key's keccak256 image is never zero: - /// ∀ pk ∊ PublicKey: keccak256(pk) != 0 - function checkStealthAddress( - SecretKey viewSk, - PublicKey memory spendPk, - StealthAddress memory stealth - ) internal vmed returns (bool) { - // Compute shared secret key from view secret key and ephemeral public - // key. - SecretKey sharedSk = _deriveSharedSecret(viewSk, stealth.ephPk); - - // Extract view tag from shared secret key. - uint8 viewTag = _extractViewTag(sharedSk); - - // Return early if view tags do not match. - if (viewTag != stealth.viewTag) { - return false; - } - - // Derive shared secret key's public key. - PublicKey memory sharedPk = sharedSk.toPublicKey(); - - // Compute stealth address' public key. - // forgefmt: disable-next-item - PublicKey memory stealthPk = spendPk.toProjectivePoint() - .add(sharedPk.toProjectivePoint()) - .intoPoint() - .intoPublicKey(); - - // Return true if computed address matches stealth address' address. - return stealthPk.toAddress() == stealth.addr; - } - - /// @dev Computes the secret key for stealth address `stealth` from the - /// spend and view secret keys. - /// - /// @dev Note that the stealth address MUST belong to the spend and view - /// secret keys! - /// - /// @custom:invariant A public key's keccak256 image is never zero: - /// ∀ pk ∊ PublicKey: keccak256(pk) != 0 - function computeStealthSecretKey( - SecretKey spendSk, - SecretKey viewSk, - StealthAddress memory stealth - ) internal view returns (SecretKey) { - // Compute shared secret key from view secret key and ephemeral public - // key. - SecretKey sharedSk = _deriveSharedSecret(viewSk, stealth.ephPk); - - // Compute stealth secret key. - SecretKey stealthSk = Secp256k1.secretKeyFromUint( - addmod(spendSk.asUint(), sharedSk.asUint(), Secp256k1.Q) - ); - - return stealthSk; - } - - //-------------------------------------------------------------------------- - // Utils - - /// @dev Returns the string representation of stealth meta address - /// `stealthMeta` 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 - view - vmed - returns (string memory) - { - string memory prefix = string.concat("st:", chain, ":0x"); - - // Use hex string of 0x-removed compressed public key encoding. - bytes memory spendPk; - bytes memory viewPk; - - string memory buffer; - - buffer = vm.toString(sma.spendPk.intoPoint().toCompressedEncoded()); - spendPk = new bytes(bytes(buffer).length - 2); - for (uint i = 2; i < bytes(buffer).length; i++) { - spendPk[i - 2] = bytes(buffer)[i]; - } - - buffer = vm.toString(sma.viewPk.intoPoint().toCompressedEncoded()); - viewPk = new bytes(bytes(buffer).length - 2); - for (uint i = 2; i < bytes(buffer).length; i++) { - viewPk[i - 2] = bytes(buffer)[i]; - } - - return string.concat(prefix, string(spendPk), string(viewPk)); - } - - //-------------------------------------------------------------------------- - // Private Helpers - - /// @dev Returns a shared secret derived from secret key `sk` and public key - /// `pk`. - /// - /// @custom:invariant A public key's keccak256 image is never zero: - /// ∀ pk ∊ PublicKey: keccak256(pk) != 0 - function _deriveSharedSecret(SecretKey sk, PublicKey memory pk) - private - view - returns (SecretKey) - { - // Compute shared public key. - // forgefmt: disable-next-item - PublicKey memory sharedPk = pk.toProjectivePoint() - .mul(sk.asUint()) - .intoPoint() - .intoPublicKey(); - - // Derive secret key from hashed public key. - bytes32 digest = sharedPk.toHash(); - - // Note to bound digest to secp256k1's order in order to use it as - // secret key. - uint scalar = uint(digest) % Secp256k1.Q; - assert(scalar != 0); // Has negligible probability. - - return Secp256k1.secretKeyFromUint(scalar); - } - - /// @dev Returns the view tag of shared secret key `sharedSk`. - /// - /// @dev Note that the view tag is defined as the highest-order byte. - function _extractViewTag(SecretKey sharedSk) private pure returns (uint8) { - return uint8(sharedSk.asUint() >> 248); - } -} diff --git a/src/secp256k1/signatures/ECDSAUnsafe.sol b/src/unsafe/secp256k1/signatures/ECDSAUnsafe.sol similarity index 80% rename from src/secp256k1/signatures/ECDSAUnsafe.sol rename to src/unsafe/secp256k1/signatures/ECDSAUnsafe.sol index 49ff000..2ab5f59 100644 --- a/src/secp256k1/signatures/ECDSAUnsafe.sol +++ b/src/unsafe/secp256k1/signatures/ECDSAUnsafe.sol @@ -11,16 +11,26 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.16; -import {ECDSA, Signature} from "./ECDSA.sol"; +import { + ECDSA, Signature +} from "../../../onchain/secp256k1/signatures/ECDSA.sol"; -import {Secp256k1} from "../Secp256k1.sol"; -import {Secp256k1Arithmetic} from "../Secp256k1Arithmetic.sol"; +import {Secp256k1} from "../../../onchain/secp256k1/Secp256k1.sol"; +import {Secp256k1Arithmetic} from + "../../../onchain/secp256k1/Secp256k1Arithmetic.sol"; /** * @title ECDSAUnsafe * * @notice Library providing unsafe ECDSA functionality * + * @dev WARNING + * + * This library MUST only be used for testing, experimenting and + * researching. + * + * Under no circumstances should unsafe/ code be used in production! + * * @author crysol (https://github.com/pmerkleplant/crysol) */ library ECDSAUnsafe { diff --git a/test/examples/secp256k1/stealth-addresses/ERC5564.t.sol b/test/examples/common/Random.t.sol similarity index 53% rename from test/examples/secp256k1/stealth-addresses/ERC5564.t.sol rename to test/examples/common/Random.t.sol index 20e9e5e..cf98540 100644 --- a/test/examples/secp256k1/stealth-addresses/ERC5564.t.sol +++ b/test/examples/common/Random.t.sol @@ -3,13 +3,13 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; -import {ERC5564Example} from "examples/secp256k1/stealth-addresses/ERC5564.sol"; +import {RandomExample} from "examples/common/Random.sol"; -contract ERC5564ExamplesTest is Test { - ERC5564Example example; +contract RandomExampleTest is Test { + RandomExample example; function setUp() public { - example = new ERC5564Example(); + example = new RandomExample(); } function test_run() public { diff --git a/test/Prelude.t.sol b/test/offchain/Prelude.t.sol similarity index 99% rename from test/Prelude.t.sol rename to test/offchain/Prelude.t.sol index 270b99c..5cd0346 100644 --- a/test/Prelude.t.sol +++ b/test/offchain/Prelude.t.sol @@ -10,10 +10,8 @@ import {console2 as console} from "forge-std/console2.sol"; contract PreludeTest is Test { // ~~~~~~~ Prelude ~~~~~~~ // forgefmt: disable-start - // Note that vm is already imported via `is Test`. //Vm private constant vm = Vm(address(uint160(uint(keccak256("hevm cheat code"))))); - modifier vmed() { if (block.chainid != 31337) revert("NotVMed()"); _; diff --git a/test/common/Random.t.sol b/test/offchain/common/Random.t.sol similarity index 66% rename from test/common/Random.t.sol rename to test/offchain/common/Random.t.sol index 6f54519..f8d7a2d 100644 --- a/test/common/Random.t.sol +++ b/test/offchain/common/Random.t.sol @@ -4,16 +4,16 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Random} from "src/common/Random.sol"; +import {RandomOffchain} from "src/offchain/common/RandomOffchain.sol"; /** - * @notice Random Unit Tests + * @notice RandomOffchain Unit Tests */ -contract RandomTest is Test { - RandomWrapper wrapper; +contract RandomOffchainTest is Test { + RandomOffchainWrapper wrapper; function setUp() public { - wrapper = new RandomWrapper(); + wrapper = new RandomOffchainWrapper(); } function test_readUint() public { @@ -29,8 +29,8 @@ contract RandomTest is Test { * * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. */ -contract RandomWrapper { +contract RandomOffchainWrapper { function readUint() public returns (uint) { - return Random.readUint(); + return RandomOffchain.readUint(); } } diff --git a/test/offchain/secp256k1/Secp256k1Offchain.sol b/test/offchain/secp256k1/Secp256k1Offchain.sol new file mode 100644 index 0000000..b5dabc8 --- /dev/null +++ b/test/offchain/secp256k1/Secp256k1Offchain.sol @@ -0,0 +1,82 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; + +/** + * @notice Secp256k1Offchain Unit Tests + */ +contract Secp256k1OffchainTest is Test { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + + Secp256k1OffchainWrapper wrapper; + + function setUp() public { + wrapper = new Secp256k1OffchainWrapper(); + } + + //-------------------------------------------------------------------------- + // Test: Secret Key + + // -- newSecretKey + + function test_newSecretKey() public { + SecretKey sk = wrapper.newSecretKey(); + + assertTrue(sk.isValid()); + + // Verify vm can create wallet from secret key. + vm.createWallet(sk.asUint()); + } + + // -- toPublicKey + + function testFuzz_SecretKey_toPublicKey(uint seed) public { + SecretKey sk = + Secp256k1.secretKeyFromUint(_bound(seed, 1, Secp256k1.Q - 1)); + + address got = wrapper.toPublicKey(sk).toAddress(); + address want = vm.addr(sk.asUint()); + + assertEq(got, want); + } + + function testFuzz_SecretKey_toPublicKey_RevertsIf_SecretKeyInvalid( + uint seed + ) public { + SecretKey sk = SecretKey.wrap(_bound(seed, Secp256k1.Q, type(uint).max)); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.toPublicKey(sk); + } +} + +/** + * @notice Library wrapper to enable forge coverage reporting + * + * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. + */ +contract Secp256k1OffchainWrapper { + using Secp256k1Offchain for SecretKey; + + //-------------------------------------------------------------------------- + // Secret Key + + function newSecretKey() public returns (SecretKey) { + return Secp256k1Offchain.newSecretKey(); + } + + function toPublicKey(SecretKey sk) public returns (PublicKey memory) { + return sk.toPublicKey(); + } +} diff --git a/test/offchain/secp256k1/signatures/ECDSAOffchain.t.sol b/test/offchain/secp256k1/signatures/ECDSAOffchain.t.sol new file mode 100644 index 0000000..92593da --- /dev/null +++ b/test/offchain/secp256k1/signatures/ECDSAOffchain.t.sol @@ -0,0 +1,182 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {Message} from "src/onchain/common/Message.sol"; + +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; + +import {ECDSAOffchain} from + "src/offchain/secp256k1/signatures/ECDSAOffchain.sol"; +import {ECDSA, Signature} from "src/onchain/secp256k1/signatures/ECDSA.sol"; +import {ECDSAUnsafe} from "src/unsafe/secp256k1/signatures/ECDSAUnsafe.sol"; + +/** + * @notice ECDSAOffchain Unit Tests + */ +contract ECDSAOffchainTest is Test { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + + using ECDSA for PublicKey; + + ECDSAOffchainWrapper wrapper; + + function setUp() public { + wrapper = new ECDSAOffchainWrapper(); + } + + //-------------------------------------------------------------------------- + // Test: Signature Creation + + function testFuzz_sign(SecretKey sk, bytes memory message) public { + vm.assume(sk.isValid()); + + Signature memory sig1 = wrapper.sign(sk, message); + Signature memory sig2 = wrapper.sign(sk, keccak256(message)); + + assertEq(sig1.v, sig2.v); + assertEq(sig1.r, sig2.r); + assertEq(sig1.s, sig2.s); + + PublicKey memory pk = sk.toPublicKey(); + assertTrue(pk.verify(message, sig1)); + assertTrue(pk.verify(message, sig2)); + } + + function testFuzz_sign_RevertsIf_SecretKeyInvalid( + SecretKey sk, + bytes memory message + ) public { + vm.assume(!sk.isValid()); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.sign(sk, message); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.sign(sk, keccak256(message)); + } + + function testFuzz_signEthereumSignedMessageHash( + SecretKey sk, + bytes memory message + ) public { + vm.assume(sk.isValid()); + + Signature memory sig1 = + wrapper.signEthereumSignedMessageHash(sk, message); + Signature memory sig2 = + wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); + + assertEq(sig1.v, sig2.v); + assertEq(sig1.r, sig2.r); + assertEq(sig1.s, sig2.s); + + PublicKey memory pk = sk.toPublicKey(); + assertTrue( + pk.verify(Message.deriveEthereumSignedMessageHash(message), sig1) + ); + assertTrue( + pk.verify( + Message.deriveEthereumSignedMessageHash(keccak256(message)), + sig2 + ) + ); + } + + function testFuzz_signEthereumSignedMessageHash_RevertsIf_SecretKeyInvalid( + SecretKey sk, + bytes memory message + ) public { + vm.assume(!sk.isValid()); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.signEthereumSignedMessageHash(sk, message); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); + } + + //-------------------------------------------------------------------------- + // Test: Utils + + // -- Signature::toString + + function test_Signature_toString() public { + Signature memory sig = Signature({ + v: 27, + r: bytes32(type(uint).max), + s: bytes32(type(uint).max) + }); + + string memory got = wrapper.toString(sig); + string memory want = + "ECDSA::Signature({ v: 27, r: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, s: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff })"; + + assertEq(got, want); + } +} + +/** + * @notice Library wrapper to enable forge coverage reporting + * + * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. + */ +contract ECDSAOffchainWrapper { + using ECDSAOffchain for SecretKey; + using ECDSAOffchain for Signature; + + //-------------------------------------------------------------------------- + // Signature Creation + + function sign(SecretKey sk, bytes memory message) + public + view + returns (Signature memory) + { + return sk.sign(message); + } + + function sign(SecretKey sk, bytes32 digest) + public + view + returns (Signature memory) + { + return sk.sign(digest); + } + + function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) + public + view + returns (Signature memory) + { + return sk.signEthereumSignedMessageHash(message); + } + + function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) + public + view + returns (Signature memory) + { + return sk.signEthereumSignedMessageHash(digest); + } + + //-------------------------------------------------------------------------- + // Utils + + function toString(Signature memory sig) + public + view + returns (string memory) + { + return sig.toString(); + } +} diff --git a/test/offchain/secp256k1/signatures/SchnorrOffchain.t.sol b/test/offchain/secp256k1/signatures/SchnorrOffchain.t.sol new file mode 100644 index 0000000..c4b471e --- /dev/null +++ b/test/offchain/secp256k1/signatures/SchnorrOffchain.t.sol @@ -0,0 +1,173 @@ +// SPDX-License-Identifier: MIT OR Apache-2.0 +pragma solidity ^0.8.16; + +import {Test} from "forge-std/Test.sol"; +import {console2 as console} from "forge-std/console2.sol"; + +import {Message} from "src/onchain/common/Message.sol"; + +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; + +import {SchnorrOffchain} from + "src/offchain/secp256k1/signatures/SchnorrOffchain.sol"; +import { + Schnorr, Signature +} from "src/onchain/secp256k1/signatures/Schnorr.sol"; + +/** + * @notice SchnorrOffchain Unit Tests + */ +contract SchnorrOffchainTest is Test { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + using Secp256k1 for PublicKey; + + using Schnorr for PublicKey; + + SchnorrOffchainWrapper wrapper; + + function setUp() public { + wrapper = new SchnorrOffchainWrapper(); + } + + //-------------------------------------------------------------------------- + // Test: Signature Creation + + function testFuzz_sign(SecretKey sk, bytes memory message) public { + vm.assume(sk.isValid()); + + Signature memory sig1 = wrapper.sign(sk, message); + Signature memory sig2 = wrapper.sign(sk, keccak256(message)); + + assertEq(sig1.signature, sig2.signature); + assertEq(sig1.commitment, sig2.commitment); + + PublicKey memory pk = sk.toPublicKey(); + assertTrue(pk.verify(message, sig1)); + assertTrue(pk.verify(message, sig2)); + } + + function testFuzz_sign_RevertsIf_SecretKeyInvalid( + SecretKey sk, + bytes memory message + ) public { + vm.assume(!sk.isValid()); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.sign(sk, message); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.sign(sk, keccak256(message)); + } + + function testFuzz_signEthereumSignedMessageHash( + SecretKey sk, + bytes memory message + ) public { + vm.assume(sk.isValid()); + + Signature memory sig1 = + wrapper.signEthereumSignedMessageHash(sk, message); + Signature memory sig2 = + wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); + + assertEq(sig1.signature, sig2.signature); + assertEq(sig1.commitment, sig2.commitment); + + PublicKey memory pk = sk.toPublicKey(); + assertTrue( + pk.verify(Message.deriveEthereumSignedMessageHash(message), sig1) + ); + assertTrue( + pk.verify(Message.deriveEthereumSignedMessageHash(message), sig2) + ); + } + + function testFuzz_signEthereumSignedMessageHash_RevertsIf_SecretKeyInvalid( + SecretKey sk, + bytes memory message + ) public { + vm.assume(!sk.isValid()); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.signEthereumSignedMessageHash(sk, message); + + vm.expectRevert("SecretKeyInvalid()"); + wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); + } + + //-------------------------------------------------------------------------- + // Test: Utils + + // -- Signature::toString + + function test_Signature_toString() public { + Signature memory sig = Signature({ + signature: bytes32(type(uint).max), + commitment: address(0x0) + }); + + string memory got = wrapper.toString(sig); + string memory want = + "Schnorr::Signature({ signature: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, commitment: 0x0000000000000000000000000000000000000000 })"; + + assertEq(got, want); + } +} + +/** + * @notice Library wrapper to enable forge coverage reporting + * + * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. + */ +contract SchnorrOffchainWrapper { + using SchnorrOffchain for SecretKey; + using SchnorrOffchain for Signature; + + //-------------------------------------------------------------------------- + // Signature Creation + + function sign(SecretKey sk, bytes memory message) + public + returns (Signature memory) + { + return sk.sign(message); + } + + function sign(SecretKey sk, bytes32 digest) + public + returns (Signature memory) + { + return sk.sign(digest); + } + + function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) + public + returns (Signature memory) + { + return sk.signEthereumSignedMessageHash(message); + } + + function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) + public + returns (Signature memory) + { + return sk.signEthereumSignedMessageHash(digest); + } + + //-------------------------------------------------------------------------- + // Utils + + function toString(Signature memory sig) + public + view + returns (string memory) + { + return sig.toString(); + } +} diff --git a/test/common/Message.t.sol b/test/onchain/common/Message.t.sol similarity index 96% rename from test/common/Message.t.sol rename to test/onchain/common/Message.t.sol index 2ce2e7c..7412336 100644 --- a/test/common/Message.t.sol +++ b/test/onchain/common/Message.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 {Message} from "src/common/Message.sol"; +import {Message} from "src/onchain/common/Message.sol"; /** * @notice Message Unit Tests diff --git a/test/common/Nonce.t.sol b/test/onchain/common/Nonce.t.sol similarity index 96% rename from test/common/Nonce.t.sol rename to test/onchain/common/Nonce.t.sol index cdac87c..6850156 100644 --- a/test/common/Nonce.t.sol +++ b/test/onchain/common/Nonce.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 {Nonce} from "src/common/Nonce.sol"; +import {Nonce} from "src/onchain/common/Nonce.sol"; /** * @notice Nonce Unit Tests diff --git a/test/secp256k1/Secp256k1.t.sol b/test/onchain/secp256k1/Secp256k1.t.sol similarity index 89% rename from test/secp256k1/Secp256k1.t.sol rename to test/onchain/secp256k1/Secp256k1.t.sol index a6c63c8..263026d 100644 --- a/test/secp256k1/Secp256k1.t.sol +++ b/test/onchain/secp256k1/Secp256k1.t.sol @@ -4,21 +4,26 @@ 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/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; import { Secp256k1Arithmetic, Point, ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; +} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; /** * @notice Secp256k1 Unit Tests */ contract Secp256k1Test is Test { + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; using Secp256k1 for Point; - using Secp256k1Arithmetic for Point; using Secp256k1Arithmetic for ProjectivePoint; @@ -48,17 +53,6 @@ contract Secp256k1Test is Test { //-------------------------------------------------------------------------- // Test: Secret Key - // -- newSecretKey - - function test_newSecretKey() public { - SecretKey sk = wrapper.newSecretKey(); - - assertTrue(sk.isValid()); - - // Verify vm can create wallet from secret key. - vm.createWallet(sk.asUint()); - } - // -- isValid function testFuzz_SecretKey_isValid(uint seed) public { @@ -79,27 +73,6 @@ contract Secp256k1Test is Test { assertFalse(wrapper.isValid(SecretKey.wrap(scalar))); } - // -- toPublicKey - - function testFuzz_SecretKey_toPublicKey(uint seed) public { - SecretKey sk = - Secp256k1.secretKeyFromUint(_bound(seed, 1, Secp256k1.Q - 1)); - - address got = wrapper.toPublicKey(sk).toAddress(); - address want = vm.addr(sk.asUint()); - - assertEq(got, want); - } - - function testFuzz_SecretKey_toPublicKey_RevertsIf_SecretKeyInvalid( - uint seed - ) public { - SecretKey sk = SecretKey.wrap(_bound(seed, Secp256k1.Q, type(uint).max)); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.toPublicKey(sk); - } - // -- secretKeyFromUint function testFuzz_secretKeyFromUint(uint seed) public { @@ -140,7 +113,7 @@ contract Secp256k1Test is Test { SecretKey sk = Secp256k1.secretKeyFromUint(_bound(seed, 1, Secp256k1.Q - 1)); - address got = wrapper.toAddress(Secp256k1.toPublicKey(sk)); + address got = wrapper.toAddress(sk.toPublicKey()); address want = vm.addr(sk.asUint()); assertEq(got, want); @@ -333,18 +306,10 @@ contract Secp256k1Wrapper { //-------------------------------------------------------------------------- // Secret Key - function newSecretKey() public returns (SecretKey) { - return Secp256k1.newSecretKey(); - } - function isValid(SecretKey sk) public pure returns (bool) { return sk.isValid(); } - function toPublicKey(SecretKey sk) public returns (PublicKey memory) { - return sk.toPublicKey(); - } - function secretKeyFromUint(uint scalar) public pure returns (SecretKey) { return Secp256k1.secretKeyFromUint(scalar); } diff --git a/test/secp256k1/Secp256k1Arithmetic.t.sol b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol similarity index 97% rename from test/secp256k1/Secp256k1Arithmetic.t.sol rename to test/onchain/secp256k1/Secp256k1Arithmetic.t.sol index 1b5bfe8..cd1c5b0 100644 --- a/test/secp256k1/Secp256k1Arithmetic.t.sol +++ b/test/onchain/secp256k1/Secp256k1Arithmetic.t.sol @@ -4,12 +4,17 @@ 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/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; import { Secp256k1Arithmetic, Point, ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; +} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; import {Secp256k1ArithmeticTestVectors} from "./test-vectors/Secp256k1ArithmeticTestVectors.sol"; @@ -18,11 +23,11 @@ import {Secp256k1ArithmeticTestVectors} from * @notice Secp256k1Arithmetic Unit Tests */ contract Secp256k1ArithmeticTest is Test { - using Secp256k1Arithmetic for Point; - using Secp256k1Arithmetic for ProjectivePoint; - + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; + using Secp256k1Arithmetic for Point; + using Secp256k1Arithmetic for ProjectivePoint; // Uncompressed Generator G. // Copied from [SEC-2 v2]. @@ -332,10 +337,10 @@ contract Secp256k1ArithmeticTest is Test { Point({ x: uint( 0x1111111111111111111111111111111111111111111111111111111111111111 - ), + ), y: uint( 0x2222222222222222222222222222222222222222222222222222222222222222 - ) + ) }) ) ); @@ -384,10 +389,10 @@ contract Secp256k1ArithmeticTest is Test { point = Point({ x: uint( 0x1111111111111111111111111111111111111111111111111111111111111111 - ), + ), y: uint( 0x2222222222222222222222222222222222222222222222222222222222222222 - ) + ) }); blob = wrapper.toEncoded(point); assertEq( @@ -470,7 +475,7 @@ contract Secp256k1ArithmeticTest is Test { Point memory point = Point({ x: uint( 0x1111111111111111111111111111111111111111111111111111111111111111 - ), + ), y: uint(2) }); bytes memory blob = wrapper.toCompressedEncoded(point); @@ -485,7 +490,7 @@ contract Secp256k1ArithmeticTest is Test { Point memory point = Point({ x: uint( 0x1111111111111111111111111111111111111111111111111111111111111111 - ), + ), y: uint(3) }); bytes memory blob = wrapper.toCompressedEncoded(point); diff --git a/test/secp256k1/Secp256k1ArithmeticProperties.t.sol b/test/onchain/secp256k1/Secp256k1ArithmeticProperties.t.sol similarity index 92% rename from test/secp256k1/Secp256k1ArithmeticProperties.t.sol rename to test/onchain/secp256k1/Secp256k1ArithmeticProperties.t.sol index 7c60c0d..f59c820 100644 --- a/test/secp256k1/Secp256k1ArithmeticProperties.t.sol +++ b/test/onchain/secp256k1/Secp256k1ArithmeticProperties.t.sol @@ -4,21 +4,26 @@ 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/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; import { Secp256k1Arithmetic, Point, ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; +} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; /** * @notice Secp256k1Arithmetic Property Tests */ contract Secp256k1ArithmeticPropertiesTest is Test { + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; using Secp256k1 for Point; - using Secp256k1Arithmetic for Point; using Secp256k1Arithmetic for ProjectivePoint; diff --git a/test/secp256k1/Secp256k1Properties.t.sol b/test/onchain/secp256k1/Secp256k1Properties.t.sol similarity index 87% rename from test/secp256k1/Secp256k1Properties.t.sol rename to test/onchain/secp256k1/Secp256k1Properties.t.sol index 998b869..8ccffce 100644 --- a/test/secp256k1/Secp256k1Properties.t.sol +++ b/test/onchain/secp256k1/Secp256k1Properties.t.sol @@ -4,12 +4,16 @@ 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/secp256k1/Secp256k1.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; import { Secp256k1Arithmetic, Point, ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; +} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; /** * @notice Secp256k1 Property Tests diff --git a/test/secp256k1/signatures/ECDSA.t.sol b/test/onchain/secp256k1/signatures/ECDSA.t.sol similarity index 72% rename from test/secp256k1/signatures/ECDSA.t.sol rename to test/onchain/secp256k1/signatures/ECDSA.t.sol index d61a560..ede8e49 100644 --- a/test/secp256k1/signatures/ECDSA.t.sol +++ b/test/onchain/secp256k1/signatures/ECDSA.t.sol @@ -4,17 +4,21 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {ECDSA, Signature} from "src/secp256k1/signatures/ECDSA.sol"; -import {ECDSAUnsafe} from "src/secp256k1/signatures/ECDSAUnsafe.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; - -import {Message} from "src/common/Message.sol"; +import {ECDSA, Signature} from "src/onchain/secp256k1/signatures/ECDSA.sol"; +import {ECDSAUnsafe} from "src/unsafe/secp256k1/signatures/ECDSAUnsafe.sol"; /** - * @notice Secp256k1 ECDSA Unit Tests + * @notice ECDSA Unit Tests */ contract ECDSATest is Test { + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; @@ -22,7 +26,6 @@ contract ECDSATest is Test { using ECDSA for SecretKey; using ECDSA for PublicKey; using ECDSA for Signature; - using ECDSAUnsafe for Signature; ECDSAWrapper wrapper; @@ -142,77 +145,6 @@ contract ECDSATest is Test { wrapper.verify(signer, keccak256(message), sig); } - //-------------------------------------------------------------------------- - // Test: Signature Creation - - function testFuzz_sign(SecretKey sk, bytes memory message) public { - vm.assume(sk.isValid()); - - Signature memory sig1 = wrapper.sign(sk, message); - Signature memory sig2 = wrapper.sign(sk, keccak256(message)); - - assertEq(sig1.v, sig2.v); - assertEq(sig1.r, sig2.r); - assertEq(sig1.s, sig2.s); - - PublicKey memory pk = sk.toPublicKey(); - assertTrue(pk.verify(message, sig1)); - assertTrue(pk.verify(message, sig2)); - } - - function testFuzz_sign_RevertsIf_SecretKeyInvalid( - SecretKey sk, - bytes memory message - ) public { - vm.assume(!sk.isValid()); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.sign(sk, message); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.sign(sk, keccak256(message)); - } - - function testFuzz_signEthereumSignedMessageHash( - SecretKey sk, - bytes memory message - ) public { - vm.assume(sk.isValid()); - - Signature memory sig1 = - wrapper.signEthereumSignedMessageHash(sk, message); - Signature memory sig2 = - wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); - - assertEq(sig1.v, sig2.v); - assertEq(sig1.r, sig2.r); - assertEq(sig1.s, sig2.s); - - PublicKey memory pk = sk.toPublicKey(); - assertTrue( - pk.verify(Message.deriveEthereumSignedMessageHash(message), sig1) - ); - assertTrue( - pk.verify( - Message.deriveEthereumSignedMessageHash(keccak256(message)), - sig2 - ) - ); - } - - function testFuzz_signEthereumSignedMessageHash_RevertsIf_SecretKeyInvalid( - SecretKey sk, - bytes memory message - ) public { - vm.assume(!sk.isValid()); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.signEthereumSignedMessageHash(sk, message); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); - } - //-------------------------------------------------------------------------- // Test: Utils @@ -232,22 +164,6 @@ contract ECDSATest is Test { assertFalse(wrapper.isMalleable(sig)); } - // -- Signature::toString - - function test_Signature_toString() public { - Signature memory sig = Signature({ - v: 27, - r: bytes32(type(uint).max), - s: bytes32(type(uint).max) - }); - - string memory got = wrapper.toString(sig); - string memory want = - "ECDSA::Signature({ v: 27, r: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, s: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff })"; - - assertEq(got, want); - } - //-------------------------------------------------------------------------- // Test: (De)Serialization @@ -403,41 +319,6 @@ contract ECDSAWrapper { return signer.verify(digest, sig); } - //-------------------------------------------------------------------------- - // Signature Creation - - function sign(SecretKey sk, bytes memory message) - public - view - returns (Signature memory) - { - return sk.sign(message); - } - - function sign(SecretKey sk, bytes32 digest) - public - view - returns (Signature memory) - { - return sk.sign(digest); - } - - function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) - public - view - returns (Signature memory) - { - return sk.signEthereumSignedMessageHash(message); - } - - function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) - public - view - returns (Signature memory) - { - return sk.signEthereumSignedMessageHash(digest); - } - //-------------------------------------------------------------------------- // Utils @@ -445,14 +326,6 @@ contract ECDSAWrapper { return sig.isMalleable(); } - function toString(Signature memory sig) - public - view - returns (string memory) - { - return sig.toString(); - } - //-------------------------------------------------------------------------- // (De)Serialization diff --git a/test/secp256k1/signatures/ECDSAProperties.t.sol b/test/onchain/secp256k1/signatures/ECDSAProperties.t.sol similarity index 84% rename from test/secp256k1/signatures/ECDSAProperties.t.sol rename to test/onchain/secp256k1/signatures/ECDSAProperties.t.sol index 8053f84..1d5861e 100644 --- a/test/secp256k1/signatures/ECDSAProperties.t.sol +++ b/test/onchain/secp256k1/signatures/ECDSAProperties.t.sol @@ -4,20 +4,29 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {ECDSA, Signature} from "src/secp256k1/signatures/ECDSA.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; +import {ECDSAOffchain} from + "src/offchain/secp256k1/signatures/ECDSAOffchain.sol"; +import {ECDSA, Signature} from "src/onchain/secp256k1/signatures/ECDSA.sol"; /** * @notice Secp256k1 ECDSA Property Tests */ contract ECDSAPropertiesTest is Test { + using Secp256k1Offchain for SecretKey; + using Secp256k1 for SecretKey; + + using ECDSAOffchain for SecretKey; using ECDSA for SecretKey; using ECDSA for PublicKey; using ECDSA for Signature; - using Secp256k1 for SecretKey; - //-------------------------------------------------------------------------- // Properties: Signature diff --git a/test/secp256k1/signatures/Schnorr.t.sol b/test/onchain/secp256k1/signatures/Schnorr.t.sol similarity index 56% rename from test/secp256k1/signatures/Schnorr.t.sol rename to test/onchain/secp256k1/signatures/Schnorr.t.sol index f111baa..acf9d32 100644 --- a/test/secp256k1/signatures/Schnorr.t.sol +++ b/test/onchain/secp256k1/signatures/Schnorr.t.sol @@ -4,19 +4,30 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Schnorr, Signature} from "src/secp256k1/signatures/Schnorr.sol"; +import {Message} from "src/onchain/common/Message.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; -import {Message} from "src/common/Message.sol"; +import { + Schnorr, Signature +} from "src/onchain/secp256k1/signatures/Schnorr.sol"; +import {SchnorrOffchain} from + "src/offchain/secp256k1/signatures/SchnorrOffchain.sol"; /** * @notice Schnorr Unit Tests */ contract SchnorrTest is Test { + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; using Secp256k1 for PublicKey; + using SchnorrOffchain for SecretKey; using Schnorr for SecretKey; using Schnorr for PublicKey; using Schnorr for Signature; @@ -114,72 +125,6 @@ contract SchnorrTest is Test { wrapper.verify(pk, keccak256(message), sig); } - //-------------------------------------------------------------------------- - // Test: Signature Creation - - function testFuzz_sign(SecretKey sk, bytes memory message) public { - vm.assume(sk.isValid()); - - Signature memory sig1 = wrapper.sign(sk, message); - Signature memory sig2 = wrapper.sign(sk, keccak256(message)); - - assertEq(sig1.signature, sig2.signature); - assertEq(sig1.commitment, sig2.commitment); - - PublicKey memory pk = sk.toPublicKey(); - assertTrue(pk.verify(message, sig1)); - assertTrue(pk.verify(message, sig2)); - } - - function testFuzz_sign_RevertsIf_SecretKeyInvalid( - SecretKey sk, - bytes memory message - ) public { - vm.assume(!sk.isValid()); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.sign(sk, message); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.sign(sk, keccak256(message)); - } - - function testFuzz_signEthereumSignedMessageHash( - SecretKey sk, - bytes memory message - ) public { - vm.assume(sk.isValid()); - - Signature memory sig1 = - wrapper.signEthereumSignedMessageHash(sk, message); - Signature memory sig2 = - wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); - - assertEq(sig1.signature, sig2.signature); - assertEq(sig1.commitment, sig2.commitment); - - PublicKey memory pk = sk.toPublicKey(); - assertTrue( - pk.verify(Message.deriveEthereumSignedMessageHash(message), sig1) - ); - assertTrue( - pk.verify(Message.deriveEthereumSignedMessageHash(message), sig2) - ); - } - - function testFuzz_signEthereumSignedMessageHash_RevertsIf_SecretKeyInvalid( - SecretKey sk, - bytes memory message - ) public { - vm.assume(!sk.isValid()); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.signEthereumSignedMessageHash(sk, message); - - vm.expectRevert("SecretKeyInvalid()"); - wrapper.signEthereumSignedMessageHash(sk, keccak256(message)); - } - //-------------------------------------------------------------------------- // Test: Utils @@ -199,21 +144,6 @@ contract SchnorrTest is Test { assertFalse(wrapper.isMalleable(sig)); } - - // -- Signature::toString - - function test_Signature_toString() public { - Signature memory sig = Signature({ - signature: bytes32(type(uint).max), - commitment: address(0x0) - }); - - string memory got = wrapper.toString(sig); - string memory want = - "Schnorr::Signature({ signature: 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff, commitment: 0x0000000000000000000000000000000000000000 })"; - - assertEq(got, want); - } } /** @@ -245,49 +175,10 @@ contract SchnorrWrapper { return pk.verify(digest, sig); } - //-------------------------------------------------------------------------- - // Signature Creation - - function sign(SecretKey sk, bytes memory message) - public - returns (Signature memory) - { - return sk.sign(message); - } - - function sign(SecretKey sk, bytes32 digest) - public - returns (Signature memory) - { - return sk.sign(digest); - } - - function signEthereumSignedMessageHash(SecretKey sk, bytes memory message) - public - returns (Signature memory) - { - return sk.signEthereumSignedMessageHash(message); - } - - function signEthereumSignedMessageHash(SecretKey sk, bytes32 digest) - public - returns (Signature memory) - { - return sk.signEthereumSignedMessageHash(digest); - } - //-------------------------------------------------------------------------- // Utils function isMalleable(Signature memory sig) public pure returns (bool) { return sig.isMalleable(); } - - function toString(Signature memory sig) - public - view - returns (string memory) - { - return sig.toString(); - } } diff --git a/test/secp256k1/signatures/SchnorrProperties.t.sol b/test/onchain/secp256k1/signatures/SchnorrProperties.t.sol similarity index 78% rename from test/secp256k1/signatures/SchnorrProperties.t.sol rename to test/onchain/secp256k1/signatures/SchnorrProperties.t.sol index 0582194..66a66a0 100644 --- a/test/secp256k1/signatures/SchnorrProperties.t.sol +++ b/test/onchain/secp256k1/signatures/SchnorrProperties.t.sol @@ -4,16 +4,27 @@ pragma solidity ^0.8.16; import {Test} from "forge-std/Test.sol"; import {console2 as console} from "forge-std/console2.sol"; -import {Schnorr, Signature} from "src/secp256k1/signatures/Schnorr.sol"; +import {Secp256k1Offchain} from "src/offchain/secp256k1/Secp256k1Offchain.sol"; +import { + Secp256k1, + SecretKey, + PublicKey +} from "src/onchain/secp256k1/Secp256k1.sol"; -import {Secp256k1, SecretKey, PublicKey} from "src/secp256k1/Secp256k1.sol"; +import { + Schnorr, Signature +} from "src/onchain/secp256k1/signatures/Schnorr.sol"; +import {SchnorrOffchain} from + "src/offchain/secp256k1/signatures/SchnorrOffchain.sol"; /** * @notice Schnorr Property Tests */ contract SchnorrPropertiesTest is Test { + using Secp256k1Offchain for SecretKey; using Secp256k1 for SecretKey; + using SchnorrOffchain for SecretKey; using Schnorr for SecretKey; using Schnorr for PublicKey; using Schnorr for Signature; diff --git a/test/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol b/test/onchain/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol similarity index 99% rename from test/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol rename to test/onchain/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol index 5b1a6e2..0d79bf1 100644 --- a/test/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol +++ b/test/onchain/secp256k1/test-vectors/Secp256k1ArithmeticTestVectors.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: MIT OR Apache-2.0 pragma solidity ^0.8.16; -import {Point} from "src/secp256k1/Secp256k1Arithmetic.sol"; +import {Point} from "src/onchain/secp256k1/Secp256k1Arithmetic.sol"; library Secp256k1ArithmeticTestVectors { /// @dev Returns a list of points being the result of repeated additions diff --git a/test/secp256k1/stealth-addresses/ERC5564.t.sol b/test/secp256k1/stealth-addresses/ERC5564.t.sol deleted file mode 100644 index 7cbe19e..0000000 --- a/test/secp256k1/stealth-addresses/ERC5564.t.sol +++ /dev/null @@ -1,244 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -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/secp256k1/Secp256k1.sol"; -import { - Secp256k1Arithmetic, - Point, - ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; - -import { - ERC5564, - StealthMetaAddress, - StealthAddress -} from "src/secp256k1/stealth-addresses/ERC5564.sol"; - -/** - * @notice ERC-5564 Unit Tests - */ -contract ERC5564Test is Test { - using Secp256k1 for SecretKey; - using Secp256k1 for PublicKey; - using Secp256k1 for Point; - - using Secp256k1Arithmetic for Point; - using Secp256k1Arithmetic for ProjectivePoint; - - using ERC5564 for SecretKey; - using ERC5564 for StealthMetaAddress; - - StealthAddressesSecp256k1Wrapper wrapper; - - function setUp() public { - wrapper = new StealthAddressesSecp256k1Wrapper(); - } - - //-------------------------------------------------------------------------- - // Sender - - function test_generateStealthAddress_GivenEph() public { - // Taken from: https://github.com/nerolation/executable-stealth-address-specs/blob/main/test.ipynb - - // Stealth meta address and respective key pairs. - SecretKey spendSk = Secp256k1.secretKeyFromUint( - 30787322447577792890566286485782027903969759412226064433999487819529647462924 - ); - SecretKey viewSk = Secp256k1.secretKeyFromUint( - 50431308649801251425320023123245644035351225602185776979597242007527042324186 - ); - StealthMetaAddress memory stealthMeta = StealthMetaAddress({ - spendPk: spendSk.toPublicKey(), - viewPk: viewSk.toPublicKey() - }); - - // Generate stealth address from stealth meta address. - StealthAddress memory stealth; - stealth = wrapper.generateStealthAddress( - stealthMeta, - Secp256k1.secretKeyFromUint( - 31582853143040820948875942041653389873450407831047855470517498178324574486065 - ) - ); - - address wantAddr = address(0xc7c1BBf258340E551061E7D561798555aA871c0d); - assertEq(stealth.addr, wantAddr); - } - - //-------------------------------------------------------------------------- - // Receiver - - function test_checkStealthAddress() public { - // Taken from: https://github.com/nerolation/executable-stealth-address-specs/blob/main/test.ipynb - - SecretKey spendSk = Secp256k1.secretKeyFromUint( - 30787322447577792890566286485782027903969759412226064433999487819529647462924 - ); - SecretKey viewSk = Secp256k1.secretKeyFromUint( - 50431308649801251425320023123245644035351225602185776979597242007527042324186 - ); - - StealthAddress memory stealth; - stealth = StealthAddress({ - addr: address(0xc7c1BBf258340E551061E7D561798555aA871c0d), - ephPk: PublicKey({ - x: uint( - 99931485108758068354634100015529707565438847495649276196131125998359569029703 - ), - y: uint( - 4744375390796532504618795785909610189099640957761399522523575349957196497592 - ) - }), - viewTag: uint8(0x3d) // 0x3d = 61 - }); - - bool found = - wrapper.checkStealthAddress(viewSk, spendSk.toPublicKey(), stealth); - assertTrue(found); - } - - function test_checkStealthAddress_FailsIf_ViewTagIncorrect() public { - // Taken from: https://github.com/nerolation/executable-stealth-address-specs/blob/main/test.ipynb - - SecretKey spendSk = Secp256k1.secretKeyFromUint( - 30787322447577792890566286485782027903969759412226064433999487819529647462924 - ); - SecretKey viewSk = Secp256k1.secretKeyFromUint( - 50431308649801251425320023123245644035351225602185776979597242007527042324186 - ); - - StealthAddress memory stealth; - stealth = StealthAddress({ - addr: address(0xc7c1BBf258340E551061E7D561798555aA871c0d), - ephPk: PublicKey({ - x: uint( - 99931485108758068354634100015529707565438847495649276196131125998359569029703 - ), - y: uint( - 4744375390796532504618795785909610189099640957761399522523575349957196497592 - ) - }), - viewTag: uint8(0x3d) // 0x3d = 61 - }); - - // Note to set incorrect view tag. - stealth.viewTag = uint8(0x00); - - bool found = - wrapper.checkStealthAddress(viewSk, spendSk.toPublicKey(), stealth); - assertFalse(found); - } - - function test_computeStealthSecretKey() public { - // Taken from: https://github.com/nerolation/executable-stealth-address-specs/blob/main/test.ipynb - - SecretKey spendSk = Secp256k1.secretKeyFromUint( - 30787322447577792890566286485782027903969759412226064433999487819529647462924 - ); - SecretKey viewSk = Secp256k1.secretKeyFromUint( - 50431308649801251425320023123245644035351225602185776979597242007527042324186 - ); - - StealthAddress memory stealth; - stealth = StealthAddress({ - addr: address(0xc7c1BBf258340E551061E7D561798555aA871c0d), - ephPk: PublicKey({ - x: uint( - 99931485108758068354634100015529707565438847495649276196131125998359569029703 - ), - y: uint( - 4744375390796532504618795785909610189099640957761399522523575349957196497592 - ) - }), - viewTag: uint8(0x3d) // 0x3d = 61 - }); - - SecretKey gotSk = - wrapper.computeStealthSecretKey(spendSk, viewSk, stealth); - SecretKey wantSk = Secp256k1.secretKeyFromUint( - 0x81c527d561a196132fe18f2242385e4cdac91990657021cd0cee71a24d55242e - ); - assertEq(gotSk.asUint(), wantSk.asUint()); - } - - function test_StealthMetaAddress_toString() public { - // Taken from: https://github.com/nerolation/executable-stealth-address-specs/blob/main/test.ipynb - - PublicKey memory spendPk = PublicKey({ - x: 101360329545495956162666051930186878698033955801916540340568215465424285633263, - y: 27884173484063268355525586231115143741771553385896109414861147204858225531545 - }); - PublicKey memory viewPk = PublicKey({ - x: 12497814997365815068905527286060252467359539672611551375389366654292063092228, - y: 6165085391294201611990159913274691549635337727676630133767399716897791323976 - }); - - StealthMetaAddress memory stealthMeta = - StealthMetaAddress(spendPk, viewPk); - - string memory got = wrapper.toString(stealthMeta, "eth"); - string memory want = - "st:eth:0x03e017e9d9dbcb9ce5771acfce74c95bc0eafb5db37ef4b1ac62375f8e7a4c8aef021ba1833a9575bd2ad924440a20a80417437f77b0539cbc3f5bbaeeb2881efe04"; - - assertEq(got, want); - } -} - -/** - * @notice Library wrapper to enable forge coverage reporting - * - * @dev For more info, see https://github.com/foundry-rs/foundry/pull/3128#issuecomment-1241245086. - */ -contract StealthAddressesSecp256k1Wrapper { - using ERC5564 for SecretKey; - using ERC5564 for StealthMetaAddress; - - //-------------------------------------------------------------------------- - // Sender - - function generateStealthAddress(StealthMetaAddress memory stealthMeta) - public - returns (StealthAddress memory) - { - return stealthMeta.generateStealthAddress(); - } - - function generateStealthAddress( - StealthMetaAddress memory stealthMeta, - SecretKey ephSk - ) public returns (StealthAddress memory) { - return stealthMeta.generateStealthAddress(ephSk); - } - - //-------------------------------------------------------------------------- - // Receiver - - function checkStealthAddress( - SecretKey viewSk, - PublicKey memory spendPk, - StealthAddress memory stealth - ) public returns (bool) { - return viewSk.checkStealthAddress(spendPk, stealth); - } - - function computeStealthSecretKey( - SecretKey spendSk, - SecretKey viewSk, - StealthAddress memory stealth - ) public view returns (SecretKey) { - return spendSk.computeStealthSecretKey(viewSk, stealth); - } - - //-------------------------------------------------------------------------- - // Utils - - function toString( - StealthMetaAddress memory stealthMeta, - string memory chain - ) public view returns (string memory) { - return stealthMeta.toString(chain); - } -} diff --git a/test/secp256k1/stealth-addresses/ERC5564Properties.t.sol b/test/secp256k1/stealth-addresses/ERC5564Properties.t.sol deleted file mode 100644 index 4c54f57..0000000 --- a/test/secp256k1/stealth-addresses/ERC5564Properties.t.sol +++ /dev/null @@ -1,91 +0,0 @@ -// SPDX-License-Identifier: MIT OR Apache-2.0 -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/secp256k1/Secp256k1.sol"; -import { - Secp256k1Arithmetic, - Point, - ProjectivePoint -} from "src/secp256k1/Secp256k1Arithmetic.sol"; - -import { - ERC5564, - StealthMetaAddress, - StealthAddress -} from "src/secp256k1/stealth-addresses/ERC5564.sol"; - -contract StealthAddressesSecp256k1PropertiesTest is Test { - using Secp256k1 for SecretKey; - using Secp256k1 for PublicKey; - using Secp256k1 for Point; - - using Secp256k1Arithmetic for Point; - using Secp256k1Arithmetic for ProjectivePoint; - - using ERC5564 for SecretKey; - using ERC5564 for StealthMetaAddress; - - function testProperty_generateStealthAddress_GivenEphSk_IsDeterministic( - SecretKey viewSk, - SecretKey spendSk, - SecretKey ephSk - ) public { - vm.assume(viewSk.isValid()); - vm.assume(spendSk.isValid()); - vm.assume(ephSk.isValid()); - - StealthMetaAddress memory stealthMeta = - StealthMetaAddress(spendSk.toPublicKey(), viewSk.toPublicKey()); - - StealthAddress memory stealth1 = - stealthMeta.generateStealthAddress(ephSk); - - StealthAddress memory stealth2 = - stealthMeta.generateStealthAddress(ephSk); - - assertEq(stealth1.addr, stealth2.addr); - } - - function testProperty_Receiver_CanSuccessfullyCheckStealthAddress( - SecretKey viewSk, - SecretKey spendSk, - SecretKey ephSk - ) public { - vm.assume(viewSk.isValid()); - vm.assume(spendSk.isValid()); - vm.assume(ephSk.isValid()); - - StealthMetaAddress memory stealthMeta = - StealthMetaAddress(spendSk.toPublicKey(), viewSk.toPublicKey()); - - StealthAddress memory stealth = - stealthMeta.generateStealthAddress(ephSk); - - bool found = viewSk.checkStealthAddress(spendSk.toPublicKey(), stealth); - assertTrue(found); - } - - function testProperty_Receiver_CanComputeStealthAddressSecretKey( - SecretKey viewSk, - SecretKey spendSk, - SecretKey ephSk - ) public { - vm.assume(viewSk.isValid()); - vm.assume(spendSk.isValid()); - vm.assume(ephSk.isValid()); - - StealthMetaAddress memory stealthMeta = - StealthMetaAddress(spendSk.toPublicKey(), viewSk.toPublicKey()); - - StealthAddress memory stealth = - stealthMeta.generateStealthAddress(ephSk); - - SecretKey sk = spendSk.computeStealthSecretKey(viewSk, stealth); - - assertTrue(sk.isValid()); - assertEq(sk.toPublicKey().toAddress(), stealth.addr); - } -}