Prover/Verifier Integration

It is very easy to develop or modify a prover or verifier to run in Gevulot. Almost any program that can be compiled as an ELF binary for the Linux x86_64 architecture works, but in order to integrate the program with Gevulot and for it to be able to handle received workloads, it needs to work with the specified gRPC protocol.

Running environment

The program running environment in a Nanos unikernel is very similar to a normal Linux system, but it does include the following restrictions that originate from either Nanos or Gevulot:

  • Not all syscalls are supported. See list of Nanos supported syscalls.

  • There is no networking for security reasons. In on-prem environment there is not even a NIC. In cloud environment there is strictly restricted networking between a Gevulot node and the program VM instance (for gRPC connection only).

  • No fork() support. The whole running environment is single-process only. Multiple threads are supported normally.

  • Root filesystem is read-only. Read-write enabled ephemeral volume is mounted in /workspace. This is replaced after every Task execution.

Shim

To ease the running of existing programs in Gevulot, there is a helper library called shim that provides some simple functionality to integrate the program with Gevulot.

Shim is written in Rust, but it has C FFI bindings to use in programs written in C/C++ or to allow creation of bindings to other programming languages.

Rust

Needed changes for existing programs

To modify an existing program for use in Gevulot using the shim library, the following changes are needed:

1. Provide callback function for running a task

Provide callback function for running a task. This could be e.g. a slightly modified existing main() function that parses program arguments from Task.args instead of std::env::args(). The signature for the callback function is the following:

impl Fn(&Task) -> Result<TaskResult, Box<dyn Error>>

2. Create a new main()

Replace the old main() function with a new one that delegates control to shim:

fn main() -> Result<(), Box<dyn Error>> {
    gevulot_shim::run(my_run_task_callback)
}

Complete example

This is a complete example program in Rust, that can be run inside Gevulot.

use std::{error::Error, result::Result};

use gevulot_shim::{Task, TaskResult};

fn main() -> Result<(), Box<dyn Error>> {
    gevulot_shim::run(run_task)
}

// The main function that executes the prover program.
fn run_task(task: &Task) -> Result<TaskResult, Box<dyn Error>> {
    // Display program arguments we received. These could be used for
    // e.g. parsing CLI arguments with clap.
    println!("prover: task.args: {:?}", &task.args);

    // -----------------------------------------------------------------------
    // Here would be the control logic to run the prover with given arguments.
    // -----------------------------------------------------------------------

    // Write generated proof to a file.
    std::fs::write("/workspace/proof.dat", b"this is a proof.")?;

    // Return TaskResult with reference to the generated proof file.
    task.result(vec![], vec![String::from("/workspace/proof.dat")])
}

C/C++

Gevulot shim

Since gevulot-shim itself is written in Rust, there is a separate gevulot-shim-ffi crate that provides C-bindings via FFI.

Compile the FFI crate to get libgevulot_shim_ffi.so that is then linked into the program binary.

Needed changes for existing programs

C/C++ programs are nearly identical with Rust programs regarding the changes needed for existing programs. There are a couple of small differences in the program flow due to differences in memory management.

1. Include the header file for FFI bindings

shim.h provides function definitions for gevulot-shim-ffi.

Shim header is a C header file. If it is included from C++ program, it must be wrapped with extern as follows:

extern "C" {
  #include "shim.h"
}

2. Provide callback function for running a task

Provide callback function for running a task. This could be e.g. a slightly modified existing main() function that parses program arguments from Task->args instead of **argv. The signature for the callback function is the following:

void *(const struct Task*)

Here the return value of void* denotes the TaskResult struct that is fully managed by the shim library.

3. Create a new main()

Replace the old main() function with a new one that delegates the control to shim:

int main() {
  run(my_run_task_callback);
  return 0;
}

Complete example

This is a complete example program in C, that can be run inside Gevulot.

#include <stdio.h>

#include "shim.h"

void* compute(const struct Task* task) {
  printf("Received task with id: %s\n", task->id);

  printf("Args: \n");
  const char ** args = (const char**)task->args;
  while ((args != NULL) && (*args != NULL)) {
    printf("\t%s\n", *args);
    args++;
  }

  printf("Files: \n");
  const char **files = (const char**)task->files;
  while ((files != NULL) && (*files != NULL)) {
    printf("\t%s\n", *files);
    files++;
  }

  printf("Done with the task.\n");

  return new_task_result(NULL, 0);
}

int main() {
  printf("Starting example C program in Gevulot...\n");
  run(compute);
  printf("Example program finished. Terminating...\n");
  return 0;
}

Differences to Rust shim

Due to memory management there are a couple of differences to the Rust version in gevulot-shim-ffi C/C++ interface:

  • TaskResult object is created with new_task_result(data, len) function.

  • Files that are communicated back to Gevulot node, are added with add_file_to_result(task_result, file_name) function.

Last updated