Skip to content

Commit

Permalink
Merge pull request #458 from zama-ai/patch/mockedDocs
Browse files Browse the repository at this point in the history
Patch/mocked docs
  • Loading branch information
jatZama authored Aug 7, 2024
2 parents ff84285 + 55c5b28 commit ef87a35
Show file tree
Hide file tree
Showing 4 changed files with 304 additions and 9 deletions.
11 changes: 5 additions & 6 deletions docs/fundamentals/write_contract/hardhat.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ It's essential to run tests of the final contract version using the real fhEVM.

## Mocked mode

For faster testing iterations, instead of launching all the tests on the local fhEVM node via `pnpm test`or `npx hardhat test` which could last several minutes, you could use instead a mocked version of the `TFHE.sol` library.
The same tests should (almost always) pass, as is, without any modification: neither the javascript files neither the solidity files need to be changed between the mocked and the real version. The mocked mode does not actually use encryption for encrypted types and runs the tests on a local hardhat node which is implementing the original EVM (i.e non-fhEVM).
For faster testing iterations, instead of launching all the tests on the local fhEVM node via `pnpm test`or `npx hardhat test` which could last several minutes, you could use instead a mocked version of the fhEVM.
The same tests should (almost always) pass, as is, without any modification: neither the javascript files neither the solidity files need to be changed between the mocked and the real version. The mocked mode does not actually real encryption for encrypted types and runs the tests on a local hardhat node which is implementing the original EVM (i.e non-fhEVM). Additionally, the mocked mode will let you use all the hardhat related special testing/debugging methods, such as `evm_mine`, `evm_snapshot`, `evm_revert` etc, which are very helpful for testing.

To run the mocked tests use either:

Expand All @@ -22,7 +22,7 @@ pnpm test:mock
Or equivalently:

```
HARDHAT_NETWORK=hardhat npx hardhat test --network hardhat
npx hardhat test --network hardhat
```

In mocked mode, all tests should pass in few seconds instead of few minutes, allowing a better developer experience.
Expand All @@ -36,10 +36,9 @@ pnpm coverage:mock
Or equivalently:

```
HARDHAT_NETWORK=hardhat npx hardhat coverage-mock --network hardhat
npx hardhat coverage --network hardhat
```

Then open the file `coverage/index.html`. This will allow increased security by pointing out missing branches not covered yet by the current test suite.

⚠️ **Warning :** Notice that, due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the most important change is the `TFHE.isInitialized` method which will always return `true` in the mocked version. Another big difference in mocked mode, compared to the real fhEVM implementation, is that there is no ciphertext verification neither checking that a ciphertext has been honestly obtained in the mocked version (see section `4` of the [whitepaper](../../../fhevm-whitepaper.pdf)).
This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with `pnpm test` or `npx hardhat test`.
⚠️ **Warning :** Due to intrinsic limitations of the original EVM, the mocked version differ in few corner cases from the real fhEVM, the main difference is the difference in gas prices for the FHE operations. This means that before deploying to production, developers still need to run the tests with the original fhEVM node, as a final check in non-mocked mode, with `pnpm test` or `npx hardhat test`.
296 changes: 296 additions & 0 deletions lib/MockedPrecompile.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,296 @@
// SPDX-License-Identifier: BSD-3-Clause-Clear

pragma solidity ^0.8.24;

contract MockedPrecompile {
uint256 public counterRand = 0; // counter used for computing handles of randomness operators
uint256 constant HANDLE_VERSION = 0;

enum Operators {
fheAdd,
fheSub,
fheMul,
fheDiv,
fheRem,
fheBitAnd,
fheBitOr,
fheBitXor,
fheShl,
fheShr,
fheRotl,
fheRotr,
fheEq,
fheNe,
fheGe,
fheGt,
fheLe,
fheLt,
fheMin,
fheMax,
fheNeg,
fheNot,
verifyCiphertext,
cast,
trivialEncrypt,
fheIfThenElse,
fheRand,
fheRandBounded
}

function isPowerOfTwo(uint256 x) internal pure returns (bool) {
return (x > 0) && ((x & (x - 1)) == 0);
}

/// @dev handle format for user inputs is: keccak256(keccak256(CiphertextFHEList)||index_handle)[0:29] || index_handle || handle_type || handle_version
/// @dev other handles format (fhe ops results) is: keccak256(keccak256(rawCiphertextFHEList)||index_handle)[0:30] || handle_type || handle_version
/// @dev the CiphertextFHEList actually contains: 1 byte (= N) for size of handles_list, N bytes for the handles_types : 1 per handle, then the original fhe160list raw ciphertext
function typeOf(uint256 handle) internal pure returns (uint8) {
uint8 typeCt = uint8(handle >> 8);
return typeCt;
}

function appendType(uint256 prehandle, uint8 handleType) internal pure returns (uint256 result) {
result = prehandle & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000;
result = result | (uint256(handleType) << 8); // append type
result = result | HANDLE_VERSION;
}

function requireType(uint256 handle, uint256 supportedTypes) internal pure {
uint8 typeCt = typeOf(handle);
require((1 << typeCt) & supportedTypes > 0, "Unsupported type");
}

function unaryOp(Operators op, uint256 ct) internal pure returns (uint256 result) {
result = uint256(keccak256(abi.encodePacked(op, ct)));
uint8 typeCt = typeOf(ct);
result = appendType(result, typeCt);
}

function binaryOp(
Operators op,
uint256 lhs,
uint256 rhs,
bytes1 scalarByte,
uint8 resultType
) internal pure returns (uint256 result) {
bytes1 scalar = scalarByte & 0x01;
if (scalar == 0x00) {
uint8 typeRhs = typeOf(rhs);
uint8 typeLhs = typeOf(lhs);
require(typeLhs == typeRhs, "Incompatible types for lhs and rhs");
}
result = uint256(keccak256(abi.encodePacked(op, lhs, rhs, scalar)));
result = appendType(result, resultType);
}

function ternaryOp(Operators op, uint256 lhs, uint256 middle, uint256 rhs) internal pure returns (uint256 result) {
uint8 typeLhs = typeOf(lhs);
uint8 typeMiddle = typeOf(middle);
uint8 typeRhs = typeOf(rhs);
require(typeLhs == 0, "Unsupported type for lhs"); // lhs must be ebool
require(typeMiddle == typeRhs, "Incompatible types for middle and rhs");
result = uint256(keccak256(abi.encodePacked(op, lhs, middle, rhs)));
result = appendType(result, typeMiddle);
}

function fheAdd(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheAdd, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheSub(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheSub, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheMul(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheMul, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheDiv(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
require(scalarByte == 0x01, "Only fheDiv by a scalar is supported");
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheDiv, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheRem(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheRem, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheBitAnd(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
require(scalarByte == 0x00, "Only fheBitAnd by a ciphertext is supported");
uint256 supportedTypes = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheBitAnd, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheBitOr(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
require(scalarByte == 0x00, "Only fheBitOr by a ciphertext is supported");
uint256 supportedTypes = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheBitOr, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheBitXor(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
require(scalarByte == 0x00, "Only fheBitXor by a ciphertext is supported");
uint256 supportedTypes = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheBitXor, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheShl(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheShl, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheShr(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheShr, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheRotl(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheRotl, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheRotr(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheRotr, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheEq(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 7) + (1 << 11);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheEq, lhs, rhs, scalarByte, 0);
}

function fheNe(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 7) + (1 << 11);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheNe, lhs, rhs, scalarByte, 0);
}

function fheGe(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheGe, lhs, rhs, scalarByte, 0);
}

function fheGt(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheGt, lhs, rhs, scalarByte, 0);
}

function fheLe(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheLe, lhs, rhs, scalarByte, 0);
}

function fheLt(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheLt, lhs, rhs, scalarByte, 0);
}

function fheMin(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheMin, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheMax(uint256 lhs, uint256 rhs, bytes1 scalarByte) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(lhs, supportedTypes);
result = binaryOp(Operators.fheMax, lhs, rhs, scalarByte, typeOf(lhs));
}

function fheNeg(uint256 ct) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(ct, supportedTypes);
result = unaryOp(Operators.fheNeg, ct);
}

function fheNot(uint256 ct) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(ct, supportedTypes);
result = unaryOp(Operators.fheNot, ct);
}

function verifyCiphertext(
bytes32 inputHandle,
address /*callerAddress*/,
address /*contractAddress*/,
bytes memory inputProof,
bytes1 handleType
) external pure returns (uint256 result) {
result = uint256(inputHandle);
uint256 indexHandle = (result & 0x0000000000000000000000000000000000000000000000000000000000ff0000) >> 16;
uint8 typeCt = typeOf(result);

require(uint8(handleType) == typeCt, "Wrong type");

uint256 inputProofLen = inputProof.length;
require(inputProofLen > 0, "Empty inputProof");

require(uint8(result) == HANDLE_VERSION, "Wrong handle version");

bytes32 checkHandle = keccak256(abi.encodePacked(keccak256(inputProof), uint8(indexHandle)));
bytes32 mask = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffff000000;
// check correct inputHandle was sent, corresponding to the inputProof: the 29 first bytes must be equal
require((inputHandle & mask) == (checkHandle & mask), "Wrong inputHandle");
}

function cast(uint256 ct, bytes1 toType) external pure returns (uint256 result) {
uint256 supportedTypesInput = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
requireType(ct, supportedTypesInput);
uint256 supportedTypesOutput = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5); // @note: unsupported casting to ebool (use fheNe instead)
require((1 << uint8(toType)) & supportedTypesOutput > 0, "Unsupported output type");
uint8 typeCt = typeOf(ct);
require(bytes1(typeCt) != toType, "Cannot cast to same type");
result = uint256(keccak256(abi.encodePacked(Operators.cast, ct, toType)));
result = appendType(result, uint8(toType));
}

function trivialEncrypt(uint256 pt, bytes1 toType) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 0) + (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 7);
require((1 << uint8(toType)) & supportedTypes > 0, "Unsupported type");
result = uint256(keccak256(abi.encodePacked(Operators.trivialEncrypt, pt, toType)));
result = appendType(result, uint8(toType));
}

function fheIfThenElse(uint256 control, uint256 ifTrue, uint256 ifFalse) external pure returns (uint256 result) {
uint256 supportedTypes = (1 << 1) + (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5) + (1 << 7);
requireType(ifTrue, supportedTypes);
result = ternaryOp(Operators.fheIfThenElse, control, ifTrue, ifFalse);
}

function fheRand(bytes1 randType, uint256 /*ok*/) external returns (uint256 result) {
uint256 supportedTypes = (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
require((1 << uint8(randType)) & supportedTypes > 0, "Unsupported erandom type");
result = uint256(keccak256(abi.encodePacked(Operators.fheRand, randType, counterRand)));
result = appendType(result, uint8(randType));
counterRand++;
}

function fheRandBounded(uint256 upperBound, bytes1 randType, uint256 /*ok*/) external returns (uint256 result) {
uint256 supportedTypes = (1 << 2) + (1 << 3) + (1 << 4) + (1 << 5);
require((1 << uint8(randType)) & supportedTypes > 0, "Unsupported erandom type");
require(isPowerOfTwo(upperBound), "UpperBound must be a power of 2");
result = uint256(keccak256(abi.encodePacked(Operators.fheRandBounded, upperBound, randType, counterRand)));
result = appendType(result, uint8(randType));
counterRand++;
}
}
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "fhevm",
"description": "A Solidity library for interacting with the Zama Blockchain",
"version": "0.5.4",
"version": "0.5.5",
"engines": {
"node": ">=20.0.0"
},
Expand Down

0 comments on commit ef87a35

Please sign in to comment.