diff --git a/README.md b/README.md index 2dbb2f7dd..907733d5d 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,7 @@ platform, making computation on privacy-sensitive data safe and simple. ### Try Teaclave - [My First Function](docs/my-first-function.md) +- [How to Add Built-in Functions](docs/builtin-functions.md) ### Design diff --git a/cmake/scripts/test.sh b/cmake/scripts/test.sh index 64859a030..87e48a83d 100755 --- a/cmake/scripts/test.sh +++ b/cmake/scripts/test.sh @@ -162,6 +162,7 @@ run_examples() { python3 mesapy_echo.py python3 builtin_gbdt_train.py python3 builtin_online_decrypt.py + python3 builtin_private_join_and_compute.py popd # kill all background services diff --git a/docs/README.md b/docs/README.md index f0b306c73..c1b87898c 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,9 +4,10 @@ permalink: /docs/ # Teaclave Documentation -- [My First Function](my-first-function.md) -- [Threat Model](threat-model.md) -- [Rust Development Guideline](rust-guideline.md) -- [Mutual Attestation: Why and How](mutual-attestation.md) - [Access Control in Teaclave](access-control.md) - [Build System](build-system.md) +- [How to Add Built-in Functions](builtin-functions.md) +- [Mutual Attestation: Why and How](mutual-attestation.md) +- [My First Function](my-first-function.md) +- [Rust Development Guideline](rust-guideline.md) +- [Threat Model](threat-model.md) diff --git a/docs/builtin-functions.md b/docs/builtin-functions.md new file mode 100644 index 000000000..cc7c36c57 --- /dev/null +++ b/docs/builtin-functions.md @@ -0,0 +1,126 @@ +--- +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 as functions, +and the scripts will be executed by the *MesaPy executor*. Another way is to add native +functions as built-in functions, and will they 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 total balance of common clients in their private +data set without leaking the raw sensitive client data to other parties. This is +a perfect usage scenarios 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: + +```rust +#[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 { + ... + 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 implement `TryFrom` trait for the struct to convert the +`FunctionArguments` type to the actual `PrivateJoinAndComputeArguments` type. + +```rust +#[derive(serde::Deserialize)] +struct PrivateJoinAndComputeArguments { + num_user: usize, // Number of users in the multiple party computation +} + +impl TryFrom for PrivateJoinAndComputeArguments { + type Error = anyhow::Error; + + fn try_from(arguments: FunctionArguments) -> Result { + 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. + + +```rust +// 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. + +``` rust +impl TeaclaveExecutor for BuiltinFunctionExecutor { + fn execute( + &self, + name: String, + arguments: FunctionArguments, + _payload: String, + runtime: FunctionRuntime, + ) -> Result { + 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 task, approving task, invoking task and getting execution +results. You can see more details in the `examples/python` directory. diff --git a/examples/python/builtin_private_join_and_compute.py b/examples/python/builtin_private_join_and_compute.py new file mode 100644 index 000000000..1c1743511 --- /dev/null +++ b/examples/python/builtin_private_join_and_compute.py @@ -0,0 +1,221 @@ +#!/usr/bin/env python3 + +import sys + +from teaclave import (AuthenticationService, FrontendService, + AuthenticationClient, FrontendClient, FunctionInput, + FunctionOutput, OwnerList, DataMap) +from utils import (AUTHENTICATION_SERVICE_ADDRESS, FRONTEND_SERVICE_ADDRESS, + AS_ROOT_CA_CERT_PATH, ENCLAVE_INFO_PATH, USER_ID, + USER_PASSWORD) + +# In the example, user 3 creates the task and user 0, 1, 2 upload their private data. +# Then user 3 invokes the task and user 0, 1, 2 get the result. + + +class UserData: + def __init__(self, + user_id, + password, + input_url="", + output_url="", + input_cmac="", + key=[]): + self.user_id = user_id + self.password = password + self.input_url = input_url + self.output_url = output_url + self.input_cmac = input_cmac + self.key = key + + +INPUT_FILE_URL_PREFIX = "http://localhost:6789/fixtures/functions/private_join_and_compute/three_party_data/" +OUTPUT_FILE_URL_PREFIX = "http://localhost:6789/fixtures/functions/private_join_and_compute/three_party_data/" + +USER_DATA_0 = UserData("user0", "password", + INPUT_FILE_URL_PREFIX + "bank_a.enc", + OUTPUT_FILE_URL_PREFIX + "user0_output.enc", + "7884a62894e7be50b9795ba22ce5ee7f", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + +USER_DATA_1 = UserData("user1", "password", + INPUT_FILE_URL_PREFIX + "bank_b.enc", + OUTPUT_FILE_URL_PREFIX + "user1_output.enc", + "75b8e931887bd57564d93df31c282bb9", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + +USER_DATA_2 = UserData("user2", "password", + INPUT_FILE_URL_PREFIX + "bank_c.enc", + OUTPUT_FILE_URL_PREFIX + "user2_output.enc", + "35acf29139485067d1ae6212c0577b43", + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]) + +USER_DATA_3 = UserData("user3", "password") + + +class DataList: + def __init__(self, data_name, data_id): + self.data_name = data_name + self.data_id = data_id + + +class ConfigClient: + def __init__(self, user_id, user_password): + self.user_id = user_id + self.user_password = user_password + self.client = AuthenticationService( + AUTHENTICATION_SERVICE_ADDRESS, AS_ROOT_CA_CERT_PATH, + ENCLAVE_INFO_PATH).connect().get_client() + print(f"[+] {self.user_id} registering user") + self.client.user_register(self.user_id, self.user_password) + print(f"[+] {self.user_id} login") + token = self.client.user_login(self.user_id, self.user_password) + self.client = FrontendService( + FRONTEND_SERVICE_ADDRESS, AS_ROOT_CA_CERT_PATH, + ENCLAVE_INFO_PATH).connect().get_client() + metadata = {"id": self.user_id, "token": token} + self.client.metadata = metadata + + def set_task(self): + client = self.client + + print(f"[+] {self.user_id} registering function") + + function_id = client.register_function( + name="builtin-private-join-and-compute", + description="Native Private Join And Compute", + executor_type="builtin", + arguments=["num_user"], + inputs=[ + FunctionInput("input_data0", "Bank A data file."), + FunctionInput("input_data1", "Bank B data file."), + FunctionInput("input_data2", "Bank C data file.") + ], + outputs=[ + FunctionOutput("output_data0", "Output data."), + FunctionOutput("output_data1", "Output data."), + FunctionOutput("output_data2", "Output date.") + ]) + + print(f"[+] {self.user_id} creating task") + task_id = client.create_task(function_id=function_id, + function_arguments=({ + "num_user": 3, + }), + executor="builtin", + inputs_ownership=[ + OwnerList("input_data0", + [USER_DATA_0.user_id]), + OwnerList("input_data1", + [USER_DATA_1.user_id]), + OwnerList("input_data2", + [USER_DATA_2.user_id]) + ], + outputs_ownership=[ + OwnerList("output_data0", + [USER_DATA_0.user_id]), + OwnerList("output_data1", + [USER_DATA_1.user_id]), + OwnerList("output_data2", + [USER_DATA_2.user_id]) + ]) + + return task_id + + def run_task(self, task_id): + client = self.client + client.approve_task(task_id) + print(f"[+] {self.user_id} invoking task") + client.invoke_task(task_id) + + +class DataClient: + def __init__(self, user_id, user_password): + self.user_id = user_id + self.user_password = user_password + self.client = AuthenticationService( + AUTHENTICATION_SERVICE_ADDRESS, AS_ROOT_CA_CERT_PATH, + ENCLAVE_INFO_PATH).connect().get_client() + print(f"[+] {self.user_id} registering user") + self.client.user_register(self.user_id, self.user_password) + print(f"[+] {self.user_id} login") + token = self.client.user_login(self.user_id, self.user_password) + self.client = FrontendService( + FRONTEND_SERVICE_ADDRESS, AS_ROOT_CA_CERT_PATH, + ENCLAVE_INFO_PATH).connect().get_client() + metadata = {"id": self.user_id, "token": token} + self.client.metadata = metadata + + def register_data(self, task_id, input_url, input_cmac, output_url, + file_key, input_label, output_label): + client = self.client + + print(f"[+] {self.user_id} registering input file") + url = input_url + cmac = input_cmac + schema = "teaclave-file-128" + key = file_key + iv = [] + input_id = client.register_input_file(url, schema, key, iv, cmac) + print(f"[+] {self.user_id} registering output file") + url = output_url + schema = "teaclave-file-128" + key = file_key + iv = [] + output_id = client.register_output_file(url, schema, key, iv) + + print(f"[+] {self.user_id} assigning data to task") + client.assign_data_to_task(task_id, [DataList(input_label, input_id)], + [DataList(output_label, output_id)]) + + def approve_task(self, task_id): + client = self.client + print(f"[+] {self.user_id} approving task") + client.approve_task(task_id) + + def get_task_result(self, task_id): + client = self.client + print(f"[+] {self.user_id} getting task result") + return bytes(client.get_task_result(task_id)) + + +def main(): + ## USER 3 creates the task + config_client = ConfigClient(USER_DATA_3.user_id, USER_DATA_3.password) + task_id = config_client.set_task() + + ## USER 0, 1, 2 join the task and upload their data + user0 = DataClient(USER_DATA_0.user_id, USER_DATA_0.password) + user0.register_data(task_id, USER_DATA_0.input_url, USER_DATA_0.input_cmac, + USER_DATA_0.output_url, USER_DATA_0.key, "input_data0", + "output_data0") + + user1 = DataClient(USER_DATA_1.user_id, USER_DATA_1.password) + user1.register_data(task_id, USER_DATA_1.input_url, USER_DATA_1.input_cmac, + USER_DATA_1.output_url, USER_DATA_1.key, "input_data1", + "output_data1") + + user2 = DataClient(USER_DATA_2.user_id, USER_DATA_2.password) + user2.register_data(task_id, USER_DATA_2.input_url, USER_DATA_2.input_cmac, + USER_DATA_2.output_url, USER_DATA_2.key, "input_data2", + "output_data2") + + user0.approve_task(task_id) + user1.approve_task(task_id) + user2.approve_task(task_id) + + ## USER 3 start the computation + config_client.run_task(task_id) + + ## USER 0, 1, 2 get the result + result_user0 = user0.get_task_result(task_id) + result_user1 = user1.get_task_result(task_id) + result_user2 = user2.get_task_result(task_id) + + print("[+] User 0 result: " + result_user0.decode("utf-8")) + print("[+] User 1 result: " + result_user1.decode("utf-8")) + print("[+] User 2 result: " + result_user2.decode("utf-8")) + + +if __name__ == '__main__': + main() diff --git a/executor/Cargo.toml b/executor/Cargo.toml index 19c0e7e24..d1505972b 100644 --- a/executor/Cargo.toml +++ b/executor/Cargo.toml @@ -32,6 +32,7 @@ full_builtin_function = [ "builtin_logistic_regression_predict", "builtin_logistic_regression_train", "builtin_online_decrypt", + "builtin_private_join_and_compute", ] builtin_echo = [] @@ -40,6 +41,7 @@ builtin_gbdt_train = [] builtin_logistic_regression_predict = [] builtin_logistic_regression_train = [] builtin_online_decrypt = [] +builtin_private_join_and_compute = [] [dependencies] log = { version = "0.4.6" } diff --git a/executor/src/builtin.rs b/executor/src/builtin.rs index 990d0147e..ef4c4137e 100644 --- a/executor/src/builtin.rs +++ b/executor/src/builtin.rs @@ -19,7 +19,8 @@ use std::prelude::v1::*; use teaclave_function::{ - Echo, GbdtPredict, GbdtTrain, LogisticRegressionPredict, LogisticRegressionTrain, OnlineDecrypt, + Echo, GbdtPredict, GbdtTrain, LogisticRegressionPredict, LogisticRegressionTrain, + OnlineDecrypt, PrivateJoinAndCompute, }; use teaclave_types::{FunctionArguments, FunctionRuntime, TeaclaveExecutor}; @@ -51,6 +52,8 @@ impl TeaclaveExecutor for BuiltinFunctionExecutor { } #[cfg(feature = "builtin_online_decrypt")] OnlineDecrypt::NAME => OnlineDecrypt::new().run(arguments, runtime), + #[cfg(feature = "builtin_private_join_and_compute")] + PrivateJoinAndCompute::NAME => PrivateJoinAndCompute::new().run(arguments, runtime), _ => bail!("Function not found."), } } diff --git a/function/README.md b/function/README.md index 10938ee65..42df46474 100644 --- a/function/README.md +++ b/function/README.md @@ -18,6 +18,8 @@ Currently, we have these built-in functions: - `builtin-gbdt-predict`: GBDT prediction with input model and input test data. - `bulitin-logistic-regression-train`: Use input data to train a LR model. - `builtin-logistic-regression-predict`: LR prediction with input model and input test data. + - `builtin-private-join-and-compute`: Find intersection of muti-parties' input + data and compute sum of the common items. The function arguments are in JSON format and can be serialized to a Rust struct very easily. You can learn more about supported arguments in the implementation diff --git a/function/src/lib.rs b/function/src/lib.rs index 43189b699..0e8388541 100644 --- a/function/src/lib.rs +++ b/function/src/lib.rs @@ -28,6 +28,7 @@ mod gbdt_train; mod logistic_regression_predict; mod logistic_regression_train; mod online_decrypt; +mod private_join_and_compute; pub use echo::Echo; pub use gbdt_predict::GbdtPredict; @@ -35,6 +36,7 @@ pub use gbdt_train::GbdtTrain; pub use logistic_regression_predict::LogisticRegressionPredict; pub use logistic_regression_train::LogisticRegressionTrain; pub use online_decrypt::OnlineDecrypt; +pub use private_join_and_compute::PrivateJoinAndCompute; #[cfg(feature = "enclave_unit_test")] pub mod tests { @@ -49,6 +51,7 @@ pub mod tests { logistic_regression_train::tests::run_tests(), logistic_regression_predict::tests::run_tests(), online_decrypt::tests::run_tests(), + private_join_and_compute::tests::run_tests(), ) } } diff --git a/function/src/private_join_and_compute.rs b/function/src/private_join_and_compute.rs new file mode 100644 index 000000000..0be5b9495 --- /dev/null +++ b/function/src/private_join_and_compute.rs @@ -0,0 +1,201 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +use anyhow::{bail, Result}; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt; +use std::format; +use std::io::Write; +#[cfg(feature = "mesalock_sgx")] +use std::prelude::v1::*; +use teaclave_types::{FunctionArguments, FunctionRuntime}; + +const IN_DATA: &str = "input_data"; +const OUT_RESULT: &str = "output_data"; + +#[derive(Default)] +pub struct PrivateJoinAndCompute; + +#[derive(serde::Deserialize)] +struct PrivateJoinAndComputeArguments { + num_user: usize, // Number of users in the mutiple party computation +} + +impl TryFrom for PrivateJoinAndComputeArguments { + type Error = anyhow::Error; + + fn try_from(arguments: FunctionArguments) -> Result { + use anyhow::Context; + serde_json::from_str(&arguments.into_string()).context("Cannot deserialize arguments") + } +} + +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, + ) -> anyhow::Result { + let args = PrivateJoinAndComputeArguments::try_from(arguments)?; + let num_user = args.num_user; + if num_user < 2 { + bail!("The demo requires at least two parties!"); + } + + let mut output = String::new(); + let data_0 = get_data(0, &runtime)?; + let input_map_0 = parse_input(data_0)?; + let mut res_map: HashMap = input_map_0; + + for i in 1..num_user { + let data = get_data(i, &runtime)?; + let input_map = parse_input(data)?; + res_map = get_intersection_sum(&input_map, &res_map); + } + + for (identity, amount) in res_map { + fmt::write(&mut output, format_args!("{}, {}\n", identity, amount))?; + } + + let output_bytes = output.as_bytes(); + + for i in 0..num_user { + let output_file_name = format!("{}{}", OUT_RESULT, i); + let mut output = runtime.create_output(&output_file_name)?; + output.write_all(&output_bytes)?; + } + + let summary = format!("{} users join the task in total.", num_user); + Ok(summary) + } +} + +fn get_data(user_id: usize, runtime: &FunctionRuntime) -> anyhow::Result> { + let mut data: Vec = Vec::new(); + let input_file_name = format!("{}{}", IN_DATA, user_id); + let mut input_io = runtime.open_input(&input_file_name)?; + input_io.read_to_end(&mut data)?; + Ok(data) +} + +fn get_intersection_sum( + map1: &HashMap, + map2: &HashMap, +) -> HashMap { + let mut res_map: HashMap = HashMap::new(); + for (identity, amount) in map1 { + if map2.contains_key(identity) { + let total = amount + map2[identity]; + res_map.insert(identity.to_owned(), total); + } + } + res_map +} + +fn parse_input(data: Vec) -> anyhow::Result> { + let data_list = String::from_utf8(data)?; + let mut ret: HashMap = HashMap::new(); + for data_item in data_list.split('\n') { + let pair = data_item.trim(); + if pair.len() < 3 { + continue; + } + let kv_pair: Vec<&str> = pair.split(':').collect(); + if kv_pair.len() != 2 { + continue; + } + let identity = kv_pair[0].trim().to_string(); + let amount = match kv_pair[1].trim().parse::() { + Ok(amount) => amount, + Err(_) => continue, + }; + ret.insert(identity, amount); + } + Ok(ret) +} + +#[cfg(feature = "enclave_unit_test")] +pub mod tests { + use super::*; + use serde_json::json; + use std::untrusted::fs; + use teaclave_crypto::*; + use teaclave_runtime::*; + use teaclave_test_utils::*; + use teaclave_types::*; + + pub fn run_tests() -> bool { + run_tests!(test_private_join_and_compute) + } + + fn test_private_join_and_compute() { + let arguments = FunctionArguments::from_json(json!({ + "num_user": 3 + })) + .unwrap(); + + let user0_input = "fixtures/functions/private_join_and_compute/three_party_data/bank_a.txt"; + let user0_output = + "fixtures/functions/private_join_and_compute/three_party_data/user0_output.txt"; + + let user1_input = "fixtures/functions/private_join_and_compute/three_party_data/bank_b.txt"; + let user1_output = + "fixtures/functions/private_join_and_compute/three_party_data/user1_output.txt"; + + let user2_input = "fixtures/functions/private_join_and_compute/three_party_data/bank_c.txt"; + let user2_output = + "fixtures/functions/private_join_and_compute/three_party_data/user2_output.txt"; + + let input_files = StagedFiles::new(hashmap!( + "input_data0" => + StagedFileInfo::new(user0_input, TeaclaveFile128Key::random(), FileAuthTag::mock()), + "input_data1" => + StagedFileInfo::new(user1_input, TeaclaveFile128Key::random(), FileAuthTag::mock()), + "input_data2" => + StagedFileInfo::new(user2_input, TeaclaveFile128Key::random(), FileAuthTag::mock()) + )); + + let output_files = StagedFiles::new(hashmap!( + "output_data0" => + StagedFileInfo::new(user0_output, TeaclaveFile128Key::random(), FileAuthTag::mock()), + "output_data1" => + StagedFileInfo::new(user1_output, TeaclaveFile128Key::random(), FileAuthTag::mock()), + "output_data2" => + StagedFileInfo::new(user2_output, TeaclaveFile128Key::random(), FileAuthTag::mock()) + )); + + let runtime = Box::new(RawIoRuntime::new(input_files, output_files)); + + let summary = PrivateJoinAndCompute::new() + .run(arguments, runtime) + .unwrap(); + + let user0 = fs::read_to_string(&user0_output).unwrap(); + let user1 = fs::read_to_string(&user1_output).unwrap(); + let user2 = fs::read_to_string(&user2_output).unwrap(); + assert_eq!(&user0[..], &user1[..]); + assert_eq!(&user1[..], &user2[..]); + assert_eq!(summary, "3 users join the task in total.") + } +} diff --git a/runtime/src/raw_io.rs b/runtime/src/raw_io.rs index 9d88d8526..d6a665ca0 100644 --- a/runtime/src/raw_io.rs +++ b/runtime/src/raw_io.rs @@ -54,6 +54,7 @@ impl TeaclaveRuntime for RawIoRuntime { .output_files .get(identifier) .ok_or_else(|| anyhow::anyhow!("Invalid output file identifier"))?; + log::debug!("create_output: {:?}", file_info.path); let f = File::create(&file_info.path)?; Ok(Box::new(f)) } diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.enc b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.enc new file mode 100644 index 000000000..58001543b Binary files /dev/null and b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.enc differ diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.txt b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.txt new file mode 100644 index 000000000..1816421a3 --- /dev/null +++ b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_a.txt @@ -0,0 +1,5 @@ +b : 2000 +a : 100 +c : 30000 +e : 5000000 +d : 400000 diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.enc b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.enc new file mode 100644 index 000000000..eb76a7db1 Binary files /dev/null and b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.enc differ diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.txt b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.txt new file mode 100644 index 000000000..06b09c23f --- /dev/null +++ b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_b.txt @@ -0,0 +1,5 @@ +e : 3000 +x : 200 +c : 40000 +y : 10 +a : 5000000 \ No newline at end of file diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.enc b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.enc new file mode 100644 index 000000000..5fa5ec1cf Binary files /dev/null and b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.enc differ diff --git a/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.txt b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.txt new file mode 100644 index 000000000..7233cbc81 --- /dev/null +++ b/tests/fixtures/functions/private_join_and_compute/three_party_data/bank_c.txt @@ -0,0 +1,5 @@ +e : 30000 +x : 200 +c : 400000 +y : 10 +d : 5000000