permalink |
---|
/docs/builtin-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.
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)?;
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),
...
}
}
}
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.