Skip to content
This repository has been archived by the owner on Dec 23, 2024. It is now read-only.

feat: a precompile to get ciphertext bytes #90

Merged
merged 1 commit into from
Feb 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
## Getting Started
- [Initial Setup](getting_started/README.md)
- [Integration](getting_started/Integration.md)
- [FheLib Library](getting_started/FheLib.md)

## 🔗 Support

Expand Down
63 changes: 63 additions & 0 deletions docs/getting_started/FheLib.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
# FheLib

`FheLib` is a library implemented inside fhevm-go. It offers FHE-related functionalities such as homomorphic operations, decryption/reencryption requests and so on. FheLib is exposed as a single `precompiled` contract (or a `precompile` for short) that is integrated into the underlying blockchain.

FheLib functions can be called by calling the FheLib precompile with a respective EVM function selector.

This page describes the required inputs, behaviours and outputs of some of these functions.

## GetCiphertext Function (selector: e4b808cb)

The `GetCiphertext` function returns a serialized TFHE ciphertext from protected storage given:
* contract address where the ciphertext is stored at
* the ebool/e(u)int value (also called a handle) for which the ciphertext is requested

GetCiphertext only works via the `eth_call` RPC.

To call GetCiphertext via `eth_call`, the following Python can serve as an example:

```python
import http.client
import json

# This is the address of the FheLib precompile. This value is hardcoded per blockchain.
fhe_lib_precompile_address = "0x000000000000000000000000000000000000005d"

# The contract address where the ciphertext is stored at.
contract_address = "ACD7Be4EBF68Bf2A5b6eB0CaFb15460C169BC459"
# 12 bytes of 0s for padding the contract address.
address_zero_padding = "000000000000000000000000"

# The ebool/e(u)int value for which the ciphertext is requested.
handle = "f038cdc8bf630e239f143abeb039b91ec82ec17a8460582e7a409fa551030c06"

# The function selector of GetCiphertext.
get_ciphertext_selector = "e4b808cb"

# Call the FheLib precompile with `data` being the concatenation of:
# - getCiphertext function selector;
# - 12 bytes of 0s to padd the contract address;
# - contract address;
# - the handle to the ciphertext.
payload = {
"jsonrpc": "2.0",
"method": "eth_call",
"params": [
{
"to": fhe_lib_precompile_address,
"data": "0x" + get_ciphertext_selector + address_zero_padding +
contract_address + handle
},
"latest"
],
"id": 1,
}

con = http.client.HTTPConnection("localhost", 8545)
con.request("POST", "/", body=json.dumps(payload),
headers={"Content-Type": "application/json"})
resp = json.loads(con.getresponse().read())

# Remove leading "0x" and decode hex to get a byte buffer with the ciphertext.
ciphertext = bytes.fromhex(resp["result"][2:])
```
210 changes: 210 additions & 0 deletions fhevm/contracts_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4101,3 +4101,213 @@ func TestDecryptInTransactionDisabled(t *testing.T) {
t.Fatalf("unexpected error for disabling decryption transactions, got %s", err.Error())
}
}

func TestFheLibGetCiphertextInvalidInputSize(t *testing.T) {
environment := newTestEVMEnvironment()
addr := common.Address{}
environment.ethCall = true
readOnly := true
input := make([]byte, 0)
zeroPadding := make([]byte, 12)
signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4]
input = append(input, signature...)
input = append(input, zeroPadding...)
// missing input data...
_, err := FheLibRun(environment, addr, addr, input, readOnly)
if err == nil {
t.Fatalf("getCiphertext expected failure on bad input size")
}
}

func TestFheLibGetCiphertextNonEthCall(t *testing.T) {
environment := newTestEVMEnvironment()
pc := uint64(0)
depth := 1
environment.depth = depth
plaintext := uint64(2)
ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32)
ctHash := ct.GetHash()
scope := newTestScopeConext()
loc := uint256.NewInt(10)
value := uint256FromBig(ctHash.Big())

// Setup and call SSTORE - it requires a location and a value to set there.
scope.pushToStack(value)
scope.pushToStack(loc)
_, err := OpSstore(&pc, environment, scope)
if err != nil {
t.Fatalf(err.Error())
}

// Call getCiphertext.
addr := common.Address{}
environment.ethCall = false
readOnly := true
input := make([]byte, 0)
zeroPadding := make([]byte, 12)
signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4]
input = append(input, signature...)
input = append(input, zeroPadding...)
input = append(input, testContractAddress{}.Address().Bytes()...)
input = append(input, ctHash.Bytes()...)
_, err = FheLibRun(environment, addr, addr, input, readOnly)
if err == nil {
t.Fatalf("getCiphertext expected failure non-EthCall")
}
}

func TestFheLibGetCiphertextNonExistentHandle(t *testing.T) {
environment := newTestEVMEnvironment()
pc := uint64(0)
depth := 1
environment.depth = depth
plaintext := uint64(2)
ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32)
ctHash := ct.GetHash()
scope := newTestScopeConext()
loc := uint256.NewInt(10)
value := uint256FromBig(ctHash.Big())

// Setup and call SSTORE - it requires a location and a value to set there.
scope.pushToStack(value)
scope.pushToStack(loc)
_, err := OpSstore(&pc, environment, scope)
if err != nil {
t.Fatalf(err.Error())
}

// Change ctHash to something that doesn't exist
ctHash[0]++

// Call getCiphertext.
addr := common.Address{}
environment.ethCall = true
readOnly := true
input := make([]byte, 0)
zeroPadding := make([]byte, 12)
signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4]
input = append(input, signature...)
input = append(input, zeroPadding...)
input = append(input, testContractAddress{}.Address().Bytes()...)
input = append(input, ctHash.Bytes()...)
out, err := FheLibRun(environment, addr, addr, input, readOnly)
if err != nil {
t.Fatalf(err.Error())
}
if len(out) != 0 {
t.Fatalf("getCiphertext expected empty output on non-existent handle")
}
}

func TestFheLibGetCiphertextWrongContractAddress(t *testing.T) {
environment := newTestEVMEnvironment()
pc := uint64(0)
depth := 1
environment.depth = depth
plaintext := uint64(2)
ct := verifyCiphertextInTestMemory(environment, plaintext, depth, FheUint32)
ctHash := ct.GetHash()
scope := newTestScopeConext()
loc := uint256.NewInt(10)
value := uint256FromBig(ctHash.Big())

// Setup and call SSTORE - it requires a location and a value to set there.
scope.pushToStack(value)
scope.pushToStack(loc)
_, err := OpSstore(&pc, environment, scope)
if err != nil {
t.Fatalf(err.Error())
}

// Call getCiphertext.
addr := common.Address{}
environment.ethCall = true
readOnly := true
contractAddress := testContractAddress{}.Address()
// Change address to another one that doesn't contain the handle.
contractAddress[0]++
input := make([]byte, 0)
zeroPadding := make([]byte, 12)
signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4]
input = append(input, signature...)
input = append(input, zeroPadding...)
input = append(input, contractAddress.Bytes()...)
input = append(input, ctHash.Bytes()...)
out, err := FheLibRun(environment, addr, addr, input, readOnly)
if err != nil {
t.Fatalf(err.Error())
}
if len(out) != 0 {
t.Fatalf("getCiphertext expected empty output on wrong contract address")
}
}

func FheLibGetCiphertext(t *testing.T, fheUintType FheUintType) {
environment := newTestEVMEnvironment()
pc := uint64(0)
depth := 1
environment.depth = depth
plaintext := uint64(2)
ct := verifyCiphertextInTestMemory(environment, plaintext, depth, fheUintType)
ctHash := ct.GetHash()
scope := newTestScopeConext()
loc := uint256.NewInt(10)
value := uint256FromBig(ctHash.Big())

// Setup and call SSTORE - it requires a location and a value to set there.
scope.pushToStack(value)
scope.pushToStack(loc)
_, err := OpSstore(&pc, environment, scope)
if err != nil {
t.Fatalf(err.Error())
}

// Call getCiphertext.
addr := common.Address{}
environment.ethCall = true
readOnly := true
input := make([]byte, 0)
zeroPadding := make([]byte, 12)
signature := crypto.Keccak256([]byte("getCiphertext(address,uint256)"))[0:4]
input = append(input, signature...)
input = append(input, zeroPadding...)
input = append(input, testContractAddress{}.Address().Bytes()...)
input = append(input, ctHash.Bytes()...)
out, err := FheLibRun(environment, addr, addr, input, readOnly)
if err != nil {
t.Fatalf(err.Error())
}
size, _ := GetExpandedFheCiphertextSize(fheUintType)
if size != uint(len(out)) {
t.Fatalf("getCiphertext returned ciphertext size of %d, expected %d", len(out), size)
}

outCt := new(TfheCiphertext)
err = outCt.Deserialize(out, fheUintType)
if err != nil {
t.Fatalf(err.Error())
}
decrypted, err := outCt.Decrypt()
if err != nil {
t.Fatalf(err.Error())
}
if decrypted.Uint64() != plaintext {
t.Fatalf("getCiphertext returned ciphertext value of %d, expected %d", decrypted.Uint64(), plaintext)
}
}

func TestFheLibGetCiphertext8(t *testing.T) {
FheLibGetCiphertext(t, FheUint8)
}

func TestFheLibGetCiphertext16(t *testing.T) {
FheLibGetCiphertext(t, FheUint16)
}

func TestFheLibGetCiphertext32(t *testing.T) {
FheLibGetCiphertext(t, FheUint32)
}

func TestFheLibGetCiphertext64(t *testing.T) {
FheLibGetCiphertext(t, FheUint64)
}
Loading
Loading