Client

Function Description

  • The Client provides an interface for users to send requests and process Node data received from the Hub.

  • The Client communicates with the Node via the gRPC protocol, sending input data and receiving proofs.

Main Process

Establishing HTTPS Connection

Request Node List: Send a request to the Hub to obtain the Node list.

Receive Node Data: Receive the Node list from the Hub.

Request Node List (with Payment): First, establish a new order request with the Hub and obtain the order number. Afterward, call the corresponding contract function for payment. Once the payment is completed, use the order number to request the Node list from the Hub.

Receive Node Data: Receive the Node list from the Hub.

Code Example

  • Users first establish an HTTPS connection with the Hub, which returns a JSON object containing response codes and node addresses array.

  • A response code of 0 indicates a successful request.

Example Request:

curl -X GET "https://prove-api.zerobase.pro/api/v1/hub/node"

Example return:

{
  "code": 0,
  "node_list": [
    "127.0.0.1:8000",
    "127.0.0.1:8001",
    "127.0.0.1:8002",
    "127.0.0.1:8003"
  ]
}

Define gRPC Protocol

Define the gRPC service and message formats using .proto files.

syntax = "proto3";

package prove_service;

message ProveBaseRequest {
  string prover_id = 1;            // Unique identifier for the prover
  string circuit_template_id = 2;  // Circuit template identifier
  string input_data = 3;           // Input data used to generate the proof
  bool is_encrypted = 4;           // True if the input data is encrypted
  string auth_token = 5;           // Authentication token
}

message StatusResponse {
  int32 code = 1; // Status code
  string msg = 2; // Additional status information
}

message ProveRequest {
  ProveBaseRequest base_request = 1; // Base request for all proof requests
}

message ProveToCairoRequest {
  ProveBaseRequest base_request = 1; // Base request for all proof requests
}

message ProveNosha256Request {
  ProveBaseRequest base_request = 1; // Base request for all proof requests
  int32 length = 2; // Length of the input data
}

message ProveNosha256OffchainRequest {
  ProveBaseRequest base_request = 1; // Base request for all proof requests
  int32 length = 2; // Length of the input data
}

message GetPublicKeyResponse {
  StatusResponse base_response = 1; // Status code and message
  string public_key = 2; // Retrieved public key string
}

message ProveResponse {
  StatusResponse base_response = 1; // Status code and message
  string proof_data = 2; // Generated proof data
}

message ProveNosha256Response {
  StatusResponse base_response = 1; // Status code and message
  string proof_data = 2; // Generated proof data
}

message ProveToCairoResponse {
  StatusResponse base_response = 1; // Status code and message
  string proof_data = 2; // Generated proof data
  string witness_data = 3; // Additional witness data related to the proof

message ProveNosha256OffchainResponse {
  StatusResponse base_response = 1; // Status code and message
  bytes proof_data = 2; // Generated proof data
  string witness_data = 3; // Additional witness data related to the proof
}

message Empty {
  // An empty message used for requests or responses that don't require additional parameters
}

service ProveService {
  rpc Prove(ProveRequest) returns (ProveResponse); // Generates a proof using the provided parameters
  rpc ProveWithWitness(ProveToCairoRequest) returns (ProveToCairoResponse); // Generates a proof with witness
  rpc ProveNosha256(ProveNosha256Request) returns (ProveNosha256Response);  // Generates a proof using Nosha256 input data
  rpc ProveNosha256WithWitness(ProveNosha256Request) returns (ProveNosha256Response);  // Generates a proof using Nosha256 input data with witness
  rpc ProveNosha256Offchain(ProveNosha256OffchainRequest) returns (ProveNosha256OffchainResponse); // Generates a proof using Nosha256 input data for offchain verify
  rpc GetPublicKey(Empty) returns (GetPublicKeyResponse); // Retrieves the public key for input data encrypted.
  rpc Ping(Empty) returns (Empty); // Ping for health check or to keep the connection alive
}

Install gRPC Tools

Install grpcio-tools via PIP to generate gRPC code.

pip install grpcio-tools

Generate gRPC Code

Use protoc to generate Python service and message handling code.

Once the installation is complete, navigate to the directory where the .proto file is stored. Open a command-line tool and ensure that the current working directory is the one where the .proto file is located. Execute the following command to generate Python code: python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. Prove.proto

python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. Prove.proto
  • -I. specifies the directory where protoc searches for imported .proto files, which in this case is the current directory.

  • --python_out=. specifies the output directory for the generated Python class files.

  • --grpc_python_out=. specifies the output directory for the generated gRPC service code.

  • prove.proto is the name of your .proto file.

After executing this command, two files will be generated in the same directory:

  • prove_pb2.py:Contains Python classes for the messages and enums defined in your .proto file.

  • prove_pb2_grpc.py:Contains server and client classes that can be used to implement and call gRPC methods.

Implement Python gRPC Client Code

  • Implement gRPC communication with the Node.

  • Send requests and handle the returned data.

Code Example:

import grpc
import time
from modules.prove_service import prove_service_pb2, prove_service_pb2_grpc
from utils.constant import GRPC_STATUS_ERROR, GRPC_STATUS_SUCCESSFULLY

class ProveServiceClient:
    def __init__(self, address: str) -> None:
        self.address = address

    async def _make_request(self, method_name: str, request_message):
        """
        Helper method to handle gRPC requests.
        """
        try:
            async with grpc.aio.insecure_channel(self.address) as channel:
                stub = prove_service_pb2_grpc.ProveServiceStub(channel)
                method = getattr(stub, method_name)
                response = await method(request_message)
                return response
        except grpc.aio.AioRpcError as e:
            return GRPC_STATUS_ERROR, str(e), None

    async def prove(self, prover: str, temp: str, input_data: str, is_encrypted: bool, token: str):
        request = prove_service_pb2.ProveRequest(
            prover=prover,
            temp=temp,
            input=input_data,
            is_encrypted=is_encrypted,
            token=token
        )
        response = await self._make_request('prove', request)
        return response.code, response.msg, response.proof

    async def prove_nosha256(self, prover: str, temp: str, input_data: str, is_encrypted: bool, token: str, length: int):
        request = prove_service_pb2.ProveNosha256Request(
            prover=prover,
            temp=temp,
            input=input_data,
            is_encrypted=is_encrypted,
            token=token,
            length=length
        )
        response = await self._make_request('prove_nosha256', request)
        return response.code, response.msg, response.proof

    async def prove_to_cairo(self, prover: str, temp: str, input_data: str, is_encrypted: bool, token: str):
        request = prove_service_pb2.ProveToCairoRequest(
            prover=prover,
            temp=temp,
            input=input_data,
            is_encrypted=is_encrypted,
            token=token
        )
        response = await self._make_request('prove2cairo', request)
        return response.code, response.msg, response.proof, response.witness

    async def prove_nosha256_offchain(self, prover: str, temp: str, input_data: str, is_encrypted: bool, token: str, length: int):
        request = prove_service_pb2.ProveNosha256OffchainRequest(
            prover=prover,
            temp=temp,
            input=input_data,
            is_encrypted=is_encrypted,
            token=token,
            length=length
        )
        response = await self._make_request('prove_nosha256_offchain', request)
        return response.code, response.msg, response.proof, response.witness

    async def get_public_key(self):
        response = await self._make_request('get_public_key', prove_service_pb2.Empty())
        return response.code, response.public_key

    async def ping(self):
        try:
            start_time = time.time()
            async with grpc.aio.insecure_channel(self.address) as channel:
                stub = prove_service_pb2_grpc.ProveServiceStub(channel)
                await stub.ping(prove_service_pb2.Empty())
            end_time = time.time()
            spent_time = end_time - start_time
            return GRPC_STATUS_SUCCESSFULLY, spent_time
        except grpc.aio.AioRpcError as e:
            return GRPC_STATUS_ERROR, str(e)

Task Preparation and Execution

  • Prepare concurrent tasks to send input data to each Node.

  • Optionally, apply encryption to the data before sending.

  • User sequentially use gRPC to send inputs to the corresponding nodes:

async def prepare_prove_task(self, node, prover, temp, input_data, is_encrypt, token):
    prove_node = ProveServiceClient(node)
    try:
        if is_encrypt:
            code, public_key = await prove_node.get_public_key()
            if code == GRPC_STATUS_ERROR:
                return code, public_key, node
            encrytor = RSAEncryption(public_key=public_key)
            input_data = encrytor.encrypt(input_data)
        
        code, msg, proof = await prove_node.prove(prover=prover, temp=temp, input=input_data, is_encrypted=is_encrypt, token=token)
        return code, msg, proof, node
    except Exception as e:
        return GRPC_STATUS_ERROR, str(e), node

Implement and Invoke Decision Tree

  • Collect verification results from each Node.

  • Use decision tree logic to determine which verification results are valid.

  • Users store successful proofs in the decision tree, which determines available proofs and returns the correct result:

import time
from typing import List, Optional, Union

class Proof:
    def __init__(self) -> None:
        self.proof_list: List[dict] = []

    def add(self, proof: str) -> None:
        """
        Adds a proof to the list along with a timestamp.

        :param proof: The proof string to add.
        """
        self.proof_list.append({"proof": proof, "timestamp": time.time()})

    def get_proof(self) -> Union[str, bool]:
        """
        Retrieves the most frequent proof within 1 second of the earliest proof timestamp.

        :return: The most frequent proof if found, otherwise False.
        """
        if not self.proof_list:
            return False

        # Find the earliest timestamp in the proof list
        earliest_timestamp = min(proof['timestamp'] for proof in self.proof_list)

        # Define the threshold timestamp (1 second after the earliest one)
        threshold_timestamp = earliest_timestamp + 1

        # Filter proofs within the valid time range (<= 1 second from the earliest)
        valid_proofs = [proof['proof'] for proof in self.proof_list if proof['timestamp'] <= threshold_timestamp]

        # Count occurrences of each proof
        proof_counts = {}
        for proof in valid_proofs:
            proof_counts[proof] = proof_counts.get(proof, 0) + 1

        # Find the proof with the highest occurrence
        max_proof = max(proof_counts, key=proof_counts.get, default=None)

        return max_proof if max_proof is not None else False

Firstly, execute the proof task and collect the results. This step involves calling the r0prove on each node to collect its returned code (status code), proof (proof result), and node (node address). These results are stored in the prove_results list.

Error Handling

Manage potential issues such as communication errors and data validation errors.

1. Process and filter proof results

Iterate through each item in the prove_results list. For each result:

  • Check if the code is equal to GRPC_STATUS_ERROR, i.e., check for any errors occurred.

  • If there is an error, log the error and relevant node information using Logging.error.

  • If there are no errors, add the proof to the decision tree's proof_list.

2. Retrieve valid proof results from the decision tree

Call the proof_list.get_proof() method to retrieve valid proof results from the decision tree. This method checks all received proofs and selects the most suitable proof based on timestamp and frequency.

3. Handle the final result

  • If the get_proof() method returns False, indicating no valid proof results, log this status using Logging.error.

  • If there are valid proof results, log the success information and proof results using Logging.info.

Example code explanation

This code is used to process and validate the results returned by multiple nodes during the proof process. The specific purpose of the code is as follows:

for (code, proof, node) in prove_results:
    if code == GRPC_STATUS_ERROR:
        Logging.error(f"[prove] - task[{task_id}] - node[{node}] - Error reason: {proof}")
        continue
    proof_list.add(proof)

proof = proof_list.get_proof()
if not proof:
    Logging.error(f"[prove] - task[{task_id}] - No valid proof")
else:
    Logging.info(f"[prove] - task[{task_id}] Prove successfully, proof: {proof}")
  • This code first loops through the prove_results and processes the results returned by each node.

  • If the returned status code indicates an error (GRPC_STATUS_ERROR), record the error message and continue to the next loop

  • If there are no errors, add the results to the decision tree.

  • Afterwards, try to obtain valid proof from the decision tree.

Record corresponding logs based on whether the proof has been successfully obtained.

Last updated