diff --git a/host-contracts/README.md b/host-contracts/README.md index ad026add07..4b78904bee 100644 --- a/host-contracts/README.md +++ b/host-contracts/README.md @@ -11,6 +11,7 @@ npm install ``` To run forge tests: + ``` npm run forge:soldeer npm run test:forge diff --git a/host-contracts/lib/FHE.sol b/host-contracts/lib/FHE.sol index 222a53e9b7..af1d778fc3 100644 --- a/host-contracts/lib/FHE.sol +++ b/host-contracts/lib/FHE.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import "./Impl.sol"; -import "./cryptography/ECDSA.sol"; +import {FhevmECDSA} from "./cryptography/FhevmECDSA.sol"; import {FheType} from "../contracts/shared/FheType.sol"; import "encrypted-types/EncryptedTypes.sol"; @@ -9646,7 +9646,7 @@ library FHE { address[] memory recoveredSigners = new address[](numSignatures); uint256 uniqueValidCount; for (uint256 i = 0; i < numSignatures; i++) { - address signerRecovered = ECDSA.recover(digest, signatures[i]); + address signerRecovered = FhevmECDSA.recover(digest, signatures[i]); if (!_isSigner(signerRecovered, KMSSigners)) { revert KMSInvalidSigner(signerRecovered); } @@ -9685,7 +9685,7 @@ library FHE { * @return signer The address that supposedly signed the message. */ function _recoverSigner(bytes32 message, bytes memory signature) private pure returns (address) { - address signerRecovered = ECDSA.recover(message, signature); + address signerRecovered = FhevmECDSA.recover(message, signature); return signerRecovered; } diff --git a/host-contracts/lib/cryptography/ECDSA.sol b/host-contracts/lib/cryptography/FhevmECDSA.sol similarity index 98% rename from host-contracts/lib/cryptography/ECDSA.sol rename to host-contracts/lib/cryptography/FhevmECDSA.sol index 1573a2a0d8..679625eadf 100644 --- a/host-contracts/lib/cryptography/ECDSA.sol +++ b/host-contracts/lib/cryptography/FhevmECDSA.sol @@ -8,8 +8,11 @@ pragma solidity ^0.8.20; * * These functions can be used to verify that a message was signed by the holder * of the private keys of a given address. + * + * @dev This library is forked from OpenZeppelin's ECDSA and renamed to FhevmECDSA + * to avoid naming conflicts with the original when both are used in the same project. */ -library ECDSA { +library FhevmECDSA { enum RecoverError { NoError, InvalidSignature, diff --git a/host-contracts/test/kmsVerifier/kmsVerifier.t.sol b/host-contracts/test/kmsVerifier/kmsVerifier.t.sol index 406f97ede8..c04ab7fb3b 100644 --- a/host-contracts/test/kmsVerifier/kmsVerifier.t.sol +++ b/host-contracts/test/kmsVerifier/kmsVerifier.t.sol @@ -565,7 +565,9 @@ contract KMSVerifierTest is Test { /** * @dev Tests that the verifyDecryptionEIP712KMSSignatures function fails if the length of the decryption proof is invalid. */ - function test_VerifyDecryptionEIP712KMSSignaturesFailsIfDeserializingDecryptionProofFail(uint256 randomValue) public { + function test_VerifyDecryptionEIP712KMSSignaturesFailsIfDeserializingDecryptionProofFail( + uint256 randomValue + ) public { _upgradeProxyWithSigners(3); bytes32[] memory handlesList = _generateMockHandlesList(3); diff --git a/library-solidity/README.md b/library-solidity/README.md index fd395787cb..19e3cfcff7 100644 --- a/library-solidity/README.md +++ b/library-solidity/README.md @@ -18,11 +18,13 @@ _See full details in the [Key concepts](https://docs.zama.ai/fhevm/smart-contrac To start writing confidential smart contracts using FHEVM Solidity, follow the Hardhat setup guide here: [Getting Started with Hardhat](https://docs.zama.ai/fhevm/getting-started/overview-1/hardhat). run + ``` npm install ``` To run forge tests: + ``` npm run forge:soldeer npm run test:forge diff --git a/library-solidity/codegen/src/main.ts b/library-solidity/codegen/src/main.ts index ebd4e9cc5e..5371d12775 100644 --- a/library-solidity/codegen/src/main.ts +++ b/library-solidity/codegen/src/main.ts @@ -23,7 +23,7 @@ import { generateSolidityHCULimit } from './hcuLimitGenerator'; import { ALL_OPERATORS } from './operators'; import { ALL_OPERATORS_PRICES } from './operatorsPrices'; import { fromDirToFile, fromFileToFile, isDirectory } from './paths'; -import { generateSolidityFHELib } from './templateFHEDotSol'; +import { generateFhevmECDSALib, generateSolidityFHELib } from './templateFHEDotSol'; import { generateSolidityFheType } from './templateFheTypeDotSol'; import { generateSolidityImplLib } from './templateImpDotSol'; import { @@ -147,6 +147,7 @@ export async function commandGenerateAllFiles(options: any) { const fheTypesDotSol = `${path.join(absConfig.lib.fheTypeDir, 'FheType.sol')}`; const implDotSol = `${path.join(absConfig.lib.outDir, 'Impl.sol')}`; + const ecdsaDotSol = `${path.join(absConfig.lib.outDir, 'cryptography', 'FhevmECDSA.sol')}`; const fheDotSol = `${path.join(absConfig.lib.outDir, 'FHE.sol')}`; const hcuLimitDotSol = `${path.join(absConfig.hostContracts.outDir, 'HCULimit.sol')}`; @@ -162,6 +163,7 @@ export async function commandGenerateAllFiles(options: any) { const implRelFheTypesDotSol = fromFileToFile(implDotSol, fheTypesDotSol); const fheRelFheTypesDotSol = fromFileToFile(fheDotSol, fheTypesDotSol); const fheRelImplDotSol = fromFileToFile(fheDotSol, implDotSol); + const fheRelEcdsaDotSol = fromFileToFile(fheDotSol, ecdsaDotSol); debugLog(`============ Config ============`); debugLog(`basePath: ${absConfig.baseDir}`); @@ -170,6 +172,7 @@ export async function commandGenerateAllFiles(options: any) { debugLog(`noTest: ${absConfig.noTest}`); debugLog(`============= Lib =============`); debugLog(`libDir: ${absConfig.lib.outDir}`); + debugLog(`FhevmECDSA.sol: ${ecdsaDotSol}`); debugLog(`Impl.sol: ${implDotSol}`); debugLog(`FHE.sol: ${fheDotSol}`); debugLog(`fheTypeDir: ${absConfig.lib.fheTypeDir}`); @@ -197,15 +200,24 @@ export async function commandGenerateAllFiles(options: any) { if (config.noLib !== true) { const fheTypesCode = generateSolidityFheType(ALL_FHE_TYPE_INFOS); const implCode = generateSolidityImplLib(ALL_OPERATORS, implRelFheTypesDotSol); - const fheCode = generateSolidityFHELib(ALL_OPERATORS, ALL_FHE_TYPE_INFOS, fheRelFheTypesDotSol, fheRelImplDotSol); + const fheCode = generateSolidityFHELib({ + operators: ALL_OPERATORS, + fheTypes: ALL_FHE_TYPE_INFOS, + fheTypeDotSol: fheRelFheTypesDotSol, + implDotSol: fheRelImplDotSol, + ecdsaDotSol: fheRelEcdsaDotSol, + }); + const ecdsaCode = generateFhevmECDSALib(); mkDir(path.dirname(fheTypesDotSol)); mkDir(path.dirname(implDotSol)); + mkDir(path.dirname(ecdsaDotSol)); mkDir(path.dirname(fheDotSol)); // Generate core Solidity contract files. await formatAndWriteFile(`${fheTypesDotSol}`, fheTypesCode); await formatAndWriteFile(`${implDotSol}`, implCode); + await formatAndWriteFile(`${ecdsaDotSol}`, ecdsaCode); await formatAndWriteFile(`${fheDotSol}`, fheCode); } else { debugLog(`Skipping lib generation.`); diff --git a/library-solidity/codegen/src/templateFHEDotSol.ts b/library-solidity/codegen/src/templateFHEDotSol.ts index 77e454c281..4462bdad72 100644 --- a/library-solidity/codegen/src/templateFHEDotSol.ts +++ b/library-solidity/codegen/src/templateFHEDotSol.ts @@ -6,15 +6,30 @@ import { OperatorArguments, ReturnType } from './common'; import { resolveTemplatePath } from './paths'; import { getUint, removeTemplateComments } from './utils'; -export function generateSolidityFHELib( - operators: Operator[], - fheTypes: FheTypeInfo[], - fheTypeDotSol: string, - implDotSol: string, -): string { +export function generateFhevmECDSALib() { + const file = resolveTemplatePath('FhevmECDSA.sol-template'); + const template = readFileSync(file, 'utf8'); + let code = removeTemplateComments(template); + return code; +} + +export function generateSolidityFHELib({ + operators, + fheTypes, + fheTypeDotSol, + implDotSol, + ecdsaDotSol, +}: { + operators: Operator[]; + fheTypes: FheTypeInfo[]; + fheTypeDotSol: string; + implDotSol: string; + ecdsaDotSol: string; +}): string { // Placeholders: // ============= // $${ImplDotSol}$$ + // $${EcdsaDotSol}$$ // $${FheTypeDotSol}$$ // $${FHEOperators}$$ // $${ACLFunctions}$$ @@ -25,6 +40,7 @@ export function generateSolidityFHELib( let code = removeTemplateComments(template); code = code.replace('$${ImplDotSol}$$', implDotSol); + code = code.replace('$${EcdsaDotSol}$$', ecdsaDotSol); code = code.replace('$${FheTypeDotSol}$$', fheTypeDotSol); // Exclude types that do not support any operators. @@ -191,9 +207,13 @@ function handleSolidityTFHEEncryptedOperatorForTwoEncryptedTypes( res.push(` /** - * @dev Evaluates ${operator.name}(e${lhsFheType.type.toLowerCase()} a, e${rhsFheType.type.toLowerCase()} b) and returns the result. + * @dev Evaluates ${ + operator.name + }(e${lhsFheType.type.toLowerCase()} a, e${rhsFheType.type.toLowerCase()} b) and returns the result. */ - function ${operator.name}(e${lhsFheType.type.toLowerCase()} a, e${rhsFheType.type.toLowerCase()} b) internal returns (${returnType}) { + function ${ + operator.name + }(e${lhsFheType.type.toLowerCase()} a, e${rhsFheType.type.toLowerCase()} b) internal returns (${returnType}) { if (!isInitialized(a)) { a = asE${lhsFheType.type.toLowerCase()}(0); } @@ -272,7 +292,9 @@ function generateSolidityTFHEScalarOperator(fheType: AdjustedFheType, operator: let implExpressionA; if (fheType.type == 'Bool') { - implExpressionA = `Impl.${operator.name}(e${fheType.type.toLowerCase()}.unwrap(a), bytes32(uint256(b?1:0))${scalarFlag})`; + implExpressionA = `Impl.${ + operator.name + }(e${fheType.type.toLowerCase()}.unwrap(a), bytes32(uint256(b?1:0))${scalarFlag})`; } else if (fheType.type.startsWith('Int')) { throw new Error('Int types are not supported!'); } else { @@ -318,9 +340,13 @@ function generateSolidityTFHEScalarOperator(fheType: AdjustedFheType, operator: res.push(` /** - * @dev Evaluates ${operator.name}(e${fheType.type.toLowerCase()} a, ${clearMatchingType.toLowerCase()} b) and returns the result. + * @dev Evaluates ${ + operator.name + }(e${fheType.type.toLowerCase()} a, ${clearMatchingType.toLowerCase()} b) and returns the result. */ - function ${operator.name}(e${fheType.type.toLowerCase()} a, ${clearMatchingType.toLowerCase()} b) internal returns (${returnType}) { + function ${ + operator.name + }(e${fheType.type.toLowerCase()} a, ${clearMatchingType.toLowerCase()} b) internal returns (${returnType}) { if (!isInitialized(a)) { a = asE${fheType.type.toLowerCase()}(${ fheType.type == 'Bool' ? 'false' : fheType.type == 'Address' ? `${clearMatchingType.toLowerCase()}(0)` : 0 @@ -335,9 +361,13 @@ function generateSolidityTFHEScalarOperator(fheType: AdjustedFheType, operator: res.push(` /** - * @dev Evaluates ${operator.name}(${clearMatchingType.toLowerCase()} a, e${fheType.type.toLowerCase()} b) and returns the result. + * @dev Evaluates ${ + operator.name + }(${clearMatchingType.toLowerCase()} a, e${fheType.type.toLowerCase()} b) and returns the result. */ - function ${operator.name}(${clearMatchingType.toLowerCase()} a, e${fheType.type.toLowerCase()} b) internal returns (${returnType}) { + function ${ + operator.name + }(${clearMatchingType.toLowerCase()} a, e${fheType.type.toLowerCase()} b) internal returns (${returnType}) { ${maybeEncryptLeft} if (!isInitialized(b)) { b = asE${fheType.type.toLowerCase()}(${ @@ -379,7 +409,9 @@ function handleSolidityTFHEShiftOperator(fheType: AdjustedFheType, operator: Ope const leftExpr = 'a'; const rightExpr = castRightToLeft ? `asE${fheType.type.toLowerCase()}(b)` : 'b'; - let implExpression: string = `Impl.${operator.name}(e${fheType.type.toLowerCase()}.unwrap(${leftExpr}), e${fheType.type.toLowerCase()}.unwrap(${rightExpr})${scalarFlag})`; + let implExpression: string = `Impl.${ + operator.name + }(e${fheType.type.toLowerCase()}.unwrap(${leftExpr}), e${fheType.type.toLowerCase()}.unwrap(${rightExpr})${scalarFlag})`; res.push(` /** @@ -398,13 +430,17 @@ function handleSolidityTFHEShiftOperator(fheType: AdjustedFheType, operator: Ope // Code and test for shift(euint{inputBits},uint8} scalarFlag = ', true'; - implExpression = `Impl.${operator.name}(e${fheType.type.toLowerCase()}.unwrap(a), bytes32(uint256(b))${scalarFlag})`; + implExpression = `Impl.${ + operator.name + }(e${fheType.type.toLowerCase()}.unwrap(a), bytes32(uint256(b))${scalarFlag})`; res.push(` /** * @dev Evaluates ${operator.name}(e${fheType.type.toLowerCase()} a, ${getUint(rhsBits)}) and returns the result. */ - function ${operator.name}(e${fheType.type.toLowerCase()} a, ${getUint(rhsBits)} b) internal returns (e${fheType.type.toLowerCase()}) { + function ${operator.name}(e${fheType.type.toLowerCase()} a, ${getUint( + rhsBits, + )} b) internal returns (e${fheType.type.toLowerCase()}) { if (!isInitialized(a)) { a = asE${fheType.type.toLowerCase()}(0); } @@ -466,7 +502,9 @@ function handleSolidityTFHECustomCastBetweenTwoEuint( */ function asE${outputFheType.type.toLowerCase()}(e${inputFheType.type.toLowerCase()} value) internal returns (e${outputFheType.type.toLowerCase()}) { ${checkInitialized('value', inputFheType.type)} - return e${outputFheType.type.toLowerCase()}.wrap(Impl.cast(e${inputFheType.type.toLowerCase()}.unwrap(value), FheType.${outputFheType.type})); + return e${outputFheType.type.toLowerCase()}.wrap(Impl.cast(e${inputFheType.type.toLowerCase()}.unwrap(value), FheType.${ + outputFheType.type + })); } `; } @@ -543,7 +581,9 @@ function handleSolidityTFHEConvertPlaintextAndEinputToRespectiveType(fheType: Ad */ function fromExternal(externalE${fheType.type.toLowerCase()} inputHandle, bytes memory inputProof) internal returns (e${fheType.type.toLowerCase()}) { if (inputProof.length!=0) { - return e${fheType.type.toLowerCase()}.wrap(Impl.verify(externalE${fheType.type.toLowerCase()}.unwrap(inputHandle), inputProof, FheType.${fheType.isAlias ? fheType.aliasType : fheType.type})); + return e${fheType.type.toLowerCase()}.wrap(Impl.verify(externalE${fheType.type.toLowerCase()}.unwrap(inputHandle), inputProof, FheType.${ + fheType.isAlias ? fheType.aliasType : fheType.type + })); } else { bytes32 inputBytes32 = externalE${fheType.type.toLowerCase()}.unwrap(inputHandle); if(inputBytes32 == 0){ @@ -583,8 +623,12 @@ function handleSolidityTFHEConvertPlaintextAndEinputToRespectiveType(fheType: Ad /** * @dev Convert a plaintext value to an encrypted e${fheType.type.toLowerCase()} value. */ - function asE${fheType.type.toLowerCase()}(${fheType.clearMatchingType} value) internal returns (e${fheType.type.toLowerCase()}) { - return e${fheType.type.toLowerCase()}.wrap(Impl.trivialEncrypt(uint256(${value}), FheType.${fheType.isAlias ? fheType.aliasType : fheType.type})); + function asE${fheType.type.toLowerCase()}(${ + fheType.clearMatchingType + } value) internal returns (e${fheType.type.toLowerCase()}) { + return e${fheType.type.toLowerCase()}.wrap(Impl.trivialEncrypt(uint256(${value}), FheType.${ + fheType.isAlias ? fheType.aliasType : fheType.type + })); } `; @@ -690,7 +734,9 @@ function handleSolidityTFHERand(fheType: AdjustedFheType): string { * @dev Generates a random encrypted value. */ function randE${fheType.type.toLowerCase()}() internal returns (e${fheType.type.toLowerCase()}) { - return e${fheType.type.toLowerCase()}.wrap(Impl.rand(FheType.${fheType.isAlias ? fheType.aliasType : fheType.type})); + return e${fheType.type.toLowerCase()}.wrap(Impl.rand(FheType.${ + fheType.isAlias ? fheType.aliasType : fheType.type + })); } `; @@ -702,8 +748,12 @@ function handleSolidityTFHERand(fheType: AdjustedFheType): string { * @dev Generates a random encrypted ${fheType.bitLength}-bit unsigned integer in the [0, upperBound) range. * The upperBound must be a power of 2. */ - function randE${fheType.type.toLowerCase()}(uint${fheType.bitLength} upperBound) internal returns (e${fheType.type.toLowerCase()}) { - return e${fheType.type.toLowerCase()}.wrap(Impl.randBounded(upperBound, FheType.${fheType.isAlias ? fheType.aliasType : fheType.type})); + function randE${fheType.type.toLowerCase()}(uint${ + fheType.bitLength + } upperBound) internal returns (e${fheType.type.toLowerCase()}) { + return e${fheType.type.toLowerCase()}.wrap(Impl.randBounded(upperBound, FheType.${ + fheType.isAlias ? fheType.aliasType : fheType.type + })); } `; diff --git a/library-solidity/codegen/src/templates/FHE.sol-template b/library-solidity/codegen/src/templates/FHE.sol-template index e1ba5eab32..3759419ab5 100644 --- a/library-solidity/codegen/src/templates/FHE.sol-template +++ b/library-solidity/codegen/src/templates/FHE.sol-template @@ -7,7 +7,7 @@ pragma solidity ^0.8.24; //$$ - //$$ ----------------------------------------------------------------------- import "$${ImplDotSol}$$"; -import "./cryptography/ECDSA.sol"; +import {FhevmECDSA} from "$${EcdsaDotSol}$$"; import {FheType} from "$${FheTypeDotSol}$$"; //$$ ----------------------------------------------------------------------- @@ -439,7 +439,7 @@ library FHE { address[] memory recoveredSigners = new address[](numSignatures); uint256 uniqueValidCount; for (uint256 i = 0; i < numSignatures; i++) { - address signerRecovered = ECDSA.recover(digest, signatures[i]); + address signerRecovered = FhevmECDSA.recover(digest, signatures[i]); if (!_isSigner(signerRecovered, KMSSigners)) { revert KMSInvalidSigner(signerRecovered); } @@ -478,7 +478,7 @@ library FHE { * @return signer The address that supposedly signed the message. */ function _recoverSigner(bytes32 message, bytes memory signature) private pure returns (address) { - address signerRecovered = ECDSA.recover(message, signature); + address signerRecovered = FhevmECDSA.recover(message, signature); return signerRecovered; } diff --git a/library-solidity/lib/cryptography/ECDSA.sol b/library-solidity/codegen/src/templates/FhevmECDSA.sol-template similarity index 98% rename from library-solidity/lib/cryptography/ECDSA.sol rename to library-solidity/codegen/src/templates/FhevmECDSA.sol-template index 1573a2a0d8..679625eadf 100644 --- a/library-solidity/lib/cryptography/ECDSA.sol +++ b/library-solidity/codegen/src/templates/FhevmECDSA.sol-template @@ -8,8 +8,11 @@ pragma solidity ^0.8.20; * * These functions can be used to verify that a message was signed by the holder * of the private keys of a given address. + * + * @dev This library is forked from OpenZeppelin's ECDSA and renamed to FhevmECDSA + * to avoid naming conflicts with the original when both are used in the same project. */ -library ECDSA { +library FhevmECDSA { enum RecoverError { NoError, InvalidSignature, diff --git a/library-solidity/lib/FHE.sol b/library-solidity/lib/FHE.sol index 63095bb397..799fd063b2 100644 --- a/library-solidity/lib/FHE.sol +++ b/library-solidity/lib/FHE.sol @@ -2,7 +2,7 @@ pragma solidity ^0.8.24; import "./Impl.sol"; -import "./cryptography/ECDSA.sol"; +import {FhevmECDSA} from "./cryptography/FhevmECDSA.sol"; import {FheType} from "./FheType.sol"; import "encrypted-types/EncryptedTypes.sol"; @@ -9646,7 +9646,7 @@ library FHE { address[] memory recoveredSigners = new address[](numSignatures); uint256 uniqueValidCount; for (uint256 i = 0; i < numSignatures; i++) { - address signerRecovered = ECDSA.recover(digest, signatures[i]); + address signerRecovered = FhevmECDSA.recover(digest, signatures[i]); if (!_isSigner(signerRecovered, KMSSigners)) { revert KMSInvalidSigner(signerRecovered); } @@ -9685,7 +9685,7 @@ library FHE { * @return signer The address that supposedly signed the message. */ function _recoverSigner(bytes32 message, bytes memory signature) private pure returns (address) { - address signerRecovered = ECDSA.recover(message, signature); + address signerRecovered = FhevmECDSA.recover(message, signature); return signerRecovered; } diff --git a/library-solidity/lib/cryptography/FhevmECDSA.sol b/library-solidity/lib/cryptography/FhevmECDSA.sol new file mode 100644 index 0000000000..679625eadf --- /dev/null +++ b/library-solidity/lib/cryptography/FhevmECDSA.sol @@ -0,0 +1,287 @@ +// SPDX-License-Identifier: MIT +// OpenZeppelin Contracts (last updated v5.5.0) (utils/cryptography/ECDSA.sol) + +pragma solidity ^0.8.20; + +/** + * @dev Elliptic Curve Digital Signature Algorithm (ECDSA) operations. + * + * These functions can be used to verify that a message was signed by the holder + * of the private keys of a given address. + * + * @dev This library is forked from OpenZeppelin's ECDSA and renamed to FhevmECDSA + * to avoid naming conflicts with the original when both are used in the same project. + */ +library FhevmECDSA { + enum RecoverError { + NoError, + InvalidSignature, + InvalidSignatureLength, + InvalidSignatureS + } + + /** + * @dev The signature is invalid. + */ + error ECDSAInvalidSignature(); + + /** + * @dev The signature has an invalid length. + */ + error ECDSAInvalidSignatureLength(uint256 length); + + /** + * @dev The signature has an S value that is in the upper half order. + */ + error ECDSAInvalidSignatureS(bytes32 s); + + /** + * @dev Returns the address that signed a hashed message (`hash`) with `signature` or an error. This will not + * return address(0) without also returning an error description. Errors are documented using an enum (error type) + * and a bytes32 providing additional information about the error. + * + * If no error is returned, then the address can be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * NOTE: This function only supports 65-byte signatures. ERC-2098 short signatures are rejected. This restriction + * is DEPRECATED and will be removed in v6.0. Developers SHOULD NOT use signatures as unique identifiers; use hash + * invalidation or nonces for replay protection. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + * + * Documentation for signature generation: + * + * - with https://web3js.readthedocs.io/en/v1.3.4/web3-eth-accounts.html#sign[Web3.js] + * - with https://docs.ethers.io/v5/api/signer/#Signer-signMessage[ethers] + */ + function tryRecover( + bytes32 hash, + bytes memory signature + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, and the only way to get them + // currently is to use assembly. + assembly ("memory-safe") { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Variant of {tryRecover} that takes a signature in calldata + */ + function tryRecoverCalldata( + bytes32 hash, + bytes calldata signature + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + if (signature.length == 65) { + bytes32 r; + bytes32 s; + uint8 v; + // ecrecover takes the signature parameters, calldata slices would work here, but are + // significantly more expensive (length check) than using calldataload in assembly. + assembly ("memory-safe") { + r := calldataload(signature.offset) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) + } + return tryRecover(hash, v, r, s); + } else { + return (address(0), RecoverError.InvalidSignatureLength, bytes32(signature.length)); + } + } + + /** + * @dev Returns the address that signed a hashed message (`hash`) with + * `signature`. This address can then be used for verification purposes. + * + * The `ecrecover` EVM precompile allows for malleable (non-unique) signatures: + * this function rejects them by requiring the `s` value to be in the lower + * half order, and the `v` value to be either 27 or 28. + * + * NOTE: This function only supports 65-byte signatures. ERC-2098 short signatures are rejected. This restriction + * is DEPRECATED and will be removed in v6.0. Developers SHOULD NOT use signatures as unique identifiers; use hash + * invalidation or nonces for replay protection. + * + * IMPORTANT: `hash` _must_ be the result of a hash operation for the + * verification to be secure: it is possible to craft signatures that + * recover to arbitrary addresses for non-hashed data. A safe way to ensure + * this is by receiving a hash of the original message (which may otherwise + * be too long), and then calling {MessageHashUtils-toEthSignedMessageHash} on it. + */ + function recover(bytes32 hash, bytes memory signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Variant of {recover} that takes a signature in calldata + */ + function recoverCalldata(bytes32 hash, bytes calldata signature) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecoverCalldata(hash, signature); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `r` and `vs` short-signature fields separately. + * + * See https://eips.ethereum.org/EIPS/eip-2098[ERC-2098 short signatures] + */ + function tryRecover( + bytes32 hash, + bytes32 r, + bytes32 vs + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + unchecked { + bytes32 s = vs & bytes32(0x7fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff); + // We do not check for an overflow here since the shift operation results in 0 or 1. + uint8 v = uint8((uint256(vs) >> 255) + 27); + return tryRecover(hash, v, r, s); + } + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `r` and `vs` short-signature fields separately. + */ + function recover(bytes32 hash, bytes32 r, bytes32 vs) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, r, vs); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Overload of {ECDSA-tryRecover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function tryRecover( + bytes32 hash, + uint8 v, + bytes32 r, + bytes32 s + ) internal pure returns (address recovered, RecoverError err, bytes32 errArg) { + // EIP-2 still allows signature malleability for ecrecover(). Remove this possibility and make the signature + // unique. Appendix F in the Ethereum Yellow paper (https://ethereum.github.io/yellowpaper/paper.pdf), defines + // the valid range for s in (301): 0 < s < secp256k1n ÷ 2 + 1, and for v in (302): v ∈ {27, 28}. Most + // signatures from current libraries generate a unique signature with an s-value in the lower half order. + // + // If your library generates malleable signatures, such as s-values in the upper range, calculate a new s-value + // with 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 - s1 and flip v from 27 to 28 or + // vice versa. If your library also generates signatures with 0/1 for v instead 27/28, add 27 to v to accept + // these malleable signatures as well. + if (uint256(s) > 0x7FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF5D576E7357A4501DDFE92F46681B20A0) { + return (address(0), RecoverError.InvalidSignatureS, s); + } + + // If the signature is valid (and not malleable), return the signer address + address signer = ecrecover(hash, v, r, s); + if (signer == address(0)) { + return (address(0), RecoverError.InvalidSignature, bytes32(0)); + } + + return (signer, RecoverError.NoError, bytes32(0)); + } + + /** + * @dev Overload of {ECDSA-recover} that receives the `v`, + * `r` and `s` signature fields separately. + */ + function recover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) internal pure returns (address) { + (address recovered, RecoverError error, bytes32 errorArg) = tryRecover(hash, v, r, s); + _throwError(error, errorArg); + return recovered; + } + + /** + * @dev Parse a signature into its `v`, `r` and `s` components. Supports 65-byte and 64-byte (ERC-2098) + * formats. Returns (0,0,0) for invalid signatures. + * + * For 64-byte signatures, `v` is automatically normalized to 27 or 28. + * For 65-byte signatures, `v` is returned as-is and MUST already be 27 or 28 for use with ecrecover. + * + * Consider validating the result before use, or use {tryRecover}/{recover} which perform full validation. + */ + function parse(bytes memory signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + assembly ("memory-safe") { + // Check the signature length + switch mload(signature) + // - case 65: r,s,v signature (standard) + case 65 { + r := mload(add(signature, 0x20)) + s := mload(add(signature, 0x40)) + v := byte(0, mload(add(signature, 0x60))) + } + // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) + case 64 { + let vs := mload(add(signature, 0x40)) + r := mload(add(signature, 0x20)) + s := and(vs, shr(1, not(0))) + v := add(shr(255, vs), 27) + } + default { + r := 0 + s := 0 + v := 0 + } + } + } + + /** + * @dev Variant of {parse} that takes a signature in calldata + */ + function parseCalldata(bytes calldata signature) internal pure returns (uint8 v, bytes32 r, bytes32 s) { + assembly ("memory-safe") { + // Check the signature length + switch signature.length + // - case 65: r,s,v signature (standard) + case 65 { + r := calldataload(signature.offset) + s := calldataload(add(signature.offset, 0x20)) + v := byte(0, calldataload(add(signature.offset, 0x40))) + } + // - case 64: r,vs signature (cf https://eips.ethereum.org/EIPS/eip-2098) + case 64 { + let vs := calldataload(add(signature.offset, 0x20)) + r := calldataload(signature.offset) + s := and(vs, shr(1, not(0))) + v := add(shr(255, vs), 27) + } + default { + r := 0 + s := 0 + v := 0 + } + } + } + + /** + * @dev Optionally reverts with the corresponding custom error according to the `error` argument provided. + */ + function _throwError(RecoverError error, bytes32 errorArg) private pure { + if (error == RecoverError.NoError) { + return; // no error: do nothing + } else if (error == RecoverError.InvalidSignature) { + revert ECDSAInvalidSignature(); + } else if (error == RecoverError.InvalidSignatureLength) { + revert ECDSAInvalidSignatureLength(uint256(errorArg)); + } else if (error == RecoverError.InvalidSignatureS) { + revert ECDSAInvalidSignatureS(errorArg); + } + } +} diff --git a/library-solidity/package.json b/library-solidity/package.json index 365a205a47..823791b445 100644 --- a/library-solidity/package.json +++ b/library-solidity/package.json @@ -1,7 +1,7 @@ { "name": "@fhevm/solidity", "description": "A Solidity library for interacting with fhevm protocol", - "version": "0.11.0", + "version": "0.11.1", "engines": { "node": ">=20.0.0" }, diff --git a/package-lock.json b/package-lock.json index 2d97b6e8ea..1060a20394 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13003,7 +13003,7 @@ }, "library-solidity": { "name": "@fhevm/solidity", - "version": "0.11.0", + "version": "0.11.1", "license": "BSD-3-Clause-Clear", "dependencies": { "encrypted-types": "^0.0.4"