fhe-http 0.2.47
This is an test package for Python Fhe Http
Requires Python: >=3.8

# FHE-HTTP-Python

## Introduce
- Fully Homomorphic Encryption (FHE) is a technology that enables processing encrypted data without decrypting it. This allows clients to delegate computations, such as machine learning (ML) tasks to resource-equipped servers without revealing their own data. It can also be used for applications like Private Information Retrieval ([PIR](https://en.wikipedia.org/wiki/Private_information_retrieval)). For more details about FHE, please refer to the  [tfhe-rs documentation](https://docs.zama.ai/tfhe-rs).
- This project offers a Python interface for using [tfhe-rs](https://github.com/zama-ai/tfhe-rs). Additionally, a zk-experimental version is supported, enabling the server to verify encrypted values before computation begins, and assembly code execution allows clients to define the computation process/
- Currently, gpu acceleration of tfhe-rs is not yet available in this project.

## Required
- rust^1.77.1 stable 
  - [installation](https://www.rust-lang.org/tools/install)
- python^3.10
- environment: 
  - `ubuntu-22.04` 
  - `macos arm64`
  - `macos x86-64`

## How to use
### About Package
```shellscript=
// download from from PyPI
$ pip install fhe-http

// Build fhe-http package from source
// This will create a package in site-packages of pip
$ cd fhe-http/fhe_http_python
$ maturin develop
```
### Run Tests
```python=
// After package downloaded or built
# cd fhe-http/fhe_http_python
# python tests/*
```
## Details of usage
### #Basic Fhe Computation
#### Description
This basic version of FHE computation enables clients to delegate computation tasks to servers. Clients use a client key which is generated by themselves to encrypt the values. After encryption, clients send the encrypted values and a server key to the servers. Once the server key is received and set by the servers, the servers can execute the FHE computation on the ciphertexts. The computation result remains encrypted and is supposed to be sent back to the clients, who can then decrypt it to obtain the result in plaintext.
#### Supported
- operations: 
  - `add`, `sub`, `mul`, `div`, `rem`, `and`, `or`, `xor`, `shr`, `shl`, `not`, `and`, `neg`
- FheType: 
  - `Int64`
  - `Uint64`
#### Example usage
```python=
import fhe_http as py_fhe

# initialize keys
key_gen = py_fhe.KeyGenerator()
key_gen.init_keys()
client_key = key_gen.get_client_key()
server_key = key_gen.get_server_key()

# Server Side:
# set server key for ciphertext computation
sk_setter = py_fhe.ServerKeySetter()
decompressed_server_key = sk_setter.decompress_server_key(server_key)
sk_setter.set_server_key(decompressed_server_key)

# Client Side:
# use Fhe module to encrypt
fhe = py_fhe.Fhe(client_key)

# Client Side:
# encryt value
serailizer = py_fhe.Serializer()
data_type = py_fhe.create_fhe_value_type("Int64")
encrypted_a = fhe.encrypt(serailizer.from_i64(123), data_type)
encrypted_b = fhe.encrypt(serailizer.from_i64(456), data_type)

# Server Side:
# using FheOps module to have ciphertext computation
fhe_ops = py_fhe.FheOps()
encrypted_c = fhe_ops.add(encrypted_a, encrypted_b, data_type)

# Client side:
# decrypt computation result
decrypted_c = fhe.decrypt(encrypted_c, data_type)
c = serailizer.to_i64(decrypted_c)
assert c == 123 + 456
```

### #ZK-experimental Fhe Computation
#### Description
This zk-experimental feature allows servers to verify encrypted values sent by clients before starting the FHE execution, preventing the processing of invalid or malicious values. Clients must use a public key, generated with zk public parameters, to encrypt the values. Servers verify these encrypted values by leveraging the zk public parameters (the same one used to generate public key). After completing the FHE computation, clients will receive the same result as they would using the non-zk feature.
#### Supported
- operations: 
  - `add`, `sub`, `mul`, `div`, `rem`, `and`, `or`, `xor`, `shr`, `shl`, `not`, `and`, `neg`
- ProvenFheType: 
  - `ProvenInt64`
  - `ProvenUint64` 
#### Example usage
```python=
import fhe_http as py_fhe

# initialize keys
key_gen = py_fhe.KeyGenerator()
key_gen.init_keys()
client_key = key_gen.get_client_key()
server_key = key_gen.get_server_key()
public_key = key_gen.get_public_key()

# initialize zk params
public_zk_params = py_fhe.get_public_zk_params(msg=2, carry=2)

# Server Side:
# set server key for ciphertext computation
sk_setter = py_fhe.ServerKeySetter()
decompressed_server_key = sk_setter.decompress_server_key(server_key)
sk_setter.set_server_key(decompressed_server_key)

# Client Side:
# use Fhe module to encrypt
fhe = py_fhe.Fhe(client_key, public_key)

# Client Side:
# encryt value with public_zk_params
serailizer = py_fhe.Serializer()
proven_fhe_type = py_fhe.create_proven_fhe_value_type("ProvenInt64")
encrypted_a = fhe.proven_encrypt(serailizer.from_i64(123), proven_fhe_type, public_zk_params)
encrypted_b = fhe.proven_encrypt(serailizer.from_i64(456), proven_fhe_type, public_zk_params)

# Server Side:
# using ProvenFheOps module to have ciphertext computation
proven_ops = py_fhe.ProvenFheOps()
encrypted_c = proven_ops.add(encrypted_a, encrypted_b, proven_fhe_type, public_zk_params, public_key)

# Client Side:
decrypted_c = fhe.decrypt(encrypted_c, proven_fhe_type)
c = serailizer.to_u64(decrypted_c)
assert c == 123 + 456
```

### #Assembly Code Fhe execution
#### Description
This assembly feature allows clients to define a function that includes multiple operations. The `assembler.code_wrapper` is a decorator that parses Python code into self-defined assembly code (see the example code below). Clients can define the function's parameters and send the encrypted actual values corresponding to those parameters to a server. Upon receiving the encrypted parameters, the server executes the assembly code and responds with the fhe computation results to the client.
#### Supported
- operations: 
  - `add`, `sub`, `mul`, `div`, `rem`, `and`, `or`, `xor`, `shr`, `shl`, `not`, `and`
  - `neg` is temporarily not supported in assembly code execution.
- Type: 
  - `Int64`
  - `Uint64`
#### Example usage
```python=
import fhe_http as py_fhe
from fhe_http.assembler.assembler import Assembler

def generate_keys():
    key_gen = py_fhe.KeyGenerator()
    key_gen.init_keys()
    client_key = key_gen.get_client_key()
    server_key = key_gen.get_server_key()
    return client_key, server_key

def set_server_key(server_key):
    sk_setter = py_fhe.ServerKeySetter()
    decompressed_server_key = sk_setter.decompress_server_key(server_key)
    sk_setter.set_server_key(decompressed_server_key)

def encrypt(num: int, client_key, data_type: str = "Uint64"):
    serailizer = py_fhe.Serializer()
    fhe_value = py_fhe.create_fhe_value_type(data_type)
    fhe = py_fhe.Fhe(client_key)
    return fhe.encrypt(serailizer.from_i64(num), fhe_value)

def decrypt(encrypted_num, client_key, data_type: str = "Uint64"):
    serailizer = py_fhe.Serializer()
    fhe_value = py_fhe.create_fhe_value_type(data_type)
    fhe = py_fhe.Fhe(client_key)
    return serailizer.to_i64(fhe.decrypt(encrypted_num, fhe_value))

def get_asm_code():
    assembler = Assembler()

    @assembler.code_wrapper
    def operation(i, j, two, three):
        a = i + j
        b = a >> two
        c = two << three
        d = c - b
        return d

    return "\n".join(operation.assembly)

if __name__ == "__main__":
    assembly = get_asm_code()
    client_key, server_key = generate_keys()
    set_server_key(server_key)

    encrypted = py_fhe.execute_assembly(
        assembly,
        {
            "i": encrypt(25, client_key),
            "j": encrypt(21, client_key),
            "two": encrypt(2, client_key),
            "three": encrypt(3, client_key),
        },
        py_fhe.create_fhe_value_type("Uint64"),
    )
    decrypted = decrypt(encrypted, client_key)
    print(decrypted)
```

## Interaction via Http
### Decription
This python package also provide some function for http request. For example: setting the header to comfirm the fhe protocol, and some json related operation for both body and header (see below exmaple)

### Example usage
#### Execution
```shellscript=
$ pip install fastapi uvicorn requests fhe-http

// machine 1
$ python3 server_test.py

// machine 2
$ python3 client_test.py
```
#### Client Side
```python=
# file name: client_test.py
import json
import fastapi
import uvicorn
from fastapi import Body
from pydantic import BaseModel
import fhe_http as py_fhe


app = fastapi.FastAPI()


class Addition(BaseModel):
    a: str
    b: str
    server_key: str


def encrypt(num: int, client_key, data_type: str = "Int64"):
    serailizer = py_fhe.Serializer()
    fhe_value = py_fhe.create_fhe_value_type(data_type)
    fhe = py_fhe.Fhe(client_key)
    return fhe.encrypt(serailizer.from_i64(num), fhe_value)


def set_server_key(server_key):
    sk_setter = py_fhe.ServerKeySetter()
    decompressed_server_key = sk_setter.decompress_server_key(server_key)
    sk_setter.set_server_key(decompressed_server_key)


@app.post("/")
async def post_request(data: Addition = Body(...)):
    print("Received request")
    data_type = py_fhe.create_fhe_value_type("Int64")
    data_json = json.loads(data.model_dump_json())
    encrypted_a = py_fhe.decode_fhe_value(data_json["a"])
    encrypted_b = py_fhe.decode_fhe_value(data_json["b"])
    server_key = py_fhe.decode_fhe_value(data_json["server_key"])
    set_server_key(server_key)
    fhe_ops = py_fhe.FheOps()
    encrypted_c = fhe_ops.add(encrypted_a, encrypted_b, data_type)
    encoded_c = py_fhe.encode_fhe_value(encrypted_c)
    return {"result": encoded_c, "status": "success"}


if __name__ == "__main__":
    uvicorn.run("server_test:app", host="localhost", port=8000, reload=True)
```

#### Server Side
```python=
# file name: server_test.py
# client.py
import json
import requests
import fhe_http as py_fhe


def generate_keys():
    key_gen = py_fhe.KeyGenerator()
    key_gen.init_keys()
    client_key = key_gen.get_client_key()
    server_key = key_gen.get_server_key()
    return client_key, server_key


def decrypt(encrypted_num, client_key, data_type: str = "Int64"):
    serailizer = py_fhe.Serializer()
    fhe_value = py_fhe.create_fhe_value_type(data_type)
    fhe = py_fhe.Fhe(client_key)
    return serailizer.to_i64(fhe.decrypt(encrypted_num, fhe_value))


def send_post_request(url):
    header = json.loads(py_fhe.create_fhe_header("123"))
    client_key, server_key = generate_keys()
    data = {"a": 123123123, "b": 123}
    data_type = py_fhe.create_fhe_value_type("Int64")
    encrypt_json = py_fhe.encrypt_fhe_body(
        [("a", data_type), ("b", data_type)], data, client_key
    )
    encrypt_json = json.loads(encrypt_json)
    payload_str = py_fhe.set_server_key_to_json(server_key, encrypt_json)
    payload = json.loads(payload_str)
    response = requests.post(url, json=payload, headers=header)
    response = response.json()
    encrypted_c = py_fhe.decode_fhe_value(response["result"])
    c = decrypt(encrypted_c, client_key)
    assert c == 123123246


if __name__ == "__main__":
    server_url = "http://localhost:8000"
    send_post_request(server_url)
```