Skip to content

Commit

Permalink
feat: a precompile to get ciphertext bytes
Browse files Browse the repository at this point in the history
Given a contract address and an ebool/euint value stored in it, return
the underlying ciphertext.

Returns an empty response if no such ciphertext exist. Only works via
`eth_call`.

The function selector for it, as of this commit, is `e4b808cb`.

Move protected storage code into its own file.

Add a `Precompiles` section in the getting started doc section.

Nit: rename `arg_types` to `argTypes` in instructions.go for naming
consistency.
  • Loading branch information
dartdart26 committed Feb 21, 2024
1 parent 965afb6 commit 1eb0838
Show file tree
Hide file tree
Showing 9 changed files with 595 additions and 226 deletions.
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)
- [Precompiles](getting_started/Precompiles.md)

## 🔗 Support

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

fhevm-go supports a number of functionalities that can be accessed via EVM function selectors. We call these functionalities `precompiles`, even though, technically, there is only one main fhevm-go precompile.

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

## GetCiphertext

The `GetCiphertext` precompile 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 `eth_call`.

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 main fhevm-go precompile. This value is hardcoded per blockchain.
fhe_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 FHE 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_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

0 comments on commit 1eb0838

Please sign in to comment.