Skip to content

Commit

Permalink
Merge pull request #33 from FhenixProtocol/YonatanE22-patch-1
Browse files Browse the repository at this point in the history
Update Useful-Tips.md
  • Loading branch information
toml01 authored Oct 29, 2024
2 parents 1b9fc87 + 7c1bf5e commit f44d6bf
Showing 1 changed file with 41 additions and 55 deletions.
96 changes: 41 additions & 55 deletions docs/devdocs/Writing Smart Contracts/Useful-Tips.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,88 +6,74 @@ description: Tidbits of wisdom for working with FHE

## Trivial Encryption

When we are using `FHE.asEuintX(plaintext_number)` we are actually using a trivial encryption of our FHE scheme. Unlike normal FHE encryption trivial encryption is a deterministic encryption. The meaning is that if you will do it twice you will still get the same result
Casting a plaintext number to an encrypted one in a contract (i.e. `FHE.asEuintX(plaintext_number)`) is called Trivial Encryption. Unlike [normal FHE encryption](../FhenixJS/Encryption.md), trivial encryption is deterministic. This means that if you perform it more than once, the resulting ciphertext will be the same every time.

Despite being obviously weaker than normal FHE encrypted numbers, Trivial Encryption can often be very useful. For example, when you're tallying votes in a contract; the tally for the option "Yes" may be encrypted, but everyone knows that you need to increment it by `1` for every incoming vote. Meaning, you can do `tally = tally + FHE.asEuint32(1)`.

Using trivially encrypted numbers is more efficient and will result in faster and cheaper execution - so it's beneficial to use them whenever possible **while being careful to not compromise your apps's security**.

**Note:** to prevent improper use, Trivial Encryption is only available in contracts.

## Default Value of a Euint

When having a `euintx` variable uninitialized it will be considered as 0. Every FHE function that will receive an uninitialized `euintx` will assume it is `FHE.asEuintX(0)`.
You can assume now that `FHE.asEuintX(0)`is used quite often - Luckily we realized this and decided to have the values of `FHE.asEuintX(0)` pre-calculated on node initialization so when you use`FHE.asEuintX(0)` we will just return those values.
When the `euintx` variable is not initialized, it is considered to be 0. Every FHE function that receives an uninitialized `euintx` assumes that it is `FHE.asEuintX(0)`.

`FHE.asEuintX(0)` is actually used quite often. Fhenix takes this frequent use into consideration and pre-calculates the values of `FHE.asEuintX(0)` during node initialization. Therefore, when `FHE.asEuintX(0)` is used during operation, the pre-calculated values are returned (which saves computing resources and gas).

## Re-encrypting a Value

To explain this tip we will use an example. Let's assume we want to develop a confidential voting and let's say we have 4 candidates.
Assuming that on each vote we increase (cryptographically with FHE.add) the tally, one can just monitor the key in the DB that represents this specific tally and once the key is changed he will know who we voted for.
An ideal solution for this issue is to change all keys no matter who we voted for, but how?!
Re-encrypting a value is sometimes necessary in smart contracts. For example, consider a confidential voting system with four candidates. Each vote increases the respective tally (using FHE addition, which is a cryptographic operation). If one monitors the (public!) database keys representing these tallies, even though a tally value is encrypted, it's enough to notice a change in the value to deduce which option got voted for. One solution is to change all the values, regardless of the vote cast - so anyone monitoring would not be able to tell which option got voted for. But how do we do that?

In order to understand how we will first need to understand that FHE encryption is a non-deterministic encryption means that encrypting (non-trivial encryption) a number twice will result with 2 different encrypted outputs.
FHE encryption is non-deterministic, meaning that encrypting the same number twice (using non-trivial encryption) results in two different encrypted outputs. Similarly, a computation on an encrypted number, **even if the computations does not change the underlying plaintext value**, changes the ciphertext. Without decrypting the number, one would not be able to tell if it actually changed or not. We leverage this feature and cryptographically add 0 to all tallies that should not be changed using FHE.add. This operation re-encrypts those values (or - changes the ciphertext), resulting in new encrypted outputs in the database, effectively updating all keys without changing the actual tallies.

Now that we know that, we can add 0 (cryptographically with FHE.add) to all of those tallies that shouldn't be changed and they will be changed in the DB!
Example (simplified pseudo code):
```solidity
// This is bad
t = getTallyToIncrement(userInput);
tallies[t] = FHE.add(tallies[t], FHE.asEuint32(1));
// This is good
for (int i = 0; i < len(tallies); i++) {
ebool b = toIncrement(userInput, i);
tallies[t] = FHE.add(tallies[t], b); // if `b` is true, this will translate to `tally + 1`, otherwise `tally + 0`
}
```

## FHE.req()

All the operations are supported both in TXs and in Queries. That being said we strongly advise to think twice before you use those operations inside a TX. `FHE.req` is actually exposing the value of your encrypted data. Assuming we will send the transaction and monitor the gas usage we can probably identify whether the `FHE.req` condition met or not and understand a lot about what the encrypted values represent.
Example:
All `FHE.req` operations are supported in both transactions (TXs) and queries. However, we strongly advise careful consideration before using these operations inside a transaction, because `FHE.req` might expose the value of encrypted data. For example, if we send a transaction and monitor its gas usage, we can likely determine whether a `FHE.req` condition was met and infer much about what the encrypted values represent.

Consider the following code:
```solidity
function f(euint8 a, euint8 b) public {
FHE.req(a.eq(b));
// Do some heavy logic
}
```

In this case, if `a` and `b` won't be equal it will fail immediately and take less gas than the case when `a` and `b` are equal which means that one who checks the gas can easily know the equality of `a` and `b` it won't leak their values, but it will leak confidential data.
The rule of thumb that we are suggesting is to use `FHE.req` only in `view` functions while the logic of `FHE.req` in txs can be implemented using `FHE.select`
If `a` and `b` are not equal, the function will fail immediately and consumes much less gas compared to a situation in which `a` and `b` are equal. This means that monitoring gas usage can easily determine whether a and b are equal, potentially leaking confidential information without revealing the actual values.

## FHE.decrypt()
**Best Practice:** use `FHE.req` only in view functions. For transactions, `FHE.req` logic can be implemented using `FHE.select`. This approach helps preserve confidentiality while achieving the desired functionality.

Generally speaking, the idea of Fhenix and having FHE in place is the ability to have your values encrypted throughout the whole lifetime of the data (since you can operate on encrypted data). When using `FHE.decrypt` you should always consider the following:
a. On mainnet (and future testnet versions) the decryption process will be done on a threshold network and the operation might not be fully deterministic (network issues for example)
b. Assuming malicious node runner have DMA (direct memory access) or any other way to read the process' memory he can see what is the decrypted value while it is being executed and use MEV techniques.

We recommended a rule of thumb to when to decrypt:
a. In view functions
b. In TXs when you are 100% confident that the data is not confidential anymore (For example in poker game when the transaction is a roundup transaction so you can reveal the cards without being afraid of data leakage)
## FHE.decrypt()

## Performance and Gas Usage
The Fhenix implementation of Fully Homomorphic Encryption (FHE) intends to keep data encrypted throughout its entire lifecycle, while providing the capability to operate on the encrypted data. However, eventually decrypting data (`FHE.decrypt`) is crucial in most use cases.

Currently, we support many FHE operations. Some of them might take a lot of time to compute, some good examples are: Div (5 seconds for euint32), Mul, Rem, and the time will grow depends on the value types you are using.
Decrypting is a risky operation. You should always consider that a malicious node runner might have DMA (direct memory access) or any other way to read the process' memory. Always assume that a node runner can see what is the decrypted value while it is being executed (before it's committed to a block) and, for example, use it for MEV.

When writing FHE code we encourage you to use the operations wisely and choose what operation should be used.
Example: Instead of `ENCRYPTED_UINT_32 * FHE.asEuint32(2)` you can use `FHE.shl(ENCRYPTED_UINT_32, FHE.asEuint32(1))` in some cases `FHE.div(ENCRYPTED_UINT_32, FHE.asEuint32(2))` can be replaced by `FHE.shr(ENCRYPTED_UINT_32, FHE.asEuint32(1))`
### Decryption – Best Practice
Follow these guidelines to maintain data security and integrity when using FHE.decrypt:
- **View functions**: preferably, decrypt in view functions only when possible, for example when the data is being accessed for read-only purposes.
- **Transactions**: use decryption in transactions only when you are absolutely certain that the data is no longer confidential. For instance, in a poker game application, during the roundup transaction, cards can be revealed without data leakage risk.

For more detailed benchmarks please refer to: [Gas and Benchmarks](./Gas-and-Benchmarks)

## Randomness
## Performance and Gas Usage

Confidentiality is a crucial step in order to achieve on-chain randomness. Fhenix, as a chain that implements confidentiality, is a great space to implement and use on-chain random numbers and this is part of our roadmap.
We know that there are some #BUIDLers that are planning to implement dapps that leverage both confidentiality and random numbers so until we will have on-chain true random, we are suggesting to use the following implementation as a MOCKUP.
Currently, Fhenix supports a large number of FHE operations. Some operations take much time to compute. Good examples of time-intensive operations are: Div, Mul, and Rem. Time increases depending on the value types being used (euint64 will take longer than euint32).
When writing FHE code, Fhenix encourages using operations wisely, especially when choosing which operation to use.

:::danger
PLEASE NOTE THAT THIS RANDOM NUMBER IS VERY PREDICTABLE AND SHOULD NOT BE USED IN PRODUCTION.
:::
For example, instead of `ENCRYPTED_UINT_32 * FHE.asEuint32(2)`, it is preferable to use `FHE.shl(ENCRYPTED_UINT_32, FHE.asEuint32(1))`.
In other cases, `FHE.div(ENCRYPTED_UINT_32, FHE.asEuint32(2))` can be replaced by `FHE.shr(ENCRYPTED_UINT_32, FHE.asEuint32(1))`.

```solidity
library RandomMock {
function getFakeRandom() internal returns (uint256) {
uint blockNumber = block.number;
uint256 blockHash = uint256(blockhash(blockNumber));
return blockHash;
}
function getFakeRandomU8() public view returns (euint8) {
uint8 blockHash = uint8(getFakeRandom());
return FHE.asEuint8(blockHash);
}
function getFakeRandomU16() public view returns (euint16) {
uint16 blockHash = uint16(getFakeRandom());
return FHE.asEuint16(blockHash);
}
function getFakeRandomU32() public view returns (euint32) {
uint32 blockHash = uint32(getFakeRandom());
return FHE.asEuint32(blockHash);
}
}
```
For more detailed benchmarks, refer to: [Gas and Benchmarks](./Gas-and-Benchmarks).

0 comments on commit f44d6bf

Please sign in to comment.