Skip to content

Commit 424332c

Browse files
authored
Merge of #1987
2 parents 3707d6e + 07c9693 commit 424332c

File tree

10 files changed

+1651
-652
lines changed

10 files changed

+1651
-652
lines changed

host-contracts/lib/FHE.sol

Lines changed: 236 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
pragma solidity ^0.8.24;
33

44
import "./Impl.sol";
5+
import "./cryptography/ECDSA.sol";
56
import {FheType} from "../contracts/shared/FheType.sol";
67

78
import "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
*/
2644
library 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

Comments
 (0)