Skip to content

Latest commit

 

History

History
126 lines (105 loc) · 4.65 KB

builtin-functions.md

File metadata and controls

126 lines (105 loc) · 4.65 KB
permalink
/docs/builtin-functions

How to Add Built-in Functions

There are several ways to execute user-defined functions in the Teaclave platform. One simple way is to write Python scripts and register them as functions, and the scripts will be executed by the MesaPy executor. Another way is to add native functions as built-in functions, and they will be managed by the Built-in executor. Compared to Python scripts, native built-in functions implemented in Rust are memory-safe, have better performance, support more third-party libraries and can be remotely attested as well. In this document, we will guide you through how to add a built-in function to Teaclave step by step with a "private join and compute" example.

In this example, consider several banks have names and balance of their clients. These banks want to compute the total balance of common clients in their private data set without leaking the raw sensitive data to other parties. This is a perfect usage scenario of the Teaclave platform, and we will provide a solution by implementing a built-in function in Teaclave.

Implement Built-in Functions in Rust

All built-in functions are implemented in the teaclave_function crate and can be selectively compiled using feature gates. Basically, one built-in function needs two things: a name and a function implementation. Follow the convention of other built-in function implementations, we define our "private join and compute" function like this:

#[derive(Default)]
pub struct PrivateJoinAndCompute;

impl PrivateJoinAndCompute {
    pub const NAME: &'static str = "builtin-private-join-and-compute";
    pub fn new() -> Self {
        Default::default()
    }
    pub fn run(
        &self,
        arguments: FunctionArguments,
        runtime: FunctionRuntime,
    ) -> Result<String> {
        ...
        Ok(summary)
}

The NAME is the identifier of a function, which is used for creating tasks. Usually, the name of a built-in function starts with the built-in prefix. In addition, we need to define an entry point of the function, which is the run function. The run function can take arguments (in the FunctionAruguments type) and runtime (in the FunctionRuntime type) for interacting with external resources (e.g., reading/writing input/output files). Also, the run function can return a summary of the function execution.

Since the function arguments is in the JSON object format and can be easily deserialized to a Rust struct with serde_json. Therefore, we define a struct PrivateJoinAndComputeArguments which derive the serde::Deserialize trait for the conversion. Then we implement TryFrom trait for the struct to convert the FunctionArguments type to the actual PrivateJoinAndComputeArguments type.

#[derive(serde::Deserialize)]
struct PrivateJoinAndComputeArguments {
    num_user: usize, // Number of users in the multiple party computation
}

impl TryFrom<FunctionArguments> for PrivateJoinAndComputeArguments {
    type Error = anyhow::Error;

    fn try_from(arguments: FunctionArguments) -> Result<Self, Self::Error> {
        use anyhow::Context;
        serde_json::from_str(&arguments.into_string()).context("Cannot deserialize arguments")
    }
}

When executing the function, a runtime object will be passed to the function. We can read or write files with the runtime with the open_input and create_output functions.

// Read data from a file
let mut input_io = runtime.open_input(&input_file_name)?;
input_io.read_to_end(&mut data)?;
...
// Write data into a file
let mut output = runtime.create_output(&output_file_name)?;
output.write_all(&output_bytes)?;

Register Functions in the Executor

To use the function, we need to register it to the built-in executor. Please also put a cfg attribute to make sure developers can conditionally build functions into the executor.

impl TeaclaveExecutor for BuiltinFunctionExecutor {
    fn execute(
        &self,
        name: String,
        arguments: FunctionArguments,
        _payload: String,
        runtime: FunctionRuntime,
    ) -> Result<String> {
        match name.as_str() {
            ...
            #[cfg(feature = "builtin_private_join_and_compute")]
            PrivateJoinAndCompute::NAME => PrivateJoinAndCompute::new().run(arguments, runtime),
            ...
        }
    }
}

Invoke Functions with the Client SDK

Finally, we can invoke the function with the client SDK. In our example, we use the Python client SDK. Basically, this process includes registering input/output files, creating tasks, approving tasks, invoking tasks and getting execution results. You can see more details in the examples/python directory.