22pragma solidity ^ 0.8.24 ;
33
44import "./Impl.sol " ;
5+ import "./cryptography/ECDSA.sol " ;
56import {FheType} from "../contracts/shared/FheType.sol " ;
67
78import "encrypted-types/EncryptedTypes.sol " ;
@@ -16,6 +17,23 @@ interface IKMSVerifier {
1617 bytes memory decryptedResult ,
1718 bytes memory decryptionProof
1819 ) external returns (bool );
20+
21+ function eip712Domain ()
22+ external
23+ view
24+ returns (
25+ bytes1 fields ,
26+ string memory name ,
27+ string memory version ,
28+ uint256 chainId ,
29+ address verifyingContract ,
30+ bytes32 salt ,
31+ uint256 [] memory extensions
32+ );
33+
34+ function getThreshold () external view returns (uint256 );
35+
36+ function getKmsSigners () external view returns (address [] memory );
1937}
2038
2139/**
@@ -24,6 +42,31 @@ interface IKMSVerifier {
2442 * that interact with the FHEVM protocol.
2543 */
2644library FHE {
45+ /// @notice Decryption result typehash.
46+ bytes32 private constant DECRYPTION_RESULT_TYPEHASH =
47+ keccak256 ("PublicDecryptVerification(bytes32[] ctHandles,bytes decryptedResult,bytes extraData) " );
48+
49+ /// @notice EIP-712 domain typehash.
50+ bytes32 private constant EIP712_DOMAIN_TYPEHASH =
51+ keccak256 ("EIP712Domain(string name,string version,uint256 chainId,address verifyingContract) " );
52+
53+ /// @notice Returned if the deserializing of the decryption proof fails.
54+ error DeserializingDecryptionProofFail ();
55+
56+ /// @notice Returned if the decryption proof is empty.
57+ error EmptyDecryptionProof ();
58+
59+ /// @notice Returned if the recovered KMS signer is not a valid KMS signer.
60+ /// @param invalidSigner Address of the invalid signer.
61+ error KMSInvalidSigner (address invalidSigner );
62+
63+ /// @notice Returned if the number of signatures is inferior to the threshold.
64+ /// @param numSignatures Number of signatures.
65+ error KMSSignatureThresholdNotReached (uint256 numSignatures );
66+
67+ /// @notice Returned if the number of signatures is equal to 0.
68+ error KMSZeroSignature ();
69+
2770 /// @notice Returned if the returned KMS signatures are not valid.
2871 error InvalidKMSSignatures ();
2972
@@ -8454,8 +8497,7 @@ library FHE {
84548497 } else {
84558498 bytes32 inputBytes32 = externalEbool.unwrap (inputHandle);
84568499 if (inputBytes32 == 0 ) {
8457- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Bool);
8458- return ebool.wrap (inputBytes32);
8500+ return asEbool (false );
84598501 }
84608502 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
84618503 return ebool.wrap (inputBytes32);
@@ -8481,8 +8523,7 @@ library FHE {
84818523 } else {
84828524 bytes32 inputBytes32 = externalEuint8.unwrap (inputHandle);
84838525 if (inputBytes32 == 0 ) {
8484- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint8);
8485- return euint8.wrap (inputBytes32);
8526+ return asEuint8 (0 );
84868527 }
84878528 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
84888529 return euint8.wrap (inputBytes32);
@@ -8508,8 +8549,7 @@ library FHE {
85088549 } else {
85098550 bytes32 inputBytes32 = externalEuint16.unwrap (inputHandle);
85108551 if (inputBytes32 == 0 ) {
8511- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint16);
8512- return euint16.wrap (inputBytes32);
8552+ return asEuint16 (0 );
85138553 }
85148554 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
85158555 return euint16.wrap (inputBytes32);
@@ -8535,8 +8575,7 @@ library FHE {
85358575 } else {
85368576 bytes32 inputBytes32 = externalEuint32.unwrap (inputHandle);
85378577 if (inputBytes32 == 0 ) {
8538- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint32);
8539- return euint32.wrap (inputBytes32);
8578+ return asEuint32 (0 );
85408579 }
85418580 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
85428581 return euint32.wrap (inputBytes32);
@@ -8562,8 +8601,7 @@ library FHE {
85628601 } else {
85638602 bytes32 inputBytes32 = externalEuint64.unwrap (inputHandle);
85648603 if (inputBytes32 == 0 ) {
8565- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint64);
8566- return euint64.wrap (inputBytes32);
8604+ return asEuint64 (0 );
85678605 }
85688606 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
85698607 return euint64.wrap (inputBytes32);
@@ -8589,8 +8627,7 @@ library FHE {
85898627 } else {
85908628 bytes32 inputBytes32 = externalEuint128.unwrap (inputHandle);
85918629 if (inputBytes32 == 0 ) {
8592- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint128);
8593- return euint128.wrap (inputBytes32);
8630+ return asEuint128 (0 );
85948631 }
85958632 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
85968633 return euint128.wrap (inputBytes32);
@@ -8616,8 +8653,7 @@ library FHE {
86168653 } else {
86178654 bytes32 inputBytes32 = externalEaddress.unwrap (inputHandle);
86188655 if (inputBytes32 == 0 ) {
8619- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint160);
8620- return eaddress.wrap (inputBytes32);
8656+ return asEaddress (address (0 ));
86218657 }
86228658 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
86238659 return eaddress.wrap (inputBytes32);
@@ -8643,8 +8679,7 @@ library FHE {
86438679 } else {
86448680 bytes32 inputBytes32 = externalEuint256.unwrap (inputHandle);
86458681 if (inputBytes32 == 0 ) {
8646- inputBytes32 = Impl.trivialEncrypt (0 , FheType.Uint256);
8647- return euint256.wrap (inputBytes32);
8682+ return asEuint256 (0 );
86488683 }
86498684 if (! Impl.isAllowed (inputBytes32, msg .sender )) revert SenderNotAllowedToUseHandle (inputBytes32, msg .sender );
86508685 return euint256.wrap (inputBytes32);
@@ -9469,6 +9504,191 @@ library FHE {
94699504 emit PublicDecryptionVerified (handlesList, abiEncodedCleartexts);
94709505 }
94719506
9507+ /// @notice Returns false or reverts if the KMS signatures verification against the provided handles and public decryption data
9508+ /// fails. Returns true only if KMS signatures verification pass. This is the `view` variant of `checkSignatures`.
9509+ /// @dev **WARNING**: Prefer using `checkSignatures` (non-view) over this function whenever possible, for several reasons:
9510+ /// 1. **Safety** – `checkSignatures` automatically reverts when signatures are invalid, making misuse impossible.
9511+ /// In contrast, `isPublicDecryptionResultValid` returns a boolean: if the caller forgets to `require` the returned
9512+ /// value, invalid signatures will silently pass, leaving the contract vulnerable to forged decryption results.
9513+ /// 2. **Front-end integration** – `checkSignatures` emits a `PublicDecryptionVerified` event upon successful
9514+ /// verification, which is critical for front-end applications that need to detect when a public decrypt result
9515+ /// has been verified on-chain. This view function does not emit any event.
9516+ /// 3. **Gas efficiency** – `checkSignatures` leverages a transient-storage mapping to cache verification results,
9517+ /// making decryption result verification cheaper.
9518+ /// Use this view variant only when you explicitly need a read-only call (e.g. off-chain simulation or static call).
9519+ /// @param handlesList The list of handles as an array of bytes32 to check
9520+ /// @param abiEncodedCleartexts The ABI-encoded list of decrypted values associated with each handle in the `handlesList`.
9521+ /// The ABI-encoded list order must match the `handlesList` order.
9522+ /// @param decryptionProof The KMS public decryption proof. It includes the KMS signatures, associated metadata,
9523+ /// and the context needed for verification.
9524+ /// @dev Reverts if any of the following conditions are met:
9525+ /// - The `decryptionProof` is empty or has an invalid length.
9526+ /// - The number of valid signatures is zero or less than the configured KMS signers threshold.
9527+ /// - Any signature is produced by an address that is not a registered KMS signer.
9528+ /// @dev Returns false if there are enough signatures to reach threshold, but some recovered signer is duplicated.
9529+ /// @return true if the signatures verification succeeds, false or reverts otherwise.
9530+ function isPublicDecryptionResultValid (
9531+ bytes32 [] memory handlesList ,
9532+ bytes memory abiEncodedCleartexts ,
9533+ bytes memory decryptionProof
9534+ ) internal view returns (bool ) {
9535+ if (decryptionProof.length == 0 ) {
9536+ revert EmptyDecryptionProof ();
9537+ }
9538+
9539+ /// @dev The decryptionProof is the numSigners + kmsSignatures + extraData (1 + 65*numSigners + extraData bytes)
9540+ uint256 numSigners = uint256 (uint8 (decryptionProof[0 ]));
9541+
9542+ /// @dev The extraData is the rest of the decryptionProof bytes after the numSigners + signatures.
9543+ uint256 extraDataOffset = 1 + 65 * numSigners;
9544+
9545+ /// @dev Check that the decryptionProof is long enough to contain at least the numSigners + kmsSignatures.
9546+ if (decryptionProof.length < extraDataOffset) {
9547+ revert DeserializingDecryptionProofFail ();
9548+ }
9549+
9550+ bytes [] memory signatures = new bytes [](numSigners);
9551+ for (uint256 j = 0 ; j < numSigners; j++ ) {
9552+ signatures[j] = new bytes (65 );
9553+ for (uint256 i = 0 ; i < 65 ; i++ ) {
9554+ signatures[j][i] = decryptionProof[1 + 65 * j + i];
9555+ }
9556+ }
9557+
9558+ /// @dev Extract the extraData from the decryptionProof.
9559+ uint256 extraDataSize = decryptionProof.length - extraDataOffset;
9560+ bytes memory extraData = new bytes (extraDataSize);
9561+ for (uint i = 0 ; i < extraDataSize; i++ ) {
9562+ extraData[i] = decryptionProof[extraDataOffset + i];
9563+ }
9564+ bytes32 digest = _hashDecryptionResult (handlesList, abiEncodedCleartexts, extraData);
9565+
9566+ return _verifySignaturesDigest (digest, signatures);
9567+ }
9568+
9569+ /*
9570+ * @notice Hashes the decryption result.
9571+ * @param ctHandles The list of handles as an array of bytes32 to check.
9572+ * @param decryptedResult ABI-encoded list of decrypted values
9573+ * @param extraData Extra data.
9574+ * @return hashTypedData Hash typed data.
9575+ */
9576+ function _hashDecryptionResult (
9577+ bytes32 [] memory ctHandles ,
9578+ bytes memory decryptedResult ,
9579+ bytes memory extraData
9580+ ) private view returns (bytes32 ) {
9581+ CoprocessorConfig storage $ = Impl.getCoprocessorConfig ();
9582+ (
9583+ ,
9584+ string memory name ,
9585+ string memory version ,
9586+ uint256 gatewayCahinId ,
9587+ address verifyingContract ,
9588+ ,
9589+
9590+ ) = IKMSVerifier ($.KMSVerifierAddress).eip712Domain ();
9591+
9592+ bytes32 domainHash = keccak256 (
9593+ abi.encode (
9594+ EIP712_DOMAIN_TYPEHASH,
9595+ keccak256 (bytes (name)),
9596+ keccak256 (bytes (version)),
9597+ gatewayCahinId,
9598+ verifyingContract
9599+ )
9600+ );
9601+
9602+ bytes32 structHash = keccak256 (
9603+ abi.encode (
9604+ DECRYPTION_RESULT_TYPEHASH,
9605+ keccak256 (abi.encodePacked (ctHandles)),
9606+ keccak256 (decryptedResult),
9607+ keccak256 (abi.encodePacked (extraData))
9608+ )
9609+ );
9610+
9611+ bytes32 typedDataHash;
9612+ assembly ("memory-safe" ) {
9613+ let ptr := mload (0x40 )
9614+ mstore (ptr, hex "19_01 " )
9615+ mstore (add (ptr, 0x02 ), domainHash)
9616+ mstore (add (ptr, 0x22 ), structHash)
9617+ typedDataHash := keccak256 (ptr, 0x42 )
9618+ }
9619+
9620+ return typedDataHash;
9621+ }
9622+
9623+ /**
9624+ * @notice View function that verifies multiple signatures for a given message at a certain threshold.
9625+ * @param digest The hash of the message that was signed by all signers.
9626+ * @param signatures An array of signatures to verify.
9627+ * @return isVerified true if enough provided signatures are valid, false otherwise.
9628+ */
9629+ function _verifySignaturesDigest (bytes32 digest , bytes [] memory signatures ) private view returns (bool ) {
9630+ uint256 numSignatures = signatures.length ;
9631+
9632+ if (numSignatures == 0 ) {
9633+ revert KMSZeroSignature ();
9634+ }
9635+
9636+ CoprocessorConfig storage $ = Impl.getCoprocessorConfig ();
9637+
9638+ uint256 threshold = IKMSVerifier ($.KMSVerifierAddress).getThreshold ();
9639+
9640+ if (numSignatures < threshold) {
9641+ revert KMSSignatureThresholdNotReached (numSignatures);
9642+ }
9643+
9644+ address [] memory KMSSigners = IKMSVerifier ($.KMSVerifierAddress).getKmsSigners ();
9645+
9646+ address [] memory recoveredSigners = new address [](numSignatures);
9647+ uint256 uniqueValidCount;
9648+ for (uint256 i = 0 ; i < numSignatures; i++ ) {
9649+ address signerRecovered = ECDSA.recover (digest, signatures[i]);
9650+ if (! _isSigner (signerRecovered, KMSSigners)) {
9651+ revert KMSInvalidSigner (signerRecovered);
9652+ }
9653+ if (! _isSigner (signerRecovered, recoveredSigners)) {
9654+ recoveredSigners[uniqueValidCount] = signerRecovered;
9655+ uniqueValidCount++ ;
9656+ }
9657+ if (uniqueValidCount >= threshold) {
9658+ return true ;
9659+ }
9660+ }
9661+ return false ;
9662+ }
9663+
9664+ /**
9665+ * @notice Checks whether a given address is present in an array of signers.
9666+ * @param signer The address to look for.
9667+ * @param signersArray The array of signer addresses to search.
9668+ * @return isSigner true if the address is found, false otherwise.
9669+ */
9670+ function _isSigner (address signer , address [] memory signersArray ) private pure returns (bool ) {
9671+ uint256 signersArrayLength = signersArray.length ;
9672+ for (uint256 i = 0 ; i < signersArrayLength; i++ ) {
9673+ if (signer == signersArray[i]) {
9674+ return true ;
9675+ }
9676+ }
9677+ return false ;
9678+ }
9679+
9680+ /**
9681+ * @notice Recovers the signer's address from a `signature` and a `message` digest.
9682+ * @dev It utilizes ECDSA for actual address recovery. It does not support contract signature (EIP-1271).
9683+ * @param message The hash of the message that was signed.
9684+ * @param signature The signature to verify.
9685+ * @return signer The address that supposedly signed the message.
9686+ */
9687+ function _recoverSigner (bytes32 message , bytes memory signature ) private pure returns (address ) {
9688+ address signerRecovered = ECDSA.recover (message, signature);
9689+ return signerRecovered;
9690+ }
9691+
94729692 /// @notice Verifies KMS signatures against the provided handles and public decryption data.
94739693 /// @param handlesList The list of handles as an array of bytes32 to verify
94749694 /// @param abiEncodedCleartexts The ABI-encoded list of decrypted values associated with each handle in the `handlesList`.
0 commit comments