// SPDX-License-Identifier: AGPLv3 pragma solidity ^0.8.0; // see https://github.com/noot/schnorr-verify for implementation details library Schnorr { // secp256k1 group order uint256 constant public Q = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141; // Fixed parity for the public keys used in this contract // This avoids spending a word passing the parity in a similar style to // Bitcoin's Taproot uint8 constant public KEY_PARITY = 27; error InvalidSOrA(); error MalformedSignature(); // px := public key x-coord, where the public key has a parity of KEY_PARITY // message := 32-byte hash of the message // c := schnorr signature challenge // s := schnorr signature function verify( bytes32 px, bytes memory message, bytes32 c, bytes32 s ) internal pure returns (bool) { // ecrecover = (m, v, r, s) -> key // We instead pass the following to obtain the nonce (not the key) // Then we hash it and verify it matches the challenge bytes32 sa = bytes32(Q - mulmod(uint256(s), uint256(px), Q)); bytes32 ca = bytes32(Q - mulmod(uint256(c), uint256(px), Q)); // For safety, we want each input to ecrecover to be 0 (sa, px, ca) // The ecreover precomple checks `r` and `s` (`px` and `ca`) are non-zero // That leaves us to check `sa` are non-zero if (sa == 0) revert InvalidSOrA(); address R = ecrecover(sa, KEY_PARITY, px, ca); if (R == address(0)) revert MalformedSignature(); // Check the signature is correct by rebuilding the challenge return c == keccak256(abi.encodePacked(R, px, message)); } }