From b882fa774bae723a2d259804dd674cdddef90221 Mon Sep 17 00:00:00 2001 From: wenyi tang Date: Tue, 13 Aug 2024 14:20:04 +0000 Subject: [PATCH 1/3] initial commit with basic contract --- .gitignore | 1 + .../pdo/exchange/plugins/token_object.py | 3 +- medperf-contract/CMakeLists.txt | 37 ++ medperf-contract/MANIFEST | 32 ++ medperf-contract/MANIFEST.in | 4 + medperf-contract/README.md | 154 ++++++ medperf-contract/context/tokens.toml | 54 ++ medperf-contract/contracts/token_object.cpp | 80 +++ medperf-contract/etc/guardian_service.toml | 69 +++ medperf-contract/etc/medperf.toml | 19 + .../medperf/contracts/token_object.cpp | 334 ++++++++++++ medperf-contract/medperf/token_object.h | 89 +++ medperf-contract/medperf_common.cmake | 39 ++ medperf-contract/pdo/medperf/__init__.py | 15 + .../pdo/medperf/common/__init__.py | 22 + .../pdo/medperf/common/capability_keys.py | 118 ++++ .../pdo/medperf/common/capability_keystore.py | 57 ++ .../pdo/medperf/common/endpoint_registry.py | 44 ++ .../pdo/medperf/common/guardian_service.py | 136 +++++ .../pdo/medperf/common/secrets.py | 87 +++ .../pdo/medperf/common/utility.py | 23 + .../pdo/medperf/operations/__init__.py | 22 + .../pdo/medperf/operations/use_dataset.py | 141 +++++ .../pdo/medperf/plugins/__init__.py | 15 + .../pdo/medperf/plugins/medperf_guardian.py | 286 ++++++++++ .../medperf/plugins/medperf_token_object.py | 511 ++++++++++++++++++ .../pdo/medperf/resources/__init__.py | 15 + .../pdo/medperf/resources/resources.py | 18 + .../pdo/medperf/scripts/__init__.py | 0 .../pdo/medperf/scripts/guardianCLI.py | 313 +++++++++++ .../pdo/medperf/scripts/scripts.py | 31 ++ medperf-contract/pdo/medperf/wsgi/__init__.py | 36 ++ .../pdo/medperf/wsgi/add_endpoint.py | 114 ++++ medperf-contract/pdo/medperf/wsgi/info.py | 56 ++ .../pdo/medperf/wsgi/process_capability.py | 118 ++++ .../medperf/wsgi/provision_token_issuer.py | 76 +++ .../medperf/wsgi/provision_token_object.py | 96 ++++ medperf-contract/scripts/gs_start.sh | 90 +++ medperf-contract/scripts/gs_status.sh | 36 ++ medperf-contract/scripts/gs_stop.sh | 48 ++ medperf-contract/scripts/ss_start.sh | 77 +++ medperf-contract/scripts/ss_status.sh | 36 ++ medperf-contract/scripts/ss_stop.sh | 48 ++ medperf-contract/setup.py | 102 ++++ medperf-contract/test/.gitignore | 1 + medperf-contract/test/script_test.sh | 231 ++++++++ 46 files changed, 3933 insertions(+), 1 deletion(-) create mode 100644 medperf-contract/CMakeLists.txt create mode 100644 medperf-contract/MANIFEST create mode 100644 medperf-contract/MANIFEST.in create mode 100644 medperf-contract/README.md create mode 100644 medperf-contract/context/tokens.toml create mode 100644 medperf-contract/contracts/token_object.cpp create mode 100644 medperf-contract/etc/guardian_service.toml create mode 100644 medperf-contract/etc/medperf.toml create mode 100644 medperf-contract/medperf/contracts/token_object.cpp create mode 100644 medperf-contract/medperf/token_object.h create mode 100644 medperf-contract/medperf_common.cmake create mode 100644 medperf-contract/pdo/medperf/__init__.py create mode 100644 medperf-contract/pdo/medperf/common/__init__.py create mode 100644 medperf-contract/pdo/medperf/common/capability_keys.py create mode 100644 medperf-contract/pdo/medperf/common/capability_keystore.py create mode 100644 medperf-contract/pdo/medperf/common/endpoint_registry.py create mode 100644 medperf-contract/pdo/medperf/common/guardian_service.py create mode 100644 medperf-contract/pdo/medperf/common/secrets.py create mode 100644 medperf-contract/pdo/medperf/common/utility.py create mode 100644 medperf-contract/pdo/medperf/operations/__init__.py create mode 100644 medperf-contract/pdo/medperf/operations/use_dataset.py create mode 100644 medperf-contract/pdo/medperf/plugins/__init__.py create mode 100644 medperf-contract/pdo/medperf/plugins/medperf_guardian.py create mode 100644 medperf-contract/pdo/medperf/plugins/medperf_token_object.py create mode 100644 medperf-contract/pdo/medperf/resources/__init__.py create mode 100644 medperf-contract/pdo/medperf/resources/resources.py create mode 100644 medperf-contract/pdo/medperf/scripts/__init__.py create mode 100644 medperf-contract/pdo/medperf/scripts/guardianCLI.py create mode 100644 medperf-contract/pdo/medperf/scripts/scripts.py create mode 100644 medperf-contract/pdo/medperf/wsgi/__init__.py create mode 100644 medperf-contract/pdo/medperf/wsgi/add_endpoint.py create mode 100644 medperf-contract/pdo/medperf/wsgi/info.py create mode 100644 medperf-contract/pdo/medperf/wsgi/process_capability.py create mode 100644 medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py create mode 100644 medperf-contract/pdo/medperf/wsgi/provision_token_object.py create mode 100755 medperf-contract/scripts/gs_start.sh create mode 100755 medperf-contract/scripts/gs_status.sh create mode 100755 medperf-contract/scripts/gs_stop.sh create mode 100755 medperf-contract/scripts/ss_start.sh create mode 100755 medperf-contract/scripts/ss_status.sh create mode 100755 medperf-contract/scripts/ss_stop.sh create mode 100644 medperf-contract/setup.py create mode 100644 medperf-contract/test/.gitignore create mode 100755 medperf-contract/test/script_test.sh diff --git a/.gitignore b/.gitignore index 95c00b7..ffb6d7e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ dist *.egg-info *.ipynb_checkpoints *.pyc +quick_*.sh \ No newline at end of file diff --git a/exchange-contract/pdo/exchange/plugins/token_object.py b/exchange-contract/pdo/exchange/plugins/token_object.py index 35ba2e6..c9af97e 100644 --- a/exchange-contract/pdo/exchange/plugins/token_object.py +++ b/exchange-contract/pdo/exchange/plugins/token_object.py @@ -240,7 +240,8 @@ def mint_one_token(cls, state, to_context, ti_context, dg_context, ledger_submit state, to_context, to_session, ledger_key, to_package, - authority) + authority, + **kwargs) return to_save_file @classmethod diff --git a/medperf-contract/CMakeLists.txt b/medperf-contract/CMakeLists.txt new file mode 100644 index 0000000..d4f1b51 --- /dev/null +++ b/medperf-contract/CMakeLists.txt @@ -0,0 +1,37 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# This is necessary to get at the definitions necessary +# for the std::string class +INCLUDE(exchange_common) +LIST(APPEND WASM_LIBRARIES ${EXCHANGE_LIB}) +LIST(APPEND WASM_INCLUDES ${EXCHANGE_INCLUDES}) + + +INCLUDE(medperf_common.cmake) +LIST(APPEND WASM_LIBRARIES ${MEDPERF_LIB}) +LIST(APPEND WASM_INCLUDES ${MEDPERF_INCLUDES}) + +ADD_LIBRARY(${MEDPERF_LIB} STATIC ${MEDPERF_SOURCES}) +TARGET_INCLUDE_DIRECTORIES(${MEDPERF_LIB} PUBLIC ${MEDPERF_INCLUDES}) + +SET_PROPERTY(TARGET ${MEDPERF_LIB} APPEND_STRING PROPERTY COMPILE_OPTIONS "${WASM_BUILD_OPTIONS}") +SET_PROPERTY(TARGET ${MEDPERF_LIB} APPEND_STRING PROPERTY LINK_OPTIONS "${WASM_LINK_OPTIONS}") +SET_TARGET_PROPERTIES(${MEDPERF_LIB} PROPERTIES EXCLUDE_FROM_ALL TRUE) + +BUILD_CONTRACT(medperf_token_object contracts/token_object.cpp) + +# ----------------------------------------------------------------- +INCLUDE(Python) +BUILD_WHEEL(medperf medperf_token_object) \ No newline at end of file diff --git a/medperf-contract/MANIFEST b/medperf-contract/MANIFEST new file mode 100644 index 0000000..7b26b13 --- /dev/null +++ b/medperf-contract/MANIFEST @@ -0,0 +1,32 @@ +MANIFEST.in +./setup.py +./pdo/medperf/wsgi/provision_token_issuer.py +./pdo/medperf/wsgi/__init__.py +./pdo/medperf/wsgi/add_endpoint.py +./pdo/medperf/wsgi/provision_token_object.py +./pdo/medperf/wsgi/info.py +./pdo/medperf/wsgi/process_capability.py +./pdo/medperf/operations/use_dataset.py +./pdo/medperf/operations/__init__.py +./pdo/medperf/plugins/medperf_token_object.py +./pdo/medperf/plugins/__init__.py +./pdo/medperf/plugins/medperf_guardian.py +./pdo/medperf/__init__.py +./pdo/medperf/resources/resources.py +./pdo/medperf/resources/__init__.py +./pdo/medperf/common/guardian_service.py +./pdo/medperf/common/capability_keystore.py +./pdo/medperf/common/secrets.py +./pdo/medperf/common/__init__.py +./pdo/medperf/common/endpoint_registry.py +./pdo/medperf/common/utility.py +./pdo/medperf/common/capability_keys.py +./pdo/medperf/scripts/__init__.py +./pdo/medperf/scripts/guardianCLI.py +./pdo/medperf/scripts/scripts.py +./scripts/gs_stop.sh +./scripts/gs_start.sh +./scripts/gs_status.sh +./context/tokens.toml +./etc/medperf.toml +./etc/guardian_service.toml \ No newline at end of file diff --git a/medperf-contract/MANIFEST.in b/medperf-contract/MANIFEST.in new file mode 100644 index 0000000..7eb8ee6 --- /dev/null +++ b/medperf-contract/MANIFEST.in @@ -0,0 +1,4 @@ +recursive-include ../build/medperf-contract *.b64 +recursive-include etc *.toml +recursive-include context *.toml +recursive-include scripts *.sh diff --git a/medperf-contract/README.md b/medperf-contract/README.md new file mode 100644 index 0000000..0deeb68 --- /dev/null +++ b/medperf-contract/README.md @@ -0,0 +1,154 @@ + + +# PDO-enhanced MedPerf Workflow +This document includes an enhanced workflow for Federated Evaluation (FE) in machine learning, aiming in providing a policy-enforced digital asset usage framwork with tokenization. The framework is built upon the FE workflow from [MedPerf](https://github.com/mlcommons/medperf). + +## Note + +The initial version of this project only covers the protection of dataset. Other digital assets, including the model (weights)and experiment results are not included for now. + +## Testing Workflow + +```mermaid +sequenceDiagram + participant E as Experiment Committee + participant PDO as PDO Contract (SGX) + participant G as Digital Guardian Service + participant D as Data Owner + participant M as MedPerf Server + %% participant MO as Model Owner + note over G, M: experiment-data association (assume finished) + note over G, M: model-data association (assume finished) + note over M, PDO: assume the states data are synchronized + loop Attestation + PDO -> G: + end + D ->> PDO: create token_issuer and call cmd_mint_token + PDO ->> PDO: generate new identity token_issuer and new contract token.token_object + D ->> PDO: transfer token.token_object to new owner token_experitment1 + PDO ->> PDO: generate new identity token_expertiment1 and new contract token.token_experiment1 + note over E, PDO: owership transferred to Experiment committee + note over M: notification about token/dataset availiability + box Assume Trusted + participant M + end + box SGX Enclave + participant G + end + E ->> PDO: request the inference on specific model model_id, experiment_id (cmd_use_dataset) + activate PDO + PDO ->> PDO: policy evaluation + note right of PDO: policy:
experiment_id = experiment_id'
model_id in {model_id} + PDO ->> E: return capabilities, encoded with urls for dockerfile and weights of the granted model.identity of + deactivate PDO + E ->> G: request access to service with capability (storage ) + E ->> D: + activate G + note over G: pull/download/deploy mlcube (the model) + G ->> G: run inference over the dataset + G ->> D: return result (basic verification with hash) + D ->> E: return results + deactivate G +``` + + +## Protocols + +From the dataset owner side: ++ The dataset is identified by its hash. ++ Tokens are minted based on hash-based binding. ++ Guardian service of the token exposes an api to the callers + + +## Core functions + ++ Allow data owner to tokenize a dataset (into a PDO contract) + + with default policy checking the registration information from MedPerf + + ownership transfer to experiment committee + + generate capability to grant access to the guardian service + ++ Allow experiment committee to initilize the use of data by interacting with PDO + + capabilities are published on MedPerf server (or PDO states/ledger?) for downloading + ++ Allow data owner to host a guardian service, which exposes interface (local) to initilize the test + + data owner downloads the capability from the server (or ledger) + + data owner feeds the capability to guardian service to initialize the experiment + + guardian service publish experiment results to the server (or ledger) + + allows access control? + + + +### Guardian service + +Datasets are hosted behind the service. + + + + +The guardian service is co-located with the dataset. guadian service provides a wsgi api to process the capability. Capability encodes `{model_id, dataset_id, url_to_docker, url_to_weights}`. After receiving capability, the guardian service: +1. pull/build docker images from `url_to_docker` +2. download weights from `url_to_weights` +3. model up and run over `dataset_id` + +no execution integrity for now. + + + + +### Contract methods + +new contract methods under the class `ww::medperf::token_object` + +`initialize`: public methode, mint token for the dataset, actual method behind `cmd_mint_token` + 1. kvs of the experiment/model/dataset info + 2. Store the registered metadata from medperf service (synthetic for PoC). + 3. set a `max_evaluation` + +| Keys | Values | +| ---- | ---- | +| experiment_id | identifier for the experimetn | +| model_id | identifier for the model | +| {urls} | url to model assets | +| dataset_id | hash of the dataset | +| max_evaluation | most models that allowed to evaluate | +| cur_evaluation | 0 | +| approved_capability| {} | +| TBA | + +`get_datasetinfo`: public method, get the non-secret kvs info of dataset token +return the information associated with the dataset. + +capability -links to- identity // invoked by the dataowner only + + + +`use_dataset(model_id, dataset_id, experiment_id)`: only invoked by TO + 1. check if model_id, dataset_id, experiment_id are in the kv storage + 2. cur_evaluation + 1, if cur_evaluation = max_evaluation, fail + 3. invoke `get_capability` and return secretly encoded {urls}. + +allows multiple models in one call + +`get_capability`: only invoked by TO, parse capability kv + + + + + + +## Testing cmd ++ create token issuer and mint the token for the dataset. ++ token issuer transfers token to model_owner1 ++ model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` + + get capability for one model, increase the count + + call service to run inference ++ model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` + + max_evaluation exceeds, fail \ No newline at end of file diff --git a/medperf-contract/context/tokens.toml b/medperf-contract/context/tokens.toml new file mode 100644 index 0000000..72e9379 --- /dev/null +++ b/medperf-contract/context/tokens.toml @@ -0,0 +1,54 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# ----------------------------------------------------------------- +# token ${token} +# ----------------------------------------------------------------- +[token.${token}.asset_type] +module = "pdo.exchange.plugins.asset_type" +identity = "token_type" +source = "${ContractFamily.Exchange.asset_type.source}" +name = "${token}" +description = "asset type for ${token} token objects" +link = "http://" + +[token.${token}.vetting] +module = "pdo.exchange.plugins.vetting" +identity = "token_vetting" +source = "${ContractFamily.Exchange.vetting.source}" +asset_type_context = "@{..asset_type}" + +[token.${token}.guardian] +module = "pdo.medperf.plugins.medperf_guardian" +url = "${url}" +identity = "${..token_issuer.identity}" +token_issuer_context = "@{..token_issuer}" +service_only = true + +[token.${token}.token_issuer] +module = "pdo.exchange.plugins.token_issuer" +identity = "token_issuer" +source = "${ContractFamily.Exchange.token_issuer.source}" +token_object_context = "@{..token_object}" +vetting_context = "@{..vetting}" +guardian_context = "@{..guardian}" +description = "issuer for token ${token}" +count = 1 + +[token.${token}.token_object] +module = "pdo.medperf.plugins.medperf_token_object" +identity = "${..token_issuer.identity}" +source = "${ContractFamily.medperf.token_object.source}" +token_issuer_context = "@{..token_issuer}" +data_guardian_context = "@{..guardian}" diff --git a/medperf-contract/contracts/token_object.cpp b/medperf-contract/contracts/token_object.cpp new file mode 100644 index 0000000..e0dff7c --- /dev/null +++ b/medperf-contract/contracts/token_object.cpp @@ -0,0 +1,80 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed 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. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/base.h" +#include "contract/attestation.h" +#include "exchange/issuer_authority_base.h" +#include "exchange/token_object.h" +#include "medperf/token_object.h" + +// ----------------------------------------------------------------- +// METHOD: initialize_contract +// ----------------------------------------------------------------- +bool initialize_contract(const Environment& env, Response& rsp) +{ + ASSERT_SUCCESS(rsp, ww::exchange::token_object::initialize_contract(env), + "failed to initialize the base contract"); + + return rsp.success(true); +} + +// ----------------------------------------------------------------- +// ----------------------------------------------------------------- +contract_method_reference_t contract_method_dispatch_table[] = { + + CONTRACT_METHOD2(get_verifying_key, ww::contract::base::get_verifying_key), + CONTRACT_METHOD2(initialize, ww::medperf::token_object::initialize), + + // issuer methods + CONTRACT_METHOD2(get_asset_type_identifier, ww::exchange::issuer_authority_base::get_asset_type_identifier), + CONTRACT_METHOD2(get_issuer_authority, ww::exchange::issuer_authority_base::get_issuer_authority), + CONTRACT_METHOD2(get_authority, ww::exchange::issuer_authority_base::get_authority), + + // from the attestation contract + CONTRACT_METHOD2(get_contract_metadata, ww::contract::attestation::get_contract_metadata), + CONTRACT_METHOD2(get_contract_code_metadata, ww::contract::attestation::get_contract_code_metadata), + + // use the asset + CONTRACT_METHOD2(get_dataset_info, ww::medperf::token_object::get_dataset_info), + CONTRACT_METHOD2(use_dataset, ww::medperf::token_object::use_dataset), + CONTRACT_METHOD2(get_capability, ww::medperf::token_object::get_capability), + CONTRACT_METHOD2(hello_world, ww::medperf::token_object::hello_world), + CONTRACT_METHOD2(hello_world_function, ww::medperf::token_object::hello_world_function), + + // object transfer, escrow & claim methods + CONTRACT_METHOD2(transfer,ww::exchange::token_object::transfer), + CONTRACT_METHOD2(escrow,ww::exchange::token_object::escrow), + CONTRACT_METHOD2(escrow_attestation,ww::exchange::token_object::escrow_attestation), + CONTRACT_METHOD2(release,ww::exchange::token_object::release), + CONTRACT_METHOD2(claim,ww::exchange::token_object::claim), + + { NULL, NULL } +}; diff --git a/medperf-contract/etc/guardian_service.toml b/medperf-contract/etc/guardian_service.toml new file mode 100644 index 0000000..c3d115a --- /dev/null +++ b/medperf-contract/etc/guardian_service.toml @@ -0,0 +1,69 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# -------------------------------------------------- +# GuardianService -- general information about the guardian service +# -------------------------------------------------- +[GuardianService] +# Identity is a string used to identify the service in log files +Identity = "${identity}" +HttpPort = 7900 +Host = "${host}" + +# -------------------------------------------------- +# StorageService -- information about passing kv stores +# -------------------------------------------------- +[StorageService] +URL = "http://${host}:7901" +KeyValueStore = "${data}/guardian_service.mdb" +BlockStore = "${data}/guardian_service.mdb" +Identity = "${identity}" +HttpPort = 7901 +Host = "${host}" +GarbageCollectionInterval = 0 +MaxDuration = 0 + +# -------------------------------------------------- +# Keys -- configuration for retrieving service keys +# -------------------------------------------------- +[Key] +SearchPath = [ ".", "./keys", "${keys}" ] +FileName = "${identity}_private.pem" + +# -------------------------------------------------- +# Logging -- configuration of service logging +# -------------------------------------------------- +[Logging] +LogLevel = "INFO" +LogFile = "${logs}/${identity}.log" + +# -------------------------------------------------- +# Data -- names for the various databases +# -------------------------------------------------- +[Data] +EndpointRegistry = "${data}/endpoints.db" +CapabilityKeyStore = "${data}/keystore.db" + +# -------------------------------------------------- +# TokenIssuer -- configuration for TI verification +# -------------------------------------------------- +[TokenIssuer] +LedgerKey = "" +CodeHash = "" +ContractIDs = [] + +# -------------------------------------------------- +# TokenObject -- configuration for TO verification +# -------------------------------------------------- +[TokenObject] diff --git a/medperf-contract/etc/medperf.toml b/medperf-contract/etc/medperf.toml new file mode 100644 index 0000000..9dafa21 --- /dev/null +++ b/medperf-contract/etc/medperf.toml @@ -0,0 +1,19 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# ----------------------------------------------------------------- +# medperf family contract source +# ----------------------------------------------------------------- +[ContractFamily.medperf] +token_object = { source = "${home}/contracts/medperf/_medperf_token_object.b64" } diff --git a/medperf-contract/medperf/contracts/token_object.cpp b/medperf-contract/medperf/contracts/token_object.cpp new file mode 100644 index 0000000..6e8bee9 --- /dev/null +++ b/medperf-contract/medperf/contracts/token_object.cpp @@ -0,0 +1,334 @@ +/* Copyright 2024 Intel Corporation + * + * Licensed 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. + */ + +#include +#include +#include + +#include "Dispatch.h" + +#include "Cryptography.h" +#include "KeyValue.h" +#include "Environment.h" +#include "Message.h" +#include "Response.h" +#include "Types.h" +#include "Util.h" +#include "Value.h" +#include "WasmExtensions.h" + +#include "contract/attestation.h" +#include "contract/base.h" +#include "exchange/token_object.h" +#include "medperf/token_object.h" + +static KeyValueStore dataset_TO_store("dataset_TO_store"); +static const std::string dataset_id_KEY("dataset_id"); +static const std::string experiment_id_KEY("experiment_id"); +static const std::string associated_model_ids_KEY("associated_model_ids_json_string"); +// static const std::string hfmodel_user_inputs_schema_KEY("hfmodel_user_inputs_schema"); +// static const std::string hfmodel_request_payload_type_KEY("hfmodel_request_payload_type"); +// static const std::string hfmodel_usage_info_KEY("hfmodel_usage_info"); +static const std::string dataset_max_use_count_KEY("dataset_max_use_count"); +static const std::string dataset_current_use_count_KEY("dataset_current_use_count"); + +static const std::string dataset_use_capability_kv_store_encryption_key_KEY("dataset_use_capability_kv_store_encryption_key"); +static const std::string dataset_use_capability_kv_store_root_block_hash_KEY("dataset_use_capability_kv_store_root_block_hash"); +static const std::string dataset_use_capability_kv_store_input_key_KEY("dataset_use_capability_kv_store_input_key"); +// static const std::string dataset_use_capability_user_inputs_KEY("dataset_use_capability_user_inputs"); + +// + +// ----------------------------------------------------------------- +// METHOD: initialize +// +// 1. Store the dataset_id (hash of the dataset) that is tokenized. This information should be generated by the dataset owner. The dataset_id is not cross-checked with the dataset itself for now. +// 2. For testing purposes, the experiment_id is associated with the dataset_token at this step, which implies that the dataset is by default consent to be inferenced by all the models under the associated experiment. +// We will Initilize the token object with no experiment/model association. The association will be done via an additional association method. +// 3. The model_id are also stored in the token object. The model_id is the path to the modelcubes. (So far we assume all model matierials are stored locally) +// +// Note that the token object is intentionally kept generic and not hard-coded to any specific model. +// ------------------------------------------------------------------------------------------------------------- +bool ww::medperf::token_object::initialize(const Message &msg, const Environment &env, Response &rsp) +{ + ASSERT_SENDER_IS_CREATOR(env, rsp); + ASSERT_UNINITIALIZED(rsp); + + // print the message to the log + + + ASSERT_SUCCESS(rsp, msg.validate_schema(DATASET_TO_INIT_PARAM_SCHEMA), + "invalid request, missing required parameters for medperf dataset token object initialization"); + + // Get the params to be stored in dataset_TO_store + const std::string dataset_id_value(msg.get_string("dataset_id")); + const std::string experiment_id_value(msg.get_string("experiment_id")); + const std::string associated_model_ids_value(msg.get_string("associated_model_ids")); + // const std::string hfmodel_user_inputs_schema_value(msg.get_string("user_inputs_schema")); + // const std::string hfmodel_request_payload_type_value(msg.get_string("payload_type")); + // const std::string hfmodel_usage_info_value(msg.get_string("hfmodel_usage_info")); + const uint32_t dataset_max_use_count_value = (uint32_t)msg.get_number("max_use_count"); + + // Store params from msg in dataset_TO_store + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_id_KEY, dataset_id_value), "failed to store dataset_id"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, experiment_id_value), "failed to store experiment_id"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_value), "failed to store associated_model_ids"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_value), "failed to store hfmodel_user_inputs_schema"); + // // // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_request_payload_type_KEY, hfmodel_request_payload_type_value), "failed to store hfmodel_request_payload_type"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_usage_info_KEY, hfmodel_usage_info_value), "failed to store hfmodel_usage_info"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to store dataset_max_use_count"); + + // Set current use count to 0 + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, (uint32_t)0), "failed to store dataset_current_use_count"); + + // Do the rest of the initialization of the token object via the initialize method in the exchange contract + ww::value::Structure to_message(TO_INITIALIZE_PARAM_SCHEMA); + + const std::string ledger_verifying_key(msg.get_string("ledger_verifying_key")); + ww::value::Object initialization_package; + msg.get_value("initialization_package", initialization_package); + ww::value::Object asset_authority_chain; + msg.get_value("asset_authority_chain", asset_authority_chain); + + ASSERT_SUCCESS(rsp, to_message.set_string("ledger_verifying_key", ledger_verifying_key.c_str()), "unexpected error: failed to set the parameter"); + ASSERT_SUCCESS(rsp, to_message.set_value("initialization_package", initialization_package), "unexpected error: failed to set the parameter"); + ASSERT_SUCCESS(rsp, to_message.set_value("asset_authority_chain", asset_authority_chain), "unexpected error: failed to set the parameter"); + + return ww::exchange::token_object::initialize(to_message, env, rsp); +} + +// ----------------------------------------------------------------- +// METHOD: get_dataset_info +// +// Return fixed model parameters, schema for user-specified model parameters, and +// model metadata useful for the TO to understand how to use the model required to invoke Inference API. +// Method is public, and can be invoked by PDO user. +// note that we are not returning the "remaining use count" as part of the model info. +// ideally a prospective token buyer would like access to "remaining use count" before purchasing the token. +// In such a case, ideally such information shall be provided only after escrow of payment for the token is done. +// Left for future enhancement. +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::get_dataset_info( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_INITIALIZED(rsp); + ww::value::Structure v(DATASET_INFO_SCHEMA); + + // Get the dataset_id + std::string dataset_id_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_string), "failed to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, v.set_string("dataset_id", dataset_id_string.c_str()), "failed to set return value for dataset_id"); + + // Get the fixed model parameters + std::string associated_model_ids_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + ASSERT_SUCCESS(rsp, v.set_string("associated_model_ids", associated_model_ids_string.c_str()), "failed to set return value for associated_model_ids"); + + // Get the schema for user-specified inputs (used only when payload type is json) + // std::string hfmodel_user_inputs_schema_string; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_string), "failed to retrieve hfmodel_user_inputs_schema"); + // ASSERT_SUCCESS(rsp, v.set_string("user_inputs_schema", hfmodel_user_inputs_schema_string.c_str()), "failed to set return value for hfmodel_user_inputs_schema"); + + // Get the model metadata + std::string experiment_id_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(experiment_id_KEY, experiment_id_string), "failed to retrieve experiment_id_string"); + ASSERT_SUCCESS(rsp, v.set_string("experiment_id", experiment_id_string.c_str()), "failed to set return value for experiment_id_string"); + + // // Get the max use count + // uint32_t dataset_max_use_count_value; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + // ASSERT_SUCCESS(rsp, v.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); + + return rsp.value(v, false); +} + +// ----------------------------------------------------------------- +// METHOD: use_dataset +// +// 1. Save the parameters required to generate a use_dataset capability to kvs, increments the current use count, and returns the call +// Capability is calculated and returned once proof of commit of state is presented (via the get_capability method). +// 2. Inputs: +// kvstore_encryption_key +// kvstore_root_block_hash +// kvstore_input_key +// user_inputs +// The first 3 parameters provide flexibility to use large inputs for the model via the kv_store attached to the guardian. +// Only TO may invoke method +// ----------------------------------------------------------------- +bool ww::medperf::token_object::use_dataset( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(USE_DATASET_SCHEMA), "invalid request, missing required parameters"); + + const std::string kvstore_encryption_key(msg.get_string("kvstore_encryption_key")); + const std::string kvstore_root_block_hash(msg.get_string("kvstore_root_block_hash")); + const std::string kvstore_input_key(msg.get_string("kvstore_input_key")); + // const std::string user_inputs(msg.get_string("user_inputs")); + + // check that current count < max count. Increment the current count. + // Note that we use < instead of <= since current count starts at 0. + uint32_t dataset_current_use_count_value; + uint32_t dataset_max_use_count_value; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + ASSERT_SUCCESS(rsp, dataset_current_use_count_value < dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, dataset_current_use_count_value + 1), "failed to update dataset_current_use_count"); + + // store the parameters required to generate a use_dataset capability + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to store dataset_use_capability_kv_store_enc_key"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to store dataset_use_capability_kv_store_hash"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to store dataset_use_capability_kv_store_input_key"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_user_inputs_KEY, user_inputs), "failed to store dataset_use_capability_user_inputs"); + + return rsp.success(true); +} + + + + + + +// ----------------------------------------------------------------- +// METHOD: get_capability +// +// Check proof of commit, calculate/return capability. +// Only TO may invoke method. It is currently possible for the TO to ask for a past capability +// even after token transfer. This is a feature, not a bug. The justification is that +// any new owner is only getting access to "unused uses" of the model. +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::get_capability( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ASSERT_SENDER_IS_OWNER(env, rsp); + ASSERT_INITIALIZED(rsp); + + ASSERT_SUCCESS(rsp, msg.validate_schema(GET_CAPABILITY_SCHEMA), "invalid request, missing required parameters"); + + // Ensure that the current use count is greater than 0, so that an attempt to use the model was made. + // Otherwise, the capability cannot be generated.ls + uint32_t dataset_current_use_count_value; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + ASSERT_SUCCESS(rsp, dataset_current_use_count_value > 0, "invalid request, capability can be obtained only after use_dataset is called"); + + // check for proof of commit of current state of the token object before returning capability + std::string ledger_key; + if (!ww::contract::attestation::get_ledger_key(ledger_key) && ledger_key.length() > 0) + return rsp.error("contract has not been initialized"); + + const std::string ledger_signature(msg.get_string("ledger_signature")); + + ww::types::ByteArray buffer; + std::copy(env.contract_id_.begin(), env.contract_id_.end(), std::back_inserter(buffer)); + std::copy(env.state_hash_.begin(), env.state_hash_.end(), std::back_inserter(buffer)); + + ww::types::ByteArray signature; + if (!ww::crypto::b64_decode(ledger_signature, signature)) + return rsp.error("failed to decode ledger signature"); + if (!ww::crypto::ecdsa::verify_signature(buffer, ledger_key, signature)) + return rsp.error("failed to verify ledger signature"); + + // the current state has been committed so now compute and return the capability + ww::value::Structure params(GENERATE_CAPABILITY_SCHEMA); + + // Get kvstore_encryption_key from dataset_TO_store + std::string kvstore_encryption_key; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to retrieve dataset_use_capability_kv_store_enc_key"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_encryption_key", kvstore_encryption_key.c_str()), "failed to set return value for kvstore_encryption_key"); + + // Get kvstore_root_block_hash from dataset_TO_store + std::string kvstore_root_block_hash; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_root_block_hash_KEY, kvstore_root_block_hash), "failed to retrieve dataset_use_capability_kv_store_hash"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_root_block_hash", kvstore_root_block_hash.c_str()), "failed to set return value for kvstore_root_block_hash"); + + // Get kvstore_input_key from dataset_TO_store + std::string kvstore_input_key; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to retrieve dataset_use_capability_kv_store_input_key"); + ASSERT_SUCCESS(rsp, params.set_string("kvstore_input_key", kvstore_input_key.c_str()), "failed to set return value for kvstore_input_key"); + + // Get payload_type from dataset_TO_store + // std::string payload_type; + // // // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_request_payload_type_KEY, payload_type), "failed to retrieve hfmodel_request_payload_type"); + // ASSERT_SUCCESS(rsp, params.set_string("payload_type", payload_type.c_str()), "failed to set return value for payload_type"); + + // Get user_inputs from dataset_TO_store + // std::string user_inputs; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_user_inputs_KEY, user_inputs), "failed to retrieve dataset_use_capability_user_inputs"); + // ASSERT_SUCCESS(rsp, params.set_string("user_inputs", user_inputs.c_str()), "failed to set return value for user_inputs"); + + // Get dataset_id from dataset_TO_store + std::string dataset_id; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id), "failed to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, params.set_string("dataset_id", dataset_id.c_str()), "failed to set return value for dataset_id"); + + // Get experiment_id from dataset_TO_store + std::string experiment_id; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(experiment_id_KEY, experiment_id), "failed to retrieve experiment_id"); + ASSERT_SUCCESS(rsp, params.set_string("experiment_id", experiment_id.c_str()), "failed to set return value for experiment_id"); + + // Get associated_model_ids from dataset_TO_store + std::string associated_model_ids; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids), "failed to retrieve associated_model_ids"); + ASSERT_SUCCESS(rsp, params.set_string("associated_model_ids", associated_model_ids.c_str()), "failed to set return value for associated_model_ids"); + + // Get user_inputs_schema from dataset_TO_store + // std::string user_inputs_schema; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_user_inputs_schema_KEY, user_inputs_schema), "failed to retrieve user_inputs_schema"); + // ASSERT_SUCCESS(rsp, params.set_string("user_inputs_schema", user_inputs_schema.c_str()), "failed to set return value for user_inputs_schema"); + + // Calculate capability + ww::value::Object result; + ASSERT_SUCCESS(rsp, ww::exchange::token_object::create_operation_package("use_dataset", params, result), + "unexpected error: failed to generate capability"); + + // this assumes that generating the capability does not change state, depending on + return rsp.value(result, false); +} + +// ----------------------------------------------------------------- +// Method: return a hello world message +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::hello_world( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ww::value::String result("Hello, World!"); + return rsp.value(result, false); +} + +// ----------------------------------------------------------------- +// Method: add simple function to the hello_world +// ----------------------------------------------------------------- + +bool ww::medperf::token_object::hello_world_function( + const Message &msg, + const Environment &env, + Response &rsp) +{ + ww::value::Structure result("{'message':'Hello, World!'}"); + return rsp.value(result, false); +} diff --git a/medperf-contract/medperf/token_object.h b/medperf-contract/medperf/token_object.h new file mode 100644 index 0000000..d92f955 --- /dev/null +++ b/medperf-contract/medperf/token_object.h @@ -0,0 +1,89 @@ +/* Copyright 2023 Intel Corporation + * + * Licensed 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. + */ + +#pragma once + +#include + +#include "Util.h" +#include "Secret.h" +#include "exchange/token_object.h" +#include "exchange/issuer_authority_base.h" + + +#define DATASET_TO_INIT_PARAM_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ + SCHEMA_KW(ledger_verifying_key, "") "," \ + SCHEMA_KWS(initialization_package, CONTRACT_SECRET_SCHEMA) "," \ + SCHEMA_KWS(asset_authority_chain, ISSUER_AUTHORITY_CHAIN_SCHEMA)\ + "}" + +#define DATASET_INFO_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + "}" + +#define USE_DATASET_SCHEMA \ + "{" \ + SCHEMA_KW(kvstore_encryption_key, "") "," \ + SCHEMA_KW(kvstore_root_block_hash, "") "," \ + SCHEMA_KW(kvstore_input_key, "") "," \ + "}" + +#define GET_CAPABILITY_SCHEMA \ + "{" \ + SCHEMA_KW(ledger_signature,"") \ + "}" + + +#define GENERATE_CAPABILITY_SCHEMA \ + "{" \ + SCHEMA_KW(kvstore_encryption_key, "") "," \ + SCHEMA_KW(kvstore_root_block_hash, "") "," \ + SCHEMA_KW(kvstore_input_key, "") "," \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + "}" + + + +namespace ww +{ +namespace medperf +{ +namespace token_object +{ + // methods + bool initialize(const Message& msg, const Environment& env, Response& rsp); + + // reserved for testing + bool get_dataset_info(const Message& msg, const Environment& env, Response& rsp); + + bool use_dataset(const Message& msg, const Environment& env, Response& rsp); + bool get_capability(const Message& msg, const Environment& env, Response& rsp); + + // reserved for testing + bool hello_world(const Message& msg, const Environment& env, Response& rsp); + bool hello_world_function(const Message& msg, const Environment& env, Response& rsp); +}; // token_object +}; // medperf +}; // ww diff --git a/medperf-contract/medperf_common.cmake b/medperf-contract/medperf_common.cmake new file mode 100644 index 0000000..7ec4dde --- /dev/null +++ b/medperf-contract/medperf_common.cmake @@ -0,0 +1,39 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +IF(NOT DEFINED EXCHANGE_INCLUDES) + MESSAGE(FATAL_ERROR "EXCHANGE_INCLUDES is not defined") +ENDIF() + +# --------------------------------------------- +# Set up the include list +# --------------------------------------------- +SET (MEDPERF_INCLUDES ${WASM_INCLUDES}) +LIST(APPEND MEDPERF_INCLUDES ${EXCHANGE_INCLUDES}) +LIST(APPEND MEDPERF_INCLUDES ${CMAKE_CURRENT_LIST_DIR}) + +# --------------------------------------------- +# Set up the default source list +# --------------------------------------------- +FILE(GLOB MEDPERF_COMMON_SOURCE ${CMAKE_CURRENT_LIST_DIR}/medperf/common/*.cpp) +FILE(GLOB MEDPERF_CONTRACT_SOURCE ${CMAKE_CURRENT_LIST_DIR}/medperf/contracts/*.cpp) + +SET (MEDPERF_SOURCES) +LIST(APPEND MEDPERF_SOURCES ${MEDPERF_COMMON_SOURCE}) +LIST(APPEND MEDPERF_SOURCES ${MEDPERF_CONTRACT_SOURCE}) + +# --------------------------------------------- +# Build the wawaka contract common library +# --------------------------------------------- +SET(MEDPERF_LIB ww_medperf) diff --git a/medperf-contract/pdo/medperf/__init__.py b/medperf-contract/pdo/medperf/__init__.py new file mode 100644 index 0000000..c0dfac2 --- /dev/null +++ b/medperf-contract/pdo/medperf/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +__all__ = [ 'plugins', 'scripts', 'operations', 'common', 'wsgi', 'resources'] diff --git a/medperf-contract/pdo/medperf/common/__init__.py b/medperf-contract/pdo/medperf/common/__init__.py new file mode 100644 index 0000000..c595226 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +__all__ = [ + 'capability_keys', + 'capability_keystore', + 'endpoint_registry', + 'guardian_service', + 'secrets', + 'utility', + ] diff --git a/medperf-contract/pdo/medperf/common/capability_keys.py b/medperf-contract/pdo/medperf/common/capability_keys.py new file mode 100644 index 0000000..dbc6cee --- /dev/null +++ b/medperf-contract/pdo/medperf/common/capability_keys.py @@ -0,0 +1,118 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +import os + +import pdo.common.crypto as crypto +import pdo.common.keys as keys + +import logging +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class CapabilityKeys(keys.ServiceKeys) : + + # ------------------------------------------------------- + @classmethod + def create_new_keys(cls) : + signing_key = crypto.SIG_PrivateKey() + signing_key.Generate() + decryption_key = crypto.PKENC_PrivateKey() + decryption_key.Generate() + + return cls(signing_key, decryption_key) + + # ------------------------------------------------------- + @classmethod + def deserialize(cls, serialized_signing_key, serialized_decryption_key) : + signing_key = crypto.SIG_PrivateKey(serialized_signing_key) + decryption_key = crypto.PKENC_PrivateKey(serialized_decryption_key) + return cls(signing_key, decryption_key) + + # ------------------------------------------------------- + def __init__(self, signing_key, decryption_key) : + super().__init__(signing_key) + self._decryption_key = decryption_key + self._encryption_key = decryption_key.GetPublicKey() + + # ------------------------------------------------------- + @property + def encryption_key(self) : + return self._encryption_key.Serialize() + + # ------------------------------------------------------- + @property + def decryption_key(self) : + return self._decryption_key.Serialize() + + # ------------------------------------------------------- + def serialize(self) : + return (self.signing_key, self.decryption_key) + + # ------------------------------------------------------- + def encrypt(self, message, encoding = 'raw') : + """ + encrypt a message to send privately to the enclave + + :param message: text to encrypt + :param encoding: encoding for the encrypted cipher text, one of raw, hex, b64 + """ + + if type(message) is bytes : + message_byte_array = message + elif type(message) is tuple : + message_byte_array = message + else : + message_byte_array = bytes(message, 'ascii') + + encrypted_byte_array = self._encryption_key.EncryptMessage(message_byte_array) + if encoding == 'raw' : + encoded_bytes = encrypted_byte_array + elif encoding == 'hex' : + encoded_bytes = crypto.byte_array_to_hex(encrypted_byte_array) + elif encoding == 'b64' : + encoded_bytes = crypto.byte_array_to_base64(encrypted_byte_array) + else : + raise ValueError('unknown encoding; {0}'.format(encoding)) + + return encoded_bytes + + # ------------------------------------------------------- + def decrypt(self, message, encoding = 'raw') : + """ + encrypt a message to send privately to the enclave + + :param message: text to encrypt + :param encoding: encoding for the encrypted cipher text, one of raw, hex, b64 + """ + + if type(message) is bytes : + message_byte_array = message + elif type(message) is tuple : + message_byte_array = message + else : + message_byte_array = bytes(message, 'ascii') + + decrypted_byte_array = self._decryption_key.DecryptMessage(message_byte_array) + if encoding == 'raw' : + encoded_bytes = decrypted_byte_array + elif encoding == 'hex' : + encoded_bytes = crypto.byte_array_to_hex(decrypted_byte_array) + elif encoding == 'b64' : + encoded_bytes = crypto.byte_array_to_base64(decrypted_byte_array) + else : + raise ValueError('unknown encoding; {0}'.format(encoding)) + + return encoded_bytes diff --git a/medperf-contract/pdo/medperf/common/capability_keystore.py b/medperf-contract/pdo/medperf/common/capability_keystore.py new file mode 100644 index 0000000..10cf674 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/capability_keystore.py @@ -0,0 +1,57 @@ +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +import shelve + +from pdo.medperf.common.capability_keys import CapabilityKeys + +import logging +logger = logging.getLogger(__name__) + +class CapabilityKeyStore(object) : + + # ------------------------------------------------------- + def __init__(self, filename = "keystore.db") : + logger.info('create capability store in file %s', filename) + self._keystore = shelve.open(filename, flag='c', writeback=True) + try : + self.mgmt_capability_key = self.get_capability_key('management_capability_key') + except KeyError as ke: + self.mgmt_capability_key = self.create_capability_key('management_capability_key') + + try : + self.svc_capability_key = self.get_capability_key('service_capability_key') + except KeyError as ke: + self.svc_capability_key = self.create_capability_key('service_capability_key') + + # ------------------------------------------------------- + def close(self) : + self._keystore.close() + self._keystore = None + + # ------------------------------------------------------- + def get_capability_key(self, minted_identity) : + (signing_key, decryption_key) = self._keystore[minted_identity] + return CapabilityKeys.deserialize(signing_key, decryption_key) + + # ------------------------------------------------------- + def set_capability_key(self, minted_identity, capability_key) : + (signing_key, decryption_key) = capability_key.serialize() + self._keystore[minted_identity] = (signing_key, decryption_key) + return capability_key + + # ------------------------------------------------------- + def create_capability_key(self, minted_identity) : + capability_key = CapabilityKeys.create_new_keys() + return self.set_capability_key(minted_identity, capability_key) diff --git a/medperf-contract/pdo/medperf/common/endpoint_registry.py b/medperf-contract/pdo/medperf/common/endpoint_registry.py new file mode 100644 index 0000000..297c0c3 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/endpoint_registry.py @@ -0,0 +1,44 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +import shelve + +from pdo.common.keys import EnclaveKeys + +import logging +logger = logging.getLogger(__name__) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class EndpointRegistry(object) : + + # ------------------------------------------------------- + def __init__(self, filename = "endpoint.db") : + logger.info('create endpoint registry in file %s', filename) + self._registry = shelve.open(filename, flag='c', writeback=True) + + # ------------------------------------------------------- + def close(self) : + self._registry.close() + self._registry = None + + # ------------------------------------------------------- + def get_endpoint(self, contract_id) : + (verifying_key, encryption_key) = self._registry[contract_id] + return EnclaveKeys(verifying_key, encryption_key) + + # ------------------------------------------------------- + def set_endpoint(self, contract_id, verifying_key, encryption_key) : + self._registry[contract_id] = (verifying_key, encryption_key) + return EnclaveKeys(verifying_key, encryption_key) diff --git a/medperf-contract/pdo/medperf/common/guardian_service.py b/medperf-contract/pdo/medperf/common/guardian_service.py new file mode 100644 index 0000000..d3361b0 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/guardian_service.py @@ -0,0 +1,136 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +Client for the guardian service frontend +""" + +import json +import requests +import time +from urllib.parse import urljoin + +from pdo.service_client.generic import MessageException +from pdo.service_client.generic import GenericServiceClient +from pdo.service_client.storage import StorageServiceClient +import pdo.common.keys as keys + +import logging +logger = logging.getLogger(__name__) + +## ----------------------------------------------------------------- +## CLASS: GuardianServiceClient +## ----------------------------------------------------------------- +class GuardianServiceClient(GenericServiceClient) : + + default_timeout = 20.0 + + # ----------------------------------------------------------------- + def __init__(self, url) : + super().__init__(url) + self.session = requests.Session() + self.session.headers.update({'x-session-identifier' : self.Identifier}) + self.request_identifier = 0 + + service_info = self.get_guardian_metadata() + self.enclave_keys = keys.EnclaveKeys(service_info['verifying_key'], service_info['encryption_key']) + + self.storage_service_url = service_info['storage_service_url'] + self.storage_service_client = StorageServiceClient(self.storage_service_url) + # ensure the local storage service used by the guardian service is running before starting the + # guardian service. + self._attach_storage_service_(self.storage_service_client) + + # ----------------------------------------------------------------- + @property + def verifying_key(self) : + return self.enclave_keys.verifying_key + + # ----------------------------------------------------------------- + @property + def encryption_key(self) : + return self.enclave_keys.encryption_key + + # ------------------------------------------------------- + def _attach_storage_service_(self, storage_service) : + self.storage_service_verifying_key = storage_service.verifying_key + + self.get_block = storage_service.get_block + self.get_blocks = storage_service.get_blocks + self.store_block = storage_service.store_block + self.store_blocks = storage_service.store_blocks + self.check_block = storage_service.check_block + self.check_blocks = storage_service.check_blocks + + # ----------------------------------------------------------------- + def __post_request__(self, path, request) : + + try : + url = urljoin(self.ServiceURL, path) + while True : + response = self.session.post(url, json=request, timeout=self.default_timeout, stream=False) + if response.status_code == 429 : + logger.info('prepare to resubmit the request') + sleeptime = min(1.0, float(response.headers.get('retry-after', 1.0))) + time.sleep(sleeptime) + continue + + response.raise_for_status() + return response.json() + + except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : + logger.warn('network error connecting to service (%s); %s', path, str(e)) + raise MessageException(str(e)) from e + + # ----------------------------------------------------------------- + def __get_request__(self, path) : + + try : + url = urljoin(self.ServiceURL, path) + while True : + response = self.session.get(url, timeout=self.default_timeout) + if response.status_code == 429 : + logger.info('prepare to resubmit the request') + sleeptime = min(1.0, float(response.headers.get('retry-after', 1.0))) + time.sleep(sleeptime) + continue + + response.raise_for_status() + return response.json() + + except (requests.HTTPError, requests.ConnectionError, requests.Timeout) as e : + logger.warn('network error connecting to service (%s); %s', path, str(e)) + raise MessageException(str(e)) from e + + # ----------------------------------------------------------------- + def get_guardian_metadata(self) : + return self.__get_request__('info') + + # ----------------------------------------------------------------- + def add_endpoint(self, **params) : + return self.__post_request__('add_endpoint', params) + + # ----------------------------------------------------------------- + def provision_token_issuer(self, **params) : + return self.__post_request__('provision_token_issuer', params) + + # ----------------------------------------------------------------- + def provision_token_object(self, **params) : + return self.__post_request__('provision_token_object', params) + + # ----------------------------------------------------------------- + def process_capability(self, **params) : + return self.__post_request__('process_capability', params) diff --git a/medperf-contract/pdo/medperf/common/secrets.py b/medperf-contract/pdo/medperf/common/secrets.py new file mode 100644 index 0000000..4117ca3 --- /dev/null +++ b/medperf-contract/pdo/medperf/common/secrets.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +import json + +from pdo.medperf.common.utility import ValidateJSON +import pdo.common.crypto as crypto + +import logging +logger = logging.getLogger(__name__) + +__all__ = [ 'recv_secret', 'send_secret' ] + +__secret_schema__ = { + "type" : "object", + "properties" : { + "encrypted_session_key" : { "type" : "string" }, + "session_key_iv" : { "type" : "string" }, + "encrypted_message" : { "type" : "string" }, + }, +} + + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def recv_secret(capability_key, secret) : + """Process an incoming secret + + :param capability_key pdo.medperf.common.capability_keys.CapabilityKeys: decryption key + :param secret str: the secret to be unpacked + :returns dict: the parsed json message in the secret + """ + + if not ValidateJSON(secret, __secret_schema__) : + return None # throw exception? + + encrypted_session_key = crypto.base64_to_byte_array(secret['encrypted_session_key']) + session_key = capability_key.decrypt(encrypted_session_key, encoding='raw') + session_iv = crypto.base64_to_byte_array(secret['session_key_iv']) + cipher = crypto.base64_to_byte_array(secret['encrypted_message']) + raw_message = crypto.SKENC_DecryptMessage(session_key, session_iv, cipher) + message = crypto.byte_array_to_string(raw_message) + + return json.loads(message) + + +# ----------------------------------------------------------------- +# send_secret +# ----------------------------------------------------------------- +def send_secret(capability_key, message) : + """Create a secret for transmission + + :param capability_key pdo.medperf.common.capability_keys.CapabilityKeys: decryption key + :param message dict: dictionary that will be encrypted as JSON in the secret + :returns dict: the secret + """ + + session_key = crypto.SKENC_GenerateKey() + session_iv = crypto.SKENC_GenerateIV() + serialized_message = crypto.string_to_byte_array(json.dumps(message)) + cipher = crypto.SKENC_EncryptMessage(session_key, session_iv, serialized_message) + encrypted_session_key = capability_key.encrypt(session_key) + + result = dict() + result['encrypted_session_key'] = crypto.byte_array_to_base64(encrypted_session_key) + result['session_key_iv'] = crypto.byte_array_to_base64(session_iv) + result['encrypted_message'] = crypto.byte_array_to_base64(cipher) + + return result diff --git a/medperf-contract/pdo/medperf/common/utility.py b/medperf-contract/pdo/medperf/common/utility.py new file mode 100644 index 0000000..f0d53cf --- /dev/null +++ b/medperf-contract/pdo/medperf/common/utility.py @@ -0,0 +1,23 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +import jsonschema + +# ----------------------------------------------------------------- +def ValidateJSON(instance, schema): + try: + jsonschema.validate(instance=instance, schema=schema) + except jsonschema.exceptions.ValidationError as err: + return False + return True diff --git a/medperf-contract/pdo/medperf/operations/__init__.py b/medperf-contract/pdo/medperf/operations/__init__.py new file mode 100644 index 0000000..13e2956 --- /dev/null +++ b/medperf-contract/pdo/medperf/operations/__init__.py @@ -0,0 +1,22 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + + +__all__ = [ 'use_dataset'] + +from pdo.medperf.operations.use_dataset import DataOperation + +capability_handler_map = { + 'use_dataset' : DataOperation, +} diff --git a/medperf-contract/pdo/medperf/operations/use_dataset.py b/medperf-contract/pdo/medperf/operations/use_dataset.py new file mode 100644 index 0000000..d0f9a3e --- /dev/null +++ b/medperf-contract/pdo/medperf/operations/use_dataset.py @@ -0,0 +1,141 @@ +# Copyright 2023 Intel Corporation +# +# Licensed 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. + + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from pdo.medperf.common.utility import ValidateJSON +from pdo.common.key_value import KeyValueStore + +import logging +import requests +import urllib.request +import json +logger = logging.getLogger(__name__) + + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class DataOperation(object) : + # ----------------------------------------------------------------- + __schema__ = { + "type" : "object", + "properties" : { + "kvstore_encryption_key" : { "type" : "string" }, + "kvstore_root_block_hash" : { "type" : "string" }, + "kvstore_input_key" : { "type" : "string" }, + "dataset_id" : { "type" : "string" }, + "experiment_id" : { "type" : "string" }, + # "payload_type" : { "type" : "string" }, + # "user_inputs" : { "type" : "string" }, + # "user_inputs_schema" : { "type" : "string" }, + "associated_model_ids" : { "type" : "string" }, + } + } + + + # ----------------------------------------------------------------- + def __init__(self, config) : + pass + + # @staticmethod + # def inference_dataset(experiment_id, dataset_id, payload, payload_type='json'): + # """ + # Utility function to query the Hugging Face model and get response. + # If the guardian is deployed in a network with a proxy, the proxy settings are automatically picked up + # as long as the proxy is set in the environment variables. + + # payload_type is either json or binary. If binary, the payload is expected to be a byte string. + # If json, the payload is expected to be a dictionary. binary payloads are used for image/audio inputs, + # while json payloads are used for text inputs (e.g. used while working with language models). + + # For example usages, refer to https://huggingface.co/docs/api-inference/en/detailed_parameters + + # """ + # headers = {"Authorization": f"Bearer {dataset_id}"} + # try: + # if payload_type == 'json': + # response = requests.post(experiment_id, headers=headers, json=payload, proxies=urllib.request.getproxies()) + # elif payload_type == 'binary': + # response = requests.post(experiment_id, headers=headers, data=payload, proxies=urllib.request.getproxies()) + # else: + # logger.error("Invalid payload type. Supported types are 'json' and 'binary'") + # return None + # except Exception as e: + # logger.error(f"Request failed: {e}") + # return None + + # if payload_type == 'binary': + # return json.loads(response.content.decode("utf-8")) + + # return response.json() + + + # ----------------------------------------------------------------- + def __call__(self, params) : + if not ValidateJSON(params, self.__schema__) : + return None + + # get the parameters + kvstore_encryption_key = params['kvstore_encryption_key'] + kvstore_root_block_hash = params['kvstore_root_block_hash'] + kvstore_input_key = params['kvstore_input_key'] + dataset_id = params['dataset_id'] + experiment_id = params['experiment_id'] + # payload_type = params['payload_type'] + # user_inputs = json.loads(params['user_inputs']) + # user_inputs_schema = json.loads(params['user_inputs_schema']) + associated_model_ids = params['associated_model_ids'] + + + # # If payload type is binary, get the input data from the key-value store. + # if payload_type == 'binary': + # kv = KeyValueStore(kvstore_encryption_key, kvstore_root_block_hash) + # with kv: + # input_data = kv.get(kvstore_input_key, output_encoding='raw') + # payload = bytes(input_data) + # elif payload_type == 'json': + # # check schema of user inputs, and generate payload by merging user inputs with fixed model parameters + # if not ValidateJSON(user_inputs, user_inputs_schema) : + # logger.error("Invalid user inputs") + # return None + # payload = {**associated_model_ids, **user_inputs} + # else: + # logger.error("Invalid payload type. Supported types are 'json' and 'binary'") + # return None + + # query the Hugging Face model and return the response + # return self.query_hf_model(experiment_id, dataset_id, payload, payload_type) + + test_message = "Hello from the guardian service. Assume the experiment has been run successfully." + # put the parameters in test_message and return + # for testing purpose + test_message += f"\nDataset ID: {dataset_id}" + test_message += f"\nExperiment ID: {experiment_id}" + test_message += f"\nAssociated Model IDs: {associated_model_ids}" + return test_message + + # to do: (Support for post processing) + # Add support for optionally encrypting response before sending back to the client + # encryption key specified by token object + # token object can implement policies for endorsing encryption keys + # for example: results encrypted for use within another PDO contract or even the token object itself + # Such a feature can be used for privacy-preserving post processing of the model outputs + # before sharing final results with the client + # If there are generic post processing steps that can be applied to a large class of models, + # the code can be added here itself. + \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/plugins/__init__.py b/medperf-contract/pdo/medperf/plugins/__init__.py new file mode 100644 index 0000000..b50ddae --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +__all__ = [ 'medperf_guardian', 'medperf_token_object'] diff --git a/medperf-contract/pdo/medperf/plugins/medperf_guardian.py b/medperf-contract/pdo/medperf/plugins/medperf_guardian.py new file mode 100644 index 0000000..4759f6e --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/medperf_guardian.py @@ -0,0 +1,286 @@ +#!/usr/bin/env python + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +""" +medperf guardian plugins +""" + +import json +import logging +logger = logging.getLogger(__name__) + +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +from pdo.client.builder import invocation_parameter + +from pdo.medperf.common.guardian_service import GuardianServiceClient + +__all__ = [ + 'op_provision_token_issuer', + 'op_provision_token_object', + 'op_process_capability', + 'op_add_endpoint', + 'cmd_provision_token_issuer', + 'cmd_provision_token_object', + 'do_medperf_guardian', + 'do_medperf_guardian_service', + 'load_commands', +] + +## ----------------------------------------------------------------- +## add_endpoint +## ----------------------------------------------------------------- +class op_add_endpoint(pcontract.contract_op_base) : + + name = "add_endpoint" + help = "add an attested contract object endpoint to the contract" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--code-metadata', + help='contract code metadata', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-m', '--contract-metadata', + help='contract metadata', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, contract_id, ledger_attestation, contract_metadata, code_metadata, url, **kwargs) : + + params = dict() + params['contract_id'] = contract_id + params['ledger_attestation'] = ledger_attestation + params['contract_metadata'] = contract_metadata + params['contract_code_metadata'] = code_metadata + + service_client = GuardianServiceClient(url) + result = service_client.add_endpoint(**params) + + return result + +## ----------------------------------------------------------------- +## provision_token_issuer +## ----------------------------------------------------------------- +class op_provision_token_issuer(pcontract.contract_op_base) : + + name = "provision_token_issuer" + help = "provision the token issuer with the capability management key" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, contract_id, url, **kwargs) : + params = dict() + params['contract_id'] = contract_id + + service_client = GuardianServiceClient(url) + raw_result = service_client.provision_token_issuer(**params) + result = json.dumps(raw_result) + return result + +## ----------------------------------------------------------------- +## provision_token_object +## ----------------------------------------------------------------- +class op_provision_token_object(pcontract.contract_op_base) : + + name = "provision_token_object" + help = "provision a token object with an identity and capability generation key" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--provisioning-package', + help='contract secret containing the provisioning package', + type=invocation_parameter, + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, provisioning_package, url, **kwargs) : + params = provisioning_package + + service_client = GuardianServiceClient(url) + raw_result = service_client.provision_token_object(**params) + result = json.dumps(raw_result) + return result + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_process_capability(pcontract.contract_op_base) : + + name = "process_capability" + help = "" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--capability', + help='capability generated by the token object create operation interface', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, session_params, capability, url, **kwargs) : + params = capability + + service_client = GuardianServiceClient(url) + raw_result = service_client.process_capability(**params) + result = json.dumps(raw_result) + return result + +# ----------------------------------------------------------------- +# provision a token issuer +# ----------------------------------------------------------------- +class cmd_provision_token_issuer(pcommand.contract_command_base) : + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-c', '--code-metadata', + help='contract code metadata', + required=True) + subparser.add_argument( + '-i', '--contract-id', + help='contract identifier', + type=str, required=True) + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger', + required=True) + subparser.add_argument( + '-m', '--contract-metadata', + help='contract metadata', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str) + + @classmethod + def invoke(cls, state, context, contract_id, ledger_attestation, contract_metadata, code_metadata, url=None, **kwargs) : + + if url is None : + url = context['url'] + + session = None + pcontract.invoke_contract_op( + op_add_endpoint, + state, context, session, + contract_id, + ledger_attestation, + contract_metadata, + code_metadata, + url) + + provisioning_package = pcontract.invoke_contract_op( + op_provision_token_issuer, + state, context, session, + contract_id, + url) + + cls.display('provisioned guardian for token issuer {}'.format(url)) + return json.loads(provisioning_package) + +# ----------------------------------------------------------------- +# provision a token object +# ----------------------------------------------------------------- +class cmd_provision_token_object(pcommand.contract_command_base) : + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-p', '--provisioning-package', + help='contract secret containing the provisioning package', + required=True) + subparser.add_argument( + '-u', '--url', + help='URL for the medperf guardian service', + type=str, required=True) + + @classmethod + def invoke(cls, state, context, provisioning_package, url=None, **kwargs) : + + if url is None : + url = context['url'] + + session = None + to_package = pcontract.invoke_contract_op( + op_provision_token_object, + state, context, session, + provisioning_package, + url, + **kwargs) + + cls.display('provisioned token object for guardian {}'.format(url)) + return json.loads(to_package) + + +## ----------------------------------------------------------------- +## Create the generic, shell independent version of the aggregate command +## ----------------------------------------------------------------- +__operations__ = [ + op_provision_token_issuer, + op_provision_token_object, + op_process_capability, + op_add_endpoint, +] + +do_medperf_guardian_service = pcontract.create_shell_command('medperf_guardian_service', __operations__) + +__commands__ = [ + cmd_provision_token_issuer, + cmd_provision_token_object, +] + +do_medperf_guardian = pcommand.create_shell_command('medperf_guardian', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'medperf_guardian', do_medperf_guardian) + pshell.bind_shell_command(cmdclass, 'medperf_guardian_service', do_medperf_guardian_service) \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py new file mode 100644 index 0000000..d0bdddb --- /dev/null +++ b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py @@ -0,0 +1,511 @@ +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +import json +import logging +import time + +from pdo.submitter.create import create_submitter +from pdo.contract import invocation_request +from pdo.common.key_value import KeyValueStore +import pdo.common.utility as putils +import pdo.common.crypto as crypto + +import pdo.client.builder as pbuilder +import pdo.client.builder.command as pcommand +import pdo.client.builder.contract as pcontract +import pdo.client.builder.shell as pshell +import pdo.client.commands.contract as pcontract_cmd + +import pdo.exchange.plugins.token_object as token_object +from pdo.common.keys import ServiceKeys + + +from pdo.medperf.common.guardian_service import GuardianServiceClient + +__all__ = [ + 'op_initialize', + 'op_get_verifying_key', + 'op_get_contract_metadata', + 'op_get_contract_code_metadata', + 'op_get_asset_type_identifier', + 'op_get_issuer_authority', + 'op_get_authority', + 'op_transfer', + 'op_escrow', + 'op_release', + 'op_claim', + 'op_get_dataset_info', + 'op_use_dataset', + 'op_get_capability', + 'op_hello_world', + 'op_hello_world_function', + 'cmd_mint_dataset_tokens', + 'cmd_mint_tokens', + 'cmd_transfer_assets', + 'cmd_use_dataset', + 'cmd_get_dataset_info', + 'cmd_hello_world', + 'cmd_hello_world_function', + 'do_medperf_token', + 'do_medperf_token_contract', + 'load_commands', +] + +## ----------------------------------------------------------------- +## inherited operations +## ----------------------------------------------------------------- +op_get_verifying_key = token_object.op_get_verifying_key +op_get_contract_metadata = token_object.op_get_contract_metadata +op_get_contract_code_metadata = token_object.op_get_contract_code_metadata +op_get_asset_type_identifier = token_object.op_get_asset_type_identifier +op_get_issuer_authority = token_object.op_get_issuer_authority +op_get_authority = token_object.op_get_authority +op_transfer = token_object.op_transfer +op_escrow = token_object.op_escrow +op_release = token_object.op_release +op_claim = token_object.op_claim +cmd_mint_tokens = token_object.cmd_mint_tokens +cmd_transfer_assets = token_object.cmd_transfer_assets + +logger = logging.getLogger(__name__) + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_initialize(pcontract.contract_op_base) : + + name = "initialize" + help = "initialize the token object with the package received from the data guardian" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-a', '--authority', + help='serialized authority from the vetting organization', + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument( + '-i', '--initialization-package', + help="the token issuer initialization package from the guardian", + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument( + '-l', '--ledger-key', + help='ledger verifying key', + type=pbuilder.invocation_parameter, required=True) + subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) + subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) + subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) + # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) + # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) + # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) + subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) + + + @classmethod + def invoke(cls, state, session_params, ledger_key, initialization_package, authority, **kwargs) : + session_params['commit'] = True + params = {} + params['ledger_verifying_key'] = ledger_key + params['initialization_package'] = initialization_package + params['asset_authority_chain'] = authority + + # Add params from kwargs + params['dataset_id'] = kwargs.get('dataset_id') + params['experiment_id'] = kwargs.get('experiment_id') + params['associated_model_ids'] = kwargs.get('associated_model_ids') + # params['user_inputs_schema'] = kwargs.get('user_inputs_schema') + # params['payload_type'] = kwargs.get('payload_type', 'json') + # params['medperf_usage_info'] = kwargs.get('medperf_usage_info') + params['max_use_count'] = kwargs.get('max_use_count', 1) + + + # parse the message (python dict as the defined schema) and call + message = invocation_request('initialize', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_get_dataset_info(pcontract.contract_op_base) : + """op_get_dataset_info implements the method to get the info about the dataset stored in the token object + """ + + name = "op_get_dataset_info" + help = "get info about the dataset stored in the token object" + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + message = invocation_request('get_dataset_info', **params) + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_use_dataset(pcontract.contract_op_base) : + """op_use_dataset implements step 1 for the execution of MedPerf experiment via the guardian service + The specific operation depends on the model. This method is used to fix all the parameters required to generate + the capability that will be sent to the guardian service. the capability itself is not returned by this method + the capability is returned only after proof of commit is received from the ledger after this method. + Step 2 obtains the capability from the token object and sends it to the guardian service. + """ + + name = "op_use_dataset" + help = "implement step 1 of execution of MedPerf experiment" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--kvstore_encryption_key', + help='Encryption key for the KV store if payload is binary', + type=str) + + subparser.add_argument( + '--kvstore_input_key', + help='Data file input key for the KV store if payload is binary', + type=str) + + subparser.add_argument( + '--kvstore_root_block_hash', + help='KV store hashed identity if payload is binary', + type=str) + + # subparser.add_argument( + # '--user_inputs', + # help='User inputs for the model, used when the payload is JSON', + # type=str) + + @classmethod + def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key, kvstore_root_block_hash, **kwargs) : + session_params['commit'] = True + + # send the request to the contract to create a capability for the guardian + params = {} + params['kvstore_encryption_key'] = kvstore_encryption_key + params['kvstore_input_key'] = kvstore_input_key + params['kvstore_root_block_hash'] = kvstore_root_block_hash + # params['user_inputs'] = user_inputs + + message = invocation_request('use_dataset', **params) + try: + result = pcontract_cmd.send_to_contract(state, message, **session_params) + except Exception as e: + raise + + cls.log_invocation(message, result) + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_get_capability(pcontract.contract_op_base) : + """op_get_capability implements step 2 for the execution of MedPerf experiment via the guardian service + This method carries proof of commit from ledger as input, and obtains the capability from the token object. + Step 1 fixes all the parameters required to generate the capability. + """ + + name = "op_get_capability" + help = "implement step 2 of execution of MedPerf experiment" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '-l', '--ledger-attestation', + help='attestation from the ledger that the current state of the token issuer is committed', + type=pbuilder.invocation_parameter, required=True) + + @classmethod + def invoke(cls, state, session_params, ledger_attestation, **kwargs) : + session_params['commit'] = False + + params = {} + params['ledger_signature'] = ledger_attestation + + message = invocation_request('get_capability', **params) + capability = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, capability) + + return capability + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +class op_hello_world(pcontract.contract_op_base) : + """op_hello_world implements a simple hello world operation + """ + + name = "hello_world" + help = "simple hello world operation" + + # # For this operation, we don't need any arguments + # @classmethod + # def add_arguments(cls, subparser) : + # pass + + # This operation doesn't require any arguments + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + + message = invocation_request('hello_world', **params) + + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_hello_world(pcommand.contract_command_base) : + """cmd_hello_world implements a simple hello world command + """ + + name = "hello_world" + help = "simple hello world command" + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + # invoke the hello_world operation + + result = pcontract.invoke_contract_op( + op_hello_world, + state, context, session, + **kwargs) + + cls.display(result) + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class op_hello_world_function(pcontract.contract_op_base) : + """op_hello_world_function implements a simple hello world operation + """ + + name = "hello_world_function" + help = "simple hello world operation" + + @classmethod + def add_arguments(cls, subparser) : + pass + + @classmethod + def invoke(cls, state, session_params, **kwargs) : + session_params['commit'] = False + + params = {} + + message = invocation_request('hello_world_function', **params) + + result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) + + return result + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_use_dataset(pcommand.contract_command_base) : + """cmd_use_dataset implements implements the usage of the dataset via the guardian service + """ + name = "use_dataset" + help = "run experiment on the dataset" + + # @classmethod + # def add_arguments(cls, subparser) : + # # subparser.add_argument( + # # '--data_file', + # # help='Filename of the data_file to use as inference input. this is used only if payload is binary', + # # type=str) + + # # subparser.add_argument( + # # '--search-path', + # # help='Directories to search for the data file', + # # nargs='+', type=str, default=['.', './data']) + + # subparser.add_argument( + # '--user_inputs', + # help='User inputs for the model, used when the payload is JSON', + # type=str) + + @classmethod + def invoke(cls, state, context, **kwargs) : + + # ensure token has been created + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + # query the token object for model info, get the payload type, and ensure that for binary payloads + # data file is provided, and for json payloads, user inputs are provided + session = pbuilder.SessionParameters(save_file=save_file) + model_info_json = pcontract.invoke_contract_op( + op_get_dataset_info, + state, context, session, + **kwargs) + + + kvstore_encryption_key = "not_used" + kvstore_input_key = "not_used" + kvstore_root_block_hash = "not_used" + + # invoke op_use_dataset to store the parameters required to generate the capability + session = pbuilder.SessionParameters(save_file=save_file) + try: + _ = pcontract.invoke_contract_op( + op_use_dataset, + state, context, session, + kvstore_encryption_key, + kvstore_input_key, + kvstore_root_block_hash, + **kwargs) + except Exception as e: + cls.display_error("op_use_dataset method evaluation failed. {}".format(e)) + return None + + # get proof of commit from the ledger + time.sleep(2) # wait for the ledger to commit the transaction, not sure if any wait is needed or the + # correct solution is to poll the ledger until the transaction is committed + + to_contract = pcontract_cmd.get_contract(state, save_file) + ledger_submitter = create_submitter(state.get(['Ledger'])) + state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) + + # get capability from the token object + capability = pcontract.invoke_contract_op ( + op_get_capability, + state, context, session, + state_attestation['signature'], + **kwargs) + + # push data file to storage service associated with the guardian if payload is binary + guardian_context = context.get_context('data_guardian_context') + url = guardian_context['url'] + service_client = GuardianServiceClient(url) + # send capability to guardian service + capability = json.loads(capability) + result = service_client.process_capability(**capability) + cls.display(result) + return result + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_mint_dataset_tokens(pcommand.contract_command_base) : + """Mint token objects + Wrapper around the mint_tokens operation from exchange.plugins.token_object + to be able to specify additional arguments during initialization + """ + + name = "mint_dataset_tokens" + help = "mint tokens for a token issuer" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) + subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) + subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) + # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) + # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) + # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) + subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) + + @classmethod + def invoke(cls, state, context, **kwargs) : + return pcommand.invoke_contract_cmd(cmd_mint_tokens, state, context, **kwargs) + + + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +class cmd_get_dataset_info(pcommand.contract_command_base) : + """cmd_get_dataset_info gets the model info for which token has been created + the model info is entirely obtained from the token object, and does not involve calls to the + guardian + """ + name = "get_dataset_info" + help = "get dataset info stored in the token object" + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + result = pcontract.invoke_contract_op( + op_get_dataset_info, + state, context, session, + **kwargs) + + cls.display(result) + + return result + +## ----------------------------------------------------------------- +## Create the generic, shell independent version of the aggregate command +## ----------------------------------------------------------------- +__operations__ = [ + op_initialize, + op_get_verifying_key, + op_get_contract_metadata, + op_get_contract_code_metadata, + op_get_asset_type_identifier, + op_get_issuer_authority, + op_get_authority, + op_transfer, + op_escrow, + op_release, + op_claim, + op_use_dataset, + op_get_dataset_info, + op_get_capability, + op_hello_world, + op_hello_world_function, +] + +do_medperf_token_contract = pcontract.create_shell_command('medperf_token_contract', __operations__) + +__commands__ = [ + cmd_mint_dataset_tokens, + cmd_transfer_assets, + cmd_use_dataset, + cmd_get_dataset_info, + cmd_hello_world, + # cmd_hello_world_function, +] + +do_medperf_token = pcommand.create_shell_command('medperf_token', __commands__) + +## ----------------------------------------------------------------- +## Enable binding of the shell independent version to a pdo-shell command +## ----------------------------------------------------------------- +def load_commands(cmdclass) : + pshell.bind_shell_command(cmdclass, 'medperf_token', do_medperf_token) + pshell.bind_shell_command(cmdclass, 'medperf_token_contract', do_medperf_token_contract) + \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/resources/__init__.py b/medperf-contract/pdo/medperf/resources/__init__.py new file mode 100644 index 0000000..d0b2a5a --- /dev/null +++ b/medperf-contract/pdo/medperf/resources/__init__.py @@ -0,0 +1,15 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +__all__ = ['resources'] diff --git a/medperf-contract/pdo/medperf/resources/resources.py b/medperf-contract/pdo/medperf/resources/resources.py new file mode 100644 index 0000000..06483c1 --- /dev/null +++ b/medperf-contract/pdo/medperf/resources/resources.py @@ -0,0 +1,18 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +import pdo.client.builder.installer as pinstaller + +def install_medperf_contract_resources() : + pinstaller.install_plugin_resources('pdo.medperf.resources', 'medperf') diff --git a/medperf-contract/pdo/medperf/scripts/__init__.py b/medperf-contract/pdo/medperf/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/medperf-contract/pdo/medperf/scripts/guardianCLI.py b/medperf-contract/pdo/medperf/scripts/guardianCLI.py new file mode 100644 index 0000000..51cbe47 --- /dev/null +++ b/medperf-contract/pdo/medperf/scripts/guardianCLI.py @@ -0,0 +1,313 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +Data guardian service. +""" + +import os +import sys +import argparse + +import signal + +import pdo.common.config as pconfig +import pdo.common.logger as plogger +import pdo.common.utility as putils + +from pdo.common.wsgi import AppWrapperMiddleware +from pdo.medperf.wsgi import wsgi_operation_map +from pdo.medperf.common.capability_keystore import CapabilityKeyStore +from pdo.medperf.common.endpoint_registry import EndpointRegistry + +import logging +logger = logging.getLogger(__name__) + + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +from twisted.web import http +from twisted.web.resource import Resource, NoResource +from twisted.web.server import Site +from twisted.python.threadpool import ThreadPool +from twisted.internet import reactor, defer +from twisted.internet.endpoints import TCP4ServerEndpoint +from twisted.web.wsgi import WSGIResource + +## ---------------------------------------------------------------- +def ErrorResponse(request, error_code, msg) : + """Generate a common error response for broken requests + """ + + result = "" + if request.method != 'HEAD' : + result = msg + '\n' + result = result.encode('utf8') + + request.setResponseCode(error_code) + request.setHeader(b'Content-Type', b'text/plain') + request.setHeader(b'Content-Length', len(result)) + request.write(result) + + try : + request.finish() + except : + logger.exception("exception during request finish") + raise + + return request + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def __shutdown__(*args) : + logger.warn('shutdown request received') + reactor.callLater(1, reactor.stop) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def TestService(config) : + """Test for the existance of a guardian service with the current configuration + """ + + from pdo.service_client.generic import MessageException + from pdo.medperf.common.guardian_service import GuardianServiceClient + + try : + http_port = config['GuardianService']['HttpPort'] + http_host = config['GuardianService']['Host'] + service_url = 'http://{}:{}'.format(http_host, http_port) + except KeyError as ke : + logger.error('missing configuration for %s', str(ke)) + sys.exit(-1) + + try : + service_client = GuardianServiceClient(service_url) + except MessageException as m : + # if the error is a message exception then the message stays as info + # since the point of this routine is to test and this means the test + # failed + logger.info('failed to contact guardian service; {}'.format(str(m))) + sys.exit(-1) + except Exception as e : + # if the exception is something more serious, then show the error + # message + logger.error('failed to contact guardian service; {}'.format(str(e))) + sys.exit(-1) + + logger.info('guardian service running; {}'.format(service_url)) + sys.exit(0) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def StartService(config, capability_keystore, endpoint_registry) : + try : + http_port = config['GuardianService']['HttpPort'] + http_host = config['GuardianService']['Host'] + worker_threads = config['GuardianService'].get('WorkerThreads', 8) + reactor_threads = config['GuardianService'].get('ReactorThreads', 8) + except KeyError as ke : + logger.error('missing configuration for %s', str(ke)) + sys.exit(-1) + + logger.info('service started on %s:%s', http_host, http_port) + + thread_pool = ThreadPool(minthreads=1, maxthreads=worker_threads) + thread_pool.start() + reactor.addSystemEventTrigger('before', 'shutdown', thread_pool.stop) + + root = Resource() + for (wsgi_verb, wsgi_app) in wsgi_operation_map.items() : + logger.info('add handler for %s', wsgi_verb) + verb = wsgi_verb.encode('utf8') + app = AppWrapperMiddleware(wsgi_app(config, capability_keystore, endpoint_registry)) + root.putChild(verb, WSGIResource(reactor, thread_pool, app)) + + site = Site(root, timeout=60) + site.displayTracebacks = True + + reactor.suggestThreadPoolSize(reactor_threads) + + signal.signal(signal.SIGQUIT, __shutdown__) + signal.signal(signal.SIGTERM, __shutdown__) + + endpoint = TCP4ServerEndpoint(reactor, http_port, backlog=32, interface=http_host) + endpoint.listen(site) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def RunService(capability_keystore, endpoint_registry) : + @defer.inlineCallbacks + def shutdown_twisted(): + logger.info("Stopping Twisted") + yield reactor.callFromThread(reactor.stop) + + reactor.addSystemEventTrigger('before', 'shutdown', shutdown_twisted) + + try : + reactor.run() + except ReactorNotRunning: + logger.warn('shutdown') + except : + logger.warn('shutdown') + + capability_keystore.close() + endpoint_registry.close() + sys.exit(0) + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def LocalMain(config) : + + # load and initialize the model and service keys + try : + logger.debug('initialize the service') + + try : + keystore_filename = config['Data']['CapabilityKeyStore'] + except KeyError as ke : + logger.error('missing required configuration; %s', str(ke)) + sys.exit(-1) + + keystore_filename = putils.build_file_name(keystore_filename, extension='db') + capability_keystore = CapabilityKeyStore(keystore_filename) + + try : + endpoint_filename = config['Data']['EndpointRegistry'] + except KeyError as ke : + logger.error('missing required configuration; %s', str(ke)) + sys.exit(-1) + + endpoint_filename = putils.build_file_name(endpoint_filename, extension='db') + endpoint_registry = EndpointRegistry(endpoint_filename) + + except Exception as e : + logger.exception('failed to initialize service keys; %s', e) + sys.exit(-1) + + # set up the handlers for the enclave service + try : + StartService(config, capability_keystore, endpoint_registry) + except Exception as e: + logger.exception('failed to start the enclave service; %s', e) + sys.exit(-1) + + # and run the service + RunService(capability_keystore, endpoint_registry) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def Main() : + config_map = pconfig.build_configuration_map() + + # parse out the configuration file first + conffiles = [ 'guardian_service.toml' ] + confpaths = [ ".", "./etc", config_map['etc'] ] + + parser = argparse.ArgumentParser() + + # allow for override of bindings in the config map + parser.add_argument('-b', '--bind', help='Define variables for configuration and script use', nargs=2, action='append') + + parser.add_argument('--config', help='configuration file', nargs = '+') + parser.add_argument('--config-dir', help='directory to search for configuration files', nargs = '+') + + parser.add_argument('--identity', help='Identity to use for the process', required = True, type = str) + + parser.add_argument('--key-dir', help='Directories to search for key files', nargs='+') + parser.add_argument('--data-dir', help='Path for storing generated files', type=str) + + parser.add_argument('--logfile', help='Name of the log file, __screen__ for standard output', type=str) + parser.add_argument('--loglevel', help='Logging level', type=str) + + parser.add_argument('--http', help='Port on which to run the http server', type=int) + parser.add_argument('--block-store', help='Name of the file where blocks are stored', type=str) + + parser.add_argument('--test', help='Test for guardian service', action='store_true') + + options = parser.parse_args() + + # first process the options necessary to load the default configuration + if options.config : + conffiles = options.config + + if options.config_dir : + confpaths = options.config_dir + + config_map['identity'] = options.identity + if options.data_dir : + config_map['data'] = options.data_dir + + # set up the configuration mapping from the parameters + if options.bind : + for (k, v) in options.bind : config_map[k] = v + + # parse the configuration files + try : + config = pconfig.parse_configuration_files(conffiles, confpaths, config_map) + except pconfig.ConfigurationException as e : + logger.error(str(e)) + sys.exit(-1) + + # set up the logging configuration + if config.get('Logging') is None : + config['Logging'] = { + 'LogFile' : '__screen__', + 'LogLevel' : 'INFO' + } + if options.logfile : + config['Logging']['LogFile'] = options.logfile + if options.loglevel : + config['Logging']['LogLevel'] = options.loglevel.upper() + + # make the configuration available to all of the PDO modules + pconfig.initialize_shared_configuration(config) + + plogger.setup_loggers(config.get('Logging', {})) + sys.stdout = plogger.stream_to_logger(logging.getLogger('STDOUT'), logging.DEBUG) + sys.stderr = plogger.stream_to_logger(logging.getLogger('STDERR'), logging.WARN) + + # set up the key search paths + if config.get('Key') is None : + config['Key'] = { + 'SearchPath' : [ '.', './keys', config_map['keys'] ], + 'FileName' : options.identity + ".pem" + } + if options.key_dir : + config['Key']['SearchPath'] = options.key_dir + + # set up the enclave service configuration + if config.get('GuardianService') is None : + config['GuardianService'] = { + 'HttpPort' : 7101, + 'Host' : 'localhost', + 'Identity' : options.identity, + } + if options.http : + config['GuardianService']['HttpPort'] = options.http + + # GO! + if options.test : + TestService(config) + else : + LocalMain(config) + +## ----------------------------------------------------------------- +## Entry points +## ----------------------------------------------------------------- +Main() diff --git a/medperf-contract/pdo/medperf/scripts/scripts.py b/medperf-contract/pdo/medperf/scripts/scripts.py new file mode 100644 index 0000000..fd26293 --- /dev/null +++ b/medperf-contract/pdo/medperf/scripts/scripts.py @@ -0,0 +1,31 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + + +from pdo.client.builder.shell import run_shell_command + +import warnings +warnings.catch_warnings() +warnings.simplefilter("ignore") + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def medperf_token() : + run_shell_command('do_medperf_token', 'pdo.medperf.plugins.medperf_token_object') + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +def medperf_guardian() : + run_shell_command('do_medperf_guardian', 'pdo.medperf.plugins.medperf_guardian') + diff --git a/medperf-contract/pdo/medperf/wsgi/__init__.py b/medperf-contract/pdo/medperf/wsgi/__init__.py new file mode 100644 index 0000000..ba6acd3 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/__init__.py @@ -0,0 +1,36 @@ +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +from pdo.medperf.wsgi.add_endpoint import AddEndpointApp +from pdo.medperf.wsgi.info import InfoApp +from pdo.medperf.wsgi.process_capability import ProcessCapabilityApp +from pdo.medperf.wsgi.provision_token_issuer import ProvisionTokenIssuerApp +from pdo.medperf.wsgi.provision_token_object import ProvisionTokenObjectApp + + +__all__ = [ + 'AddEndpointApp', + 'InfoApp', + 'ProcessCapabilityApp', + 'ProvisionTokenIssuerApp', + 'ProvisionTokenObjectApp' + ] + +wsgi_operation_map = { + 'add_endpoint' : AddEndpointApp, + 'info' : InfoApp, + 'process_capability' : ProcessCapabilityApp, + 'provision_token_issuer' : ProvisionTokenIssuerApp, + 'provision_token_object' : ProvisionTokenObjectApp + } diff --git a/medperf-contract/pdo/medperf/wsgi/add_endpoint.py b/medperf-contract/pdo/medperf/wsgi/add_endpoint.py new file mode 100644 index 0000000..f970ade --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/add_endpoint.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import io +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class AddEndpointApp(object) : + + __input_schema__ = { + "type" : "object", + "properties" : { + "contract_id" : {"type" : "string"}, + "ledger_attestation" : { + "type" : "object", + "properties": { + "contract_code_hash": {"type" : "string"}, + "metadata_hash": {"type" : "string"}, + "signature": {"type" : "string"}, + }, + }, + "contract_metadata" : { + "type" : "object", + "properties": { + "verifying_key" : {"type" : "string"}, + "encryption_key" : {"type" : "string"}, + } + }, + "contract_code_metadata" : { + "type" : "object", + "properties" : { + "code_hash": {"type" : "string"}, + "code_nonce": {"type" : "string"}, + }, + }, + }, + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + self.code_hash = config.get("TokenIssuer", {}).get("CodeHash", "") + self.contractIDs = config.get("TokenIssuer", {}).get("ContractIDs", []) + self.ledger_key = config.get("TokenIssuer", {}).get("LedgerKey", "") + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + contract_id = request['contract_id'] + ledger_attestation = request['ledger_attestation'] + contract_metadata = request['contract_metadata'] + contract_code_metadata = request['contract_code_metadata'] + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # verify the endpoint information + ## ## + + ## what is the policy for allowing an endpoint to be registered? + if self.code_hash and contract_code_metadata["code_hash"] != self.code_hash : + return ErrorResponse(start_response, 'endpoint not authorized') + if self.contractIDs and contract_id not in self.contractIDs : + return ErrorResponse(start_response, 'endpoint not authorized') + + # add the endpoint to the endpoint registry + verifying_key = contract_metadata['verifying_key'] + encryption_key = contract_metadata['encryption_key'] + self.endpoint_registry.set_endpoint(contract_id, verifying_key, encryption_key) + + # return success + result = json.dumps({'success' : True}).encode() + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/info.py b/medperf-contract/pdo/medperf/wsgi/info.py new file mode 100644 index 0000000..96c3eee --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/info.py @@ -0,0 +1,56 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InfoApp class, a WSGI interface class for +handling requests for enclave service information. +""" + +import json + +from http import HTTPStatus +from pdo.common.wsgi import ErrorResponse + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class InfoApp(object) : + def __init__(self, config, capability_store, endpoint_registry) : + self.storage_url = config['StorageService']['URL'] + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + def __call__(self, environ, start_response) : + try : + response = dict() + response['verifying_key'] = self.capability_store.svc_capability_key.verifying_key + response['encryption_key'] = self.capability_store.svc_capability_key.encryption_key + response['storage_service_url'] = self.storage_url + + result = json.dumps(response).encode() + except Exception as e : + logger.exception("info") + return ErrorResponse(start_response, "exception; {0}".format(str(e))) + + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/process_capability.py b/medperf-contract/pdo/medperf/wsgi/process_capability.py new file mode 100644 index 0000000..9451813 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/process_capability.py @@ -0,0 +1,118 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import recv_secret +from pdo.medperf.operations import capability_handler_map +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProcessCapabilityApp(object) : + __input_schema__ = { + "type" : "object", + "properties" : { + "minted_identity" : { "type" : "string" }, + "operation" : { + "type" : "object", + "properties" : { + "encrypted_session_key" : { "type" : "string" }, + "session_key_iv" : { "type" : "string" }, + "encrypted_message" : { "type" : "string" }, + }, + }, + } + } + + __operation_schema__ = { + "type" : "object", + "properties" : { + "nonce" : { "type" : "string" }, + "method_name" : { "type" : "string" }, + "parameters" : { "type" : "object" }, + } + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.config = config + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + self.capability_handler_map = {} + for (op, handler) in capability_handler_map.items() : + self.capability_handler_map[op] = handler(config) + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + capability_key = self.capability_store.get_capability_key(request['minted_identity']) + + operation_message = recv_secret(capability_key, request['operation']) + if not ValidateJSON(operation_message, self.__operation_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (ProcessCapability); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # dispatch the operation + try : + method_name = operation_message['method_name'] + parameters = operation_message['parameters'] + logger.info("process capability operation %s with parameters %s", method_name, parameters) + + operation = self.capability_handler_map[method_name] + operation_result = operation(parameters) + if operation_result is None : + return ErrorResponse(start_response, "operation failed") + + except KeyError as ke : + logger.error('unknown operation: %s', ) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception performing operation (ProcessCapability); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while performing operation") + + # and process the result + result = bytes(json.dumps(operation_result), 'utf8') + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/octet-stream'), + ('Content-Transfer-Encoding', 'utf-8'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py b/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py new file mode 100644 index 0000000..14b7dd0 --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/provision_token_issuer.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import send_secret +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest +import pdo.common.crypto as crypto + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProvisionTokenIssuerApp(object) : + __input_schema__ = { + "type" : "object", + "properties" : { + "contract_id" : {"type" : "string"}, + } + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + if not ValidateJSON(request, self.__input_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + contract_id = request['contract_id'] + issuer_keys = self.endpoint_registry.get_endpoint(contract_id) + + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # get the keys + provisioning_secret = dict() + provisioning_secret['capability_management_key'] = self.capability_store.mgmt_capability_key.encryption_key + provisioning_package = send_secret(issuer_keys, provisioning_secret) + + # return success + result = json.dumps(provisioning_package).encode() + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/json'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/pdo/medperf/wsgi/provision_token_object.py b/medperf-contract/pdo/medperf/wsgi/provision_token_object.py new file mode 100644 index 0000000..4324e7a --- /dev/null +++ b/medperf-contract/pdo/medperf/wsgi/provision_token_object.py @@ -0,0 +1,96 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +""" +This file defines the InvokeApp class, a WSGI interface class for +handling contract method invocation requests. +""" + +from http import HTTPStatus +import json + +from pdo.medperf.common.utility import ValidateJSON +from pdo.medperf.common.secrets import send_secret, recv_secret +from pdo.common.wsgi import ErrorResponse, UnpackJSONRequest +from pdo.common.keys import EnclaveKeys + +import logging +logger = logging.getLogger(__name__) + +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +## XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX +class ProvisionTokenObjectApp(object) : + __secret_schema__ = { + "type" : "object", + "properties" : { + "minted_identity" : { "type" : "string" }, + "token_description" : { "type" : "string" }, + "token_object_encryption_key" : { "type" : "string" }, + "token_object_verifying_key" : { "type" : "string" }, + "token_metadata": {"type" : "object" }, + }, + } + + # ----------------------------------------------------------------- + def __init__(self, config, capability_store, endpoint_registry) : + self.capability_store = capability_store + self.endpoint_registry = endpoint_registry + + # ----------------------------------------------------------------- + def __call__(self, environ, start_response) : + # unpack the request, this is WSGI magic + try : + request = UnpackJSONRequest(environ) + # the incoming message is in standard secret format, the session key + # should be encrypted with the management capability key + secret_message = recv_secret(self.capability_store.mgmt_capability_key, request) + + if not ValidateJSON(secret_message, self.__secret_schema__) : + return ErrorResponse(start_response, "invalid JSON") + + except KeyError as ke : + logger.error('missing field in request: %s', ke) + return ErrorResponse(start_response, 'missing field {0}'.format(ke)) + except Exception as e : + logger.error("unknown exception unpacking request (Invoke); %s", str(e)) + return ErrorResponse(start_response, "unknown exception while unpacking request") + + # create and save the token object capability key + capability_key = self.capability_store.create_capability_key(secret_message['minted_identity']) + token_object_package = dict() + token_object_package['minted_identity'] = secret_message['minted_identity'] + token_object_package['token_description'] = secret_message['token_description'] + token_object_package['token_metadata'] = secret_message['token_metadata'] + token_object_package['capability_generation_key'] = capability_key.encryption_key + + + # the provisioning package for the token object is encrypted with the + # token object's encryption key + token_object_keys = EnclaveKeys( + secret_message['token_object_verifying_key'], + secret_message['token_object_encryption_key']) + secret_package = send_secret(token_object_keys, token_object_package) + + # create the result + result = bytes(json.dumps(secret_package), 'utf8') + status = "{0} {1}".format(HTTPStatus.OK.value, HTTPStatus.OK.name) + headers = [ + ('Content-Type', 'application/octet-stream'), + ('Content-Transfer-Encoding', 'utf-8'), + ('Content-Length', str(len(result))) + ] + start_response(status, headers) + return [result] diff --git a/medperf-contract/scripts/gs_start.sh b/medperf-contract/scripts/gs_start.sh new file mode 100755 index 0000000..580e5d3 --- /dev/null +++ b/medperf-contract/scripts/gs_start.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +F_SCRIPT_NAME=$(basename ${BASH_SOURCE[-1]} ) + +F_CLEAN='no' +F_OUTPUTDIR='' + +F_USAGE='-c|--clean -o|--output dir --help -- ' +F_SHORT_OPTS='co:' +F_LONG_OPTS='clean,output:,help' + + +TEMP=$(getopt -o ${F_SHORT_OPTS} --long ${F_LONG_OPTS} -n "${F_SCRIPT_NAME}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT_NAME} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -c|--clean) F_CLEAN="yes" ; shift 1 ;; + -o|--output) F_OUTPUTDIR="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +# do not start if the service is already running +PLIST=$(pgrep -f ${F_SERVICE_CMD}) +if [ -n "$PLIST" ] ; then + echo existing services detected, please shutdown first + exit 1 +fi + +# start service asynchronously +yell start ${F_SERVICE_NAME} + +if [ "${F_CLEAN}" == "yes" ]; then + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log +fi + +rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +if [ "${F_OUTPUTDIR}" != "" ] ; then + EFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.err" + OFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.out" + rm -f $EFILE $OFILE +else + EFILE=/dev/null + OFILE=/dev/null +fi + +${F_SERVICE_CMD} $@ 2> $EFILE > $OFILE & +echo $! > ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +# verify that the service has started before continuing, we'll +# wait for 10 seconds plus or minus +declare tries=0 +declare max_tries=10 + +until $(${F_SERVICE_CMD} $@ --test 2> /dev/null > /dev/null) ; do + sleep 1 + tries=$((tries+1)) + if [ $tries = $max_tries ] ; then + die guardian service failed to start + fi +done diff --git a/medperf-contract/scripts/gs_status.sh b/medperf-contract/scripts/gs_status.sh new file mode 100755 index 0000000..7d2fcbc --- /dev/null +++ b/medperf-contract/scripts/gs_status.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f "${F_SERVICE_CMD}") + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +try ps -h --format pid,start,cmd -p $F_PID diff --git a/medperf-contract/scripts/gs_stop.sh b/medperf-contract/scripts/gs_stop.sh new file mode 100755 index 0000000..e22160c --- /dev/null +++ b/medperf-contract/scripts/gs_stop.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=medperf_guardian_service +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f ${F_SERVICE_CMD}) + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +# kill the service +if kill -SIGTERM ${F_PID} > /dev/null +then + sleep 1 +fi + +# clean up the PID file if we were successful +if ps -p ${F_PID} > /dev/null +then + yell failed to terminate process ${F_PID} +else + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid +fi diff --git a/medperf-contract/scripts/ss_start.sh b/medperf-contract/scripts/ss_start.sh new file mode 100755 index 0000000..415e0bb --- /dev/null +++ b/medperf-contract/scripts/ss_start.sh @@ -0,0 +1,77 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/sservice + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +F_SCRIPT_NAME=$(basename ${BASH_SOURCE[-1]} ) + +F_CLEAN='no' +F_OUTPUTDIR='' + +F_USAGE='-c|--clean -o|--output dir --help -- ' +F_SHORT_OPTS='co:' +F_LONG_OPTS='clean,output:,help' + +TEMP=$(getopt -o ${F_SHORT_OPTS} --long ${F_LONG_OPTS} -n "${F_SCRIPT_NAME}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT_NAME} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -c|--clean) F_CLEAN="yes" ; shift 1 ;; + -o|--output) F_OUTPUTDIR="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +# do not start if the service is already running +PLIST=$(pgrep -f ${F_SERVICE_CMD}) +if [ -n "$PLIST" ] ; then + echo existing storage service detected, please shutdown first + exit 1 +fi + +# start service asynchronously +echo start ${F_SERVICE_NAME} + +if [ "${F_CLEAN}" == "yes" ]; then + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.log +fi + +rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid + +if [ "${F_OUTPUTDIR}" != "" ] ; then + EFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.err" + OFILE="${F_OUTPUTDIR}/${F_SERVICE_NAME}.out" + rm -f $EFILE $OFILE +else + EFILE=/dev/null + OFILE=/dev/null +fi + +${F_SERVICE_CMD} $@ 2> $EFILE > $OFILE & +echo $! > ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid diff --git a/medperf-contract/scripts/ss_status.sh b/medperf-contract/scripts/ss_status.sh new file mode 100755 index 0000000..0f6f13e --- /dev/null +++ b/medperf-contract/scripts/ss_status.sh @@ -0,0 +1,36 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/${F_SERVICE_NAME} + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f "${F_SERVICE_CMD}") + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +try ps -h --format pid,start,cmd -p $F_PID diff --git a/medperf-contract/scripts/ss_stop.sh b/medperf-contract/scripts/ss_stop.sh new file mode 100755 index 0000000..5732aa1 --- /dev/null +++ b/medperf-contract/scripts/ss_stop.sh @@ -0,0 +1,48 @@ +#!/bin/bash + +# Copyright 2023 Intel Corporation +# +# Licensed 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. + +source ${PDO_HOME}/bin/lib/common.sh + +check_python_version + +F_SERVICE_NAME=guardian_sservice +F_SERVICE_CMD=${PDO_INSTALL_ROOT}/bin/sservice + +if [ -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid ]; then + F_PID=$(cat ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid) +else + yell unable to locate service pid file + + F_PID=$(pgrep -f ${F_SERVICE_CMD}) + if [ ! -n "${F_PID}" ] ; then + yell unable to locate service process + exit + fi +fi + +# kill the service +if kill -SIGTERM ${F_PID} > /dev/null +then + sleep 1 +fi + +# clean up the PID file if we were successful +if ps -p ${F_PID} > /dev/null +then + yell failed to terminate process ${F_PID} +else + rm -f ${PDO_HOME}/logs/${F_SERVICE_NAME}.pid +fi diff --git a/medperf-contract/setup.py b/medperf-contract/setup.py new file mode 100644 index 0000000..9f84d93 --- /dev/null +++ b/medperf-contract/setup.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +import os +import sys +import subprocess +import warnings + +# this should only be run with python3 +import sys +if sys.version_info[0] < 3: + print('ERROR: must run with python3') + sys.exit(1) + +from setuptools import setup, find_packages, find_namespace_packages + +# ----------------------------------------------------------------- +# Versions are tied to tags on the repository; to compute correctly +# it is necessary to be within the repository itself hence the need +# to set the cwd for the bin/get_version command. +# ----------------------------------------------------------------- +root_dir = os.path.dirname(os.path.realpath(__file__)) +try : + pdo_contracts_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir)).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_contracts version, using the default') + pdo_contracts_version = '0.0.0' + +try : + pdo_client_version = subprocess.check_output( + 'bin/get_version', cwd=os.path.join(root_dir, os.pardir, 'private-data-objects')).decode('ascii').strip() +except Exception as e : + warnings.warn('Failed to get pdo_client version, using the default') + pdo_client_version = '0.0.0' + +## ----------------------------------------------------------------- +## ----------------------------------------------------------------- +setup( + name='pdo_medperf', + version=pdo_contracts_version, + description='Implementation of guardian service for MedPerf experiment execution', + author='Wenyi Tang, Intel Labs', + author_email='wenyi1.tang@intel.com', + url='http://www.intel.com', + package_dir = { + 'pdo' : 'pdo', + 'pdo.medperf.resources.etc' : 'etc', + 'pdo.medperf.resources.context' : 'context', + 'pdo.medperf.resources.contracts' : '../build/medperf-contract', + 'pdo.medperf.resources.scripts' : 'scripts', + }, + packages = [ + 'pdo', + 'pdo.medperf', + 'pdo.medperf.plugins', + 'pdo.medperf.scripts', + 'pdo.medperf.common', + 'pdo.medperf.operations', + 'pdo.medperf.wsgi', + 'pdo.medperf.resources', + 'pdo.medperf.resources.etc', + 'pdo.medperf.resources.context', + 'pdo.medperf.resources.contracts', + 'pdo.medperf.resources.scripts', + ], + include_package_data=True, + # add everything from requirements.txt here + install_requires = [ + 'colorama', + 'colorlog', + 'lmdb', + 'toml', + 'twisted', + 'jsonschema>=3.0.1', + 'requests', + 'urllib3', + 'pdo-client>=' + pdo_client_version, + 'pdo-common-library>=' + pdo_client_version, + 'pdo-sservice>=' + pdo_client_version, + 'pdo-exchange>=' + pdo_contracts_version, + ], + entry_points = { + 'console_scripts' : [ + 'medperf_token=pdo.medperf.scripts.scripts:medperf_token', + 'medperf_guardian_service=pdo.medperf.scripts.guardianCLI:Main', + ] + } +) diff --git a/medperf-contract/test/.gitignore b/medperf-contract/test/.gitignore new file mode 100644 index 0000000..eb0abc5 --- /dev/null +++ b/medperf-contract/test/.gitignore @@ -0,0 +1 @@ +test_context.toml diff --git a/medperf-contract/test/script_test.sh b/medperf-contract/test/script_test.sh new file mode 100755 index 0000000..c0c6c96 --- /dev/null +++ b/medperf-contract/test/script_test.sh @@ -0,0 +1,231 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +# note +# ./script_test.sh --host 10.54.66.43 --ledger http://10.54.66.43:6600 + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_GUARDIAN_HOST=10.54.66.42 +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/medperf/context +F_EXCHANGE_TEMPLATES=${PDO_HOME}/contracts/exchange/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +if [ ! -f ${PDO_HOME}/keys/guardian_service.pem ]; then + yell create keys for the guardian service + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_service --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_service.pem) + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_sservice --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_sservice.pem) +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done + + yell "shutdown guardian and storage service" + ${PDO_HOME}/contracts/medperf/scripts/gs_stop.sh + ${PDO_HOME}/contracts/medperf/scripts/ss_stop.sh +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# Start the guardian service and the storage service +# ----------------------------------------------------------------- +try ${PDO_HOME}/contracts/medperf/scripts/ss_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_sservice + +sleep 3 + +try ${PDO_HOME}/contracts/medperf/scripts/gs_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_service \ + --bind host ${F_GUARDIAN_HOST} \ + --bind service_host ${F_SERVICE_HOST} + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests, check this +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${CONTEXT_FILE} + +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/tokens.toml \ + --bind token test1 --bind url http://${F_GUARDIAN_HOST}:7900 + +# ----------------------------------------------------------------- +# start the tests +# ----------------------------------------------------------------- + +yell create a token issuer and mint the tokens. Token configured so that asset can be used at most 3 times. +try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer +try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_object \ + --dataset_id "test_dataset_1" \ + --experiment_id "test_experiment_1" \ + --associated_model_ids "model_1" \ + --max_use_count 3 + + +yell get dataset info from the token object +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + +yell a test call of the hello world method in the contract +try medperf_token hello_world ${OPTS} --contract token.test1.token_object.token_1 + +yell use the dataset for a model, this is the first usage prior to transfer +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 + # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' + +# yell a test call of the hello world method in the contract +# try medperf_token hello_world ${OPTS} --contract token.test1.token_object.token_1 + +yell transfer the token to token_holder1 , only one more use permitted by the new owner +try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 + +yell new owner uses the model, this is the second and last usage of the asset +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + # --user_inputs '{"inputs": "Do you know me ?"}' --identity token_holder1 + +yell new owner attempts 3rd usage, and this should fail +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + # --user_inputs '{"inputs": "Am I lucky today ?"}' --identity token_holder1 + +exit From 7fbb3e7221b8c7296c4ff0206c95f69a2ec00441 Mon Sep 17 00:00:00 2001 From: Wenyi Tang Date: Fri, 16 Aug 2024 17:08:43 +0000 Subject: [PATCH 2/3] contract update for dataset token object, with a update_policy interface --- medperf-contract/contracts/token_object.cpp | 4 +- .../medperf/contracts/token_object.cpp | 379 +++++++++++--- medperf-contract/medperf/token_object.h | 46 +- .../pdo/medperf/operations/use_dataset.py | 91 ++-- .../medperf/plugins/medperf_token_object.py | 340 ++++++++---- medperf-contract/test/mp_test.sh | 495 ++++++++++++++++++ medperf-contract/test/script_test.sh | 106 +++- 7 files changed, 1223 insertions(+), 238 deletions(-) create mode 100755 medperf-contract/test/mp_test.sh diff --git a/medperf-contract/contracts/token_object.cpp b/medperf-contract/contracts/token_object.cpp index e0dff7c..0ffa95f 100644 --- a/medperf-contract/contracts/token_object.cpp +++ b/medperf-contract/contracts/token_object.cpp @@ -66,8 +66,8 @@ contract_method_reference_t contract_method_dispatch_table[] = { CONTRACT_METHOD2(get_dataset_info, ww::medperf::token_object::get_dataset_info), CONTRACT_METHOD2(use_dataset, ww::medperf::token_object::use_dataset), CONTRACT_METHOD2(get_capability, ww::medperf::token_object::get_capability), - CONTRACT_METHOD2(hello_world, ww::medperf::token_object::hello_world), - CONTRACT_METHOD2(hello_world_function, ww::medperf::token_object::hello_world_function), + CONTRACT_METHOD2(owner_test, ww::medperf::token_object::owner_test), + CONTRACT_METHOD2(update_policy, ww::medperf::token_object::update_policy), // object transfer, escrow & claim methods CONTRACT_METHOD2(transfer,ww::exchange::token_object::transfer), diff --git a/medperf-contract/medperf/contracts/token_object.cpp b/medperf-contract/medperf/contracts/token_object.cpp index 6e8bee9..8c921a4 100644 --- a/medperf-contract/medperf/contracts/token_object.cpp +++ b/medperf-contract/medperf/contracts/token_object.cpp @@ -14,9 +14,11 @@ */ #include +#include +// #include #include #include - +#include #include "Dispatch.h" #include "Cryptography.h" @@ -34,20 +36,19 @@ #include "exchange/token_object.h" #include "medperf/token_object.h" + static KeyValueStore dataset_TO_store("dataset_TO_store"); static const std::string dataset_id_KEY("dataset_id"); static const std::string experiment_id_KEY("experiment_id"); -static const std::string associated_model_ids_KEY("associated_model_ids_json_string"); -// static const std::string hfmodel_user_inputs_schema_KEY("hfmodel_user_inputs_schema"); -// static const std::string hfmodel_request_payload_type_KEY("hfmodel_request_payload_type"); -// static const std::string hfmodel_usage_info_KEY("hfmodel_usage_info"); +static const std::string associated_model_ids_KEY("associated_model_ids"); +static const std::string associated_model_tags_KEY("associated_model_tags"); static const std::string dataset_max_use_count_KEY("dataset_max_use_count"); static const std::string dataset_current_use_count_KEY("dataset_current_use_count"); +static const std::string pdo_challenge_KEY("pdo_challenge"); static const std::string dataset_use_capability_kv_store_encryption_key_KEY("dataset_use_capability_kv_store_encryption_key"); static const std::string dataset_use_capability_kv_store_root_block_hash_KEY("dataset_use_capability_kv_store_root_block_hash"); static const std::string dataset_use_capability_kv_store_input_key_KEY("dataset_use_capability_kv_store_input_key"); -// static const std::string dataset_use_capability_user_inputs_KEY("dataset_use_capability_user_inputs"); // @@ -74,21 +75,12 @@ bool ww::medperf::token_object::initialize(const Message &msg, const Environment // Get the params to be stored in dataset_TO_store const std::string dataset_id_value(msg.get_string("dataset_id")); - const std::string experiment_id_value(msg.get_string("experiment_id")); - const std::string associated_model_ids_value(msg.get_string("associated_model_ids")); - // const std::string hfmodel_user_inputs_schema_value(msg.get_string("user_inputs_schema")); - // const std::string hfmodel_request_payload_type_value(msg.get_string("payload_type")); - // const std::string hfmodel_usage_info_value(msg.get_string("hfmodel_usage_info")); - const uint32_t dataset_max_use_count_value = (uint32_t)msg.get_number("max_use_count"); // Store params from msg in dataset_TO_store ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_id_KEY, dataset_id_value), "failed to store dataset_id"); - ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, experiment_id_value), "failed to store experiment_id"); - ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_value), "failed to store associated_model_ids"); - // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_value), "failed to store hfmodel_user_inputs_schema"); - // // // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_request_payload_type_KEY, hfmodel_request_payload_type_value), "failed to store hfmodel_request_payload_type"); - // ASSERT_SUCCESS(rsp, dataset_TO_store.set(hfmodel_usage_info_KEY, hfmodel_usage_info_value), "failed to store hfmodel_usage_info"); - ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to store dataset_max_use_count"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, "none"), "failed to store experiment_id"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, "none"), "failed to store associated_model_ids"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, 0), "failed to store dataset_max_use_count"); // Set current use count to 0 ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, (uint32_t)0), "failed to store dataset_current_use_count"); @@ -109,6 +101,15 @@ bool ww::medperf::token_object::initialize(const Message &msg, const Environment return ww::exchange::token_object::initialize(to_message, env, rsp); } + +// update_policy attaches the policy +// bool ww::medperf::token_object::update_policy(const Message &msg, const Environment &env, Response &rsp) +// { +// // only the owner can +// ASSERT_SENDER_IS_CREATOR(env, rsp); +// ASSERT_UNINITIALIZED(rsp); +// } + // ----------------------------------------------------------------- // METHOD: get_dataset_info // @@ -127,32 +128,32 @@ bool ww::medperf::token_object::get_dataset_info( Response &rsp) { ASSERT_INITIALIZED(rsp); - ww::value::Structure v(DATASET_INFO_SCHEMA); + // ww::value::Structure v(DATASET_INFO_SCHEMA); + ww::value::Object v; // Get the dataset_id std::string dataset_id_string; ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_string), "failed to retrieve dataset_id"); ASSERT_SUCCESS(rsp, v.set_string("dataset_id", dataset_id_string.c_str()), "failed to set return value for dataset_id"); - // Get the fixed model parameters + std::string associated_model_ids_string; ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); ASSERT_SUCCESS(rsp, v.set_string("associated_model_ids", associated_model_ids_string.c_str()), "failed to set return value for associated_model_ids"); - // Get the schema for user-specified inputs (used only when payload type is json) - // std::string hfmodel_user_inputs_schema_string; - // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_user_inputs_schema_KEY, hfmodel_user_inputs_schema_string), "failed to retrieve hfmodel_user_inputs_schema"); - // ASSERT_SUCCESS(rsp, v.set_string("user_inputs_schema", hfmodel_user_inputs_schema_string.c_str()), "failed to set return value for hfmodel_user_inputs_schema"); - // Get the model metadata std::string experiment_id_string; ASSERT_SUCCESS(rsp, dataset_TO_store.get(experiment_id_KEY, experiment_id_string), "failed to retrieve experiment_id_string"); ASSERT_SUCCESS(rsp, v.set_string("experiment_id", experiment_id_string.c_str()), "failed to set return value for experiment_id_string"); - // // Get the max use count - // uint32_t dataset_max_use_count_value; - // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); - // ASSERT_SUCCESS(rsp, v.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ASSERT_SUCCESS(rsp, v.set_string("associated_model_tags", associated_model_tags_string.c_str()), "failed to set return value for associated_model_tags"); + + // Get the max use count + uint32_t dataset_max_use_count_value; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + ASSERT_SUCCESS(rsp, v.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); return rsp.value(v, false); } @@ -166,7 +167,7 @@ bool ww::medperf::token_object::get_dataset_info( // kvstore_encryption_key // kvstore_root_block_hash // kvstore_input_key -// user_inputs +// model_ids_to_evaluate // The first 3 parameters provide flexibility to use large inputs for the model via the kv_store attached to the guardian. // Only TO may invoke method // ----------------------------------------------------------------- @@ -180,19 +181,75 @@ bool ww::medperf::token_object::use_dataset( ASSERT_SUCCESS(rsp, msg.validate_schema(USE_DATASET_SCHEMA), "invalid request, missing required parameters"); + const std::string dataset_id(msg.get_string("dataset_id")); + std::string dataset_id_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_store), "fail to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, dataset_id == dataset_id_store, "dataset does not exist"); + const std::string kvstore_encryption_key(msg.get_string("kvstore_encryption_key")); const std::string kvstore_root_block_hash(msg.get_string("kvstore_root_block_hash")); const std::string kvstore_input_key(msg.get_string("kvstore_input_key")); - // const std::string user_inputs(msg.get_string("user_inputs")); - // check that current count < max count. Increment the current count. - // Note that we use < instead of <= since current count starts at 0. + + std::string associated_model_ids_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + uint32_t associated_model_ids = std::stoi(associated_model_ids_string); + + std::string model_ids_to_evaluate_string(msg.get_string("model_ids_to_evaluate")); + const uint32_t model_ids_to_evaluate = std::stoi(model_ids_to_evaluate_string) - 1; + ASSERT_SUCCESS(rsp, model_ids_to_evaluate < associated_model_ids, "model is not associated with the dataset"); + + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ww::types::StringArray associated_model_tags_vec(associated_model_tags_string); + ASSERT_SUCCESS(rsp, associated_model_tags_vec[model_ids_to_evaluate] == '0', "model has already been scheduled or used"); + uint32_t dataset_current_use_count_value; uint32_t dataset_max_use_count_value; ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); - ASSERT_SUCCESS(rsp, dataset_current_use_count_value < dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + ASSERT_SUCCESS(rsp, dataset_current_use_count_value + 1 <= dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + + // '1' represents that the model is scheduled for evaluation + associated_model_tags_vec[model_ids_to_evaluate] = '1'; + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_vec.str().c_str()), "failed to update associated_model_tags"); ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, dataset_current_use_count_value + 1), "failed to update dataset_current_use_count"); + + // ww::types::StringArray model_ids_to_evaluate_vec(model_ids_to_evaluate); + // uint32_t num_models_to_evaluate = std::count(model_ids_to_evaluate_vec.begin(), model_ids_to_evaluate_vec.end(), ',') + 1; + + // ASSERT_SUCCESS(rsp, num_models_to_evaluate > 0, "invalid request, no models to evaluate"); + + // std::string associated_model_ids_string; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids_string), "failed to retrieve associated_model_ids"); + // ww::types::StringArray associated_model_ids_vec(associated_model_ids_string); + // uint32_t num_associated_models = std::count(associated_model_ids_vec.begin(), associated_model_ids_vec.end(), ',') + 1; + + // std::vector associated_models = stringToVector(associated_model_ids_string); + // std::vector to_evaluate_models = stringToVector(model_ids_to_evaluate); + // std::vector model_index; + // std::string error_message; + // for (std::string to_eva_model_id : to_evaluate_models) + // { + // error_message = to_eva_model_id + " not associated with the dataset"; + // ASSERT_SUCCESS(rsp, std::find(associated_models.begin(), associated_models.end(), to_eva_model_id) != associated_models.end(), error_message.c_str()); + // model_index.push_back(std::find(associated_models.begin(), associated_models.end(), to_eva_model_id) - associated_models.begin()); + // } + + + // uint32_t dataset_current_use_count_value; + // uint32_t dataset_max_use_count_value; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to retrieve dataset_max_use_count"); + // ASSERT_SUCCESS(rsp, dataset_current_use_count_value + num_models_to_evaluate <= dataset_max_use_count_value, "max use count is reached, cannot use dataset"); + + // // Add the counter if the use_dataset is successful + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_current_use_count_KEY, dataset_current_use_count_value + num_models_to_evaluate), "failed to update dataset_current_use_count"); + + // // change the status of the model_ids to "scheduled" + // // const std::string associated_model_ids_to_be_stored = associated_model_ids_json.dump(); + // const std::string associated_model_ids_to_be_stored = mapToString(associated_model_ids_map); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_to_be_stored), "failed to store associated_model_ids"); // store the parameters required to generate a use_dataset capability ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_use_capability_kv_store_encryption_key_KEY, kvstore_encryption_key), "failed to store dataset_use_capability_kv_store_enc_key"); @@ -206,8 +263,6 @@ bool ww::medperf::token_object::use_dataset( - - // ----------------------------------------------------------------- // METHOD: get_capability // @@ -224,14 +279,13 @@ bool ww::medperf::token_object::get_capability( { ASSERT_SENDER_IS_OWNER(env, rsp); ASSERT_INITIALIZED(rsp); - ASSERT_SUCCESS(rsp, msg.validate_schema(GET_CAPABILITY_SCHEMA), "invalid request, missing required parameters"); // Ensure that the current use count is greater than 0, so that an attempt to use the model was made. // Otherwise, the capability cannot be generated.ls - uint32_t dataset_current_use_count_value; - ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); - ASSERT_SUCCESS(rsp, dataset_current_use_count_value > 0, "invalid request, capability can be obtained only after use_dataset is called"); + // uint32_t dataset_current_use_count_value; + // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_current_use_count_KEY, dataset_current_use_count_value), "failed to retrieve dataset_current_use_count"); + // ASSERT_SUCCESS(rsp, dataset_current_use_count_value > 0, "invalid request, capability can be obtained only after use_dataset is called"); // check for proof of commit of current state of the token object before returning capability std::string ledger_key; @@ -268,67 +322,246 @@ bool ww::medperf::token_object::get_capability( ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_kv_store_input_key_KEY, kvstore_input_key), "failed to retrieve dataset_use_capability_kv_store_input_key"); ASSERT_SUCCESS(rsp, params.set_string("kvstore_input_key", kvstore_input_key.c_str()), "failed to set return value for kvstore_input_key"); - // Get payload_type from dataset_TO_store - // std::string payload_type; - // // // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_request_payload_type_KEY, payload_type), "failed to retrieve hfmodel_request_payload_type"); - // ASSERT_SUCCESS(rsp, params.set_string("payload_type", payload_type.c_str()), "failed to set return value for payload_type"); - - // Get user_inputs from dataset_TO_store - // std::string user_inputs; - // ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_use_capability_user_inputs_KEY, user_inputs), "failed to retrieve dataset_use_capability_user_inputs"); - // ASSERT_SUCCESS(rsp, params.set_string("user_inputs", user_inputs.c_str()), "failed to set return value for user_inputs"); - // Get dataset_id from dataset_TO_store - std::string dataset_id; - ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id), "failed to retrieve dataset_id"); - ASSERT_SUCCESS(rsp, params.set_string("dataset_id", dataset_id.c_str()), "failed to set return value for dataset_id"); - - // Get experiment_id from dataset_TO_store - std::string experiment_id; - ASSERT_SUCCESS(rsp, dataset_TO_store.get(experiment_id_KEY, experiment_id), "failed to retrieve experiment_id"); - ASSERT_SUCCESS(rsp, params.set_string("experiment_id", experiment_id.c_str()), "failed to set return value for experiment_id"); + std::string dataset_id_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_store), "failed to retrieve dataset_id"); + std::string dataset_id_order(msg.get_string("dataset_id")); + ASSERT_SUCCESS(rsp, dataset_id_store == dataset_id_order, "dataset does not exist"); + ASSERT_SUCCESS(rsp, params.set_string("dataset_id", dataset_id_store.c_str()), "failed to set return value for dataset_id"); // Get associated_model_ids from dataset_TO_store - std::string associated_model_ids; - ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_ids_KEY, associated_model_ids), "failed to retrieve associated_model_ids"); - ASSERT_SUCCESS(rsp, params.set_string("associated_model_ids", associated_model_ids.c_str()), "failed to set return value for associated_model_ids"); + std::string associated_model_tags_string; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(associated_model_tags_KEY, associated_model_tags_string), "failed to retrieve associated_model_tags"); + ww::types::StringArray associated_model_tags_vec(associated_model_tags_string); + std::string model_id_to_evaluate_string = ""; + + for (size_t i = 0; i < associated_model_tags_vec.size(); i++) + { + if (associated_model_tags_vec[i] == '1') + { + model_id_to_evaluate_string += std::to_string(i+1) + ","; + associated_model_tags_vec[i] = '2'; + + } + } + model_id_to_evaluate_string.pop_back(); + + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_vec.str().c_str()), "failed to store associated_model_ids"); + ASSERT_SUCCESS(rsp, params.set_string("model_ids_to_evaluate", model_id_to_evaluate_string.c_str()), "failed to set return value for model_ids_to_evaluate"); - // Get user_inputs_schema from dataset_TO_store - // std::string user_inputs_schema; - // ASSERT_SUCCESS(rsp, dataset_TO_store.get(hfmodel_user_inputs_schema_KEY, user_inputs_schema), "failed to retrieve user_inputs_schema"); - // ASSERT_SUCCESS(rsp, params.set_string("user_inputs_schema", user_inputs_schema.c_str()), "failed to set return value for user_inputs_schema"); // Calculate capability ww::value::Object result; ASSERT_SUCCESS(rsp, ww::exchange::token_object::create_operation_package("use_dataset", params, result), "unexpected error: failed to generate capability"); - // this assumes that generating the capability does not change state, depending on - return rsp.value(result, false); + // this assumes that generating the capability changes state + return rsp.value(result, true); } // ----------------------------------------------------------------- // Method: return a hello world message // ----------------------------------------------------------------- -bool ww::medperf::token_object::hello_world( +bool ww::medperf::token_object::owner_test( const Message &msg, const Environment &env, Response &rsp) { - ww::value::String result("Hello, World!"); + // check if this is called by the owner + ASSERT_SENDER_IS_OWNER(env, rsp); + ww::value::String result("owner!"); return rsp.value(result, false); } // ----------------------------------------------------------------- -// Method: add simple function to the hello_world +// Method: check if the sender is the creator // ----------------------------------------------------------------- -bool ww::medperf::token_object::hello_world_function( +bool ww::medperf::token_object::update_policy( const Message &msg, const Environment &env, Response &rsp) { - ww::value::Structure result("{'message':'Hello, World!'}"); - return rsp.value(result, false); + // check if this is called by the creator + ASSERT_SENDER_IS_CREATOR(env, rsp); + + // schema validation + ASSERT_SUCCESS(rsp, msg.validate_schema(UPDATE_POLICY_SCHEMA), "invalid request, missing required parameters"); + + ww::value::Structure result(DATASET_INFO_SCHEMA); + + + const std::string dataset_id_value(msg.get_string("dataset_id")); + std::string dataset_id_value_from_store; + ASSERT_SUCCESS(rsp, dataset_TO_store.get(dataset_id_KEY, dataset_id_value_from_store), "failed to retrieve dataset_id"); + ASSERT_SUCCESS(rsp, dataset_id_value_from_store == dataset_id_value, "dataset does not exist"); + ASSERT_SUCCESS(rsp, result.set_string("dataset_id", dataset_id_value.c_str()), "failed to set return value for dataset_id"); + + std::string experiment_id_value(msg.get_string("experiment_id")); + // ASSERT_SUCCESS(rsp, msg.get_string("experiment_id", experiment_id_value), "failed to retrieve experiment_id"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(experiment_id_KEY, experiment_id_value), "failed to store experiment_id"); + ASSERT_SUCCESS(rsp, result.set_string("experiment_id", experiment_id_value.c_str()), "failed to set return value for experiment_id"); + + const std::string associated_model_ids_value(msg.get_string("associated_model_ids")); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_ids_value), "failed to store associated_model_ids"); + ASSERT_SUCCESS(rsp, result.set_string("associated_model_ids", associated_model_ids_value.c_str()), "failed to set return value for associated_model_ids"); + + // size_t model_count; + // ww::types::StringArray associated_model_ids_for_tag_string(associated_model_ids_value); + // model_count = std::count(associated_model_ids_for_tag_string.begin(), associated_model_ids_for_tag_string.end(), ',') + 1; + + size_t model_count = std::stoi(associated_model_ids_value); + const std::string associated_model_tags_value(model_count, '0'); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_value.c_str()), "failed to store associated_model_tags"); + ASSERT_SUCCESS(rsp, result.set_string("associated_model_tags", associated_model_tags_value.c_str()), "failed to set return value for associated_model_tags"); + // ww::value::Array associated_model_ids_array = splitString(associated_model_ids_value.c_str(), ','); + + + // std::vector associated_model_vec = stringToVector(associated_model_ids_value); + // set the associated_model_tag for each model_id in associated_model_ids to '0' + // std::vector associated_model_tags_vec(associated_model_vec.size(), '0'); + // const std::string associated_model_tags_value = vectorToString(associated_model_tags_vec); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_tags_KEY, associated_model_tags_value), "failed to store associated_model_tags"); + + // removed for testing + // ============================================= + // std::vector associated_model_ids = stringToVector(associated_model_ids_value); + // std::map associated_model_ids_map; + // for (auto model_id : associated_model_ids) + // { + // associated_model_ids_map[model_id] = "not used"; + // } + // const std::string associated_model_to_be_stored = mapToString(associated_model_ids_map); + // ASSERT_SUCCESS(rsp, dataset_TO_store.set(associated_model_ids_KEY, associated_model_to_be_stored), "failed to store associated_model_ids"); + + const uint32_t dataset_max_use_count_value = (uint32_t)msg.get_number("max_use_count"); + ASSERT_SUCCESS(rsp, dataset_TO_store.set(dataset_max_use_count_KEY, dataset_max_use_count_value), "failed to store dataset_max_use_count"); + ASSERT_SUCCESS(rsp, result.set_number("max_use_count", dataset_max_use_count_value), "failed to set return value for max_use_count"); + // ww::value::String result("Policy updated!"); + + return rsp.value(result, true); + // return rsp.success(true); } + + + +std::string mapToString(const std::map& myMap) { + std::ostringstream oss; // Create a string stream + oss << "{"; + for (auto it = myMap.begin(); it != myMap.end(); ++it) { + oss << "\"" << it->first << "\": \"" << it->second << "\""; + if (std::next(it) != myMap.end()) { + oss << ", "; // Add a comma, except for the last item + } + } + oss << "}"; + return oss.str(); // Return the formatted string +} + +std::map stringToMap(const std::string& str) { + std::map myMap; + size_t start = 0; + size_t end = 0; + + // Check if the string starts with '{' and ends with '}' + if (str.front() == '{' && str.back() == '}') { + start = 1; // Skip '{' + end = str.size() - 1; // Calculate end position + } else { + return myMap; // Return an empty map if format is incorrect + } + + while (start < end) { + size_t colonPos = str.find(':', start); // Find the position of ':' + size_t keyStart = str.find('"', start) + 1; // Find the key start position + size_t keyEnd = str.find('"', keyStart); // Find the key end position + + if (keyEnd == std::string::npos || colonPos == std::string::npos || keyEnd > colonPos) { + break; // Exit if format is incorrect + } + + std::string key = str.substr(keyStart, keyEnd - keyStart); // Get the key + size_t valueStart = colonPos + 1; // Start position of value + size_t valueEnd = str.find(',', valueStart); // Find the next ',' position + + if (valueEnd == std::string::npos) { + valueEnd = end; // If no ',' found, set to the end + } + + // Remove whitespace after ':' and before the starting quote of the value + size_t valueQuoteStart = str.find('"', valueStart); + size_t valueQuoteEnd = str.find('"', valueQuoteStart + 1); // Find the value end + + std::string value = str.substr(valueQuoteStart + 1, valueQuoteEnd - valueQuoteStart - 1); // Get value + + myMap[key] = value; // Add to the map + start = valueEnd + 1; // Update the start position + } + + return myMap; // Return the constructed map +} + +std::vector stringToVector(const std::string& str) { + std::vector result; + std::string::size_type start = 0; + std::string::size_type end = str.find(','); + + while (end != std::string::npos) { + result.push_back(str.substr(start, end - start)); + start = end + 1; + end = str.find(',', start); + } + result.push_back(str.substr(start)); + + return result; +} + +std::string vectorToString(const std::vector& vec) { + std::string result = "{"; + for (size_t i = 0; i < vec.size(); ++i) { + result += "\"" + vec[i] + "\""; + if (i < vec.size() - 1) { + result += ", "; + } + } + result += "}"; + return result; +} + +// // splitString takes a string and a delimiter character and returns a ww::value::Array +// ww::value::Array splitString(const std::string& str, char delimiter) { +// ww::value::Array result; +// ww::value::Object model_map; +// std::stringstream ss(str); +// std::string item; + +// while (std::getline(ss, item, delimiter)) { +// model_map.set_value(item.c_str(), ww::value::String("not used")); +// result.append_value(model_map); +// } + +// return result; +// } + + +std::vector splitForTags(const std::string& str) { + std::vector result; + std::stringstream ss(str); + std::string item; + + while (std::getline(ss, item, ',')) { + result.push_back('0'); + } + + return result; +} + +std::string joinString(const std::vector& vec, char delimiter) { + std::string result; + for (const auto& item : vec) { + result += item + delimiter; + } + + return result; +} \ No newline at end of file diff --git a/medperf-contract/medperf/token_object.h b/medperf-contract/medperf/token_object.h index d92f955..b794b9d 100644 --- a/medperf-contract/medperf/token_object.h +++ b/medperf-contract/medperf/token_object.h @@ -16,6 +16,9 @@ #pragma once #include +#include +#include +#include #include "Util.h" #include "Secret.h" @@ -26,9 +29,6 @@ #define DATASET_TO_INIT_PARAM_SCHEMA \ "{" \ SCHEMA_KW(dataset_id, "") "," \ - SCHEMA_KW(experiment_id, "") "," \ - SCHEMA_KW(associated_model_ids, "") "," \ - SCHEMA_KW(max_use_count, 0) "," \ SCHEMA_KW(ledger_verifying_key, "") "," \ SCHEMA_KWS(initialization_package, CONTRACT_SECRET_SCHEMA) "," \ SCHEMA_KWS(asset_authority_chain, ISSUER_AUTHORITY_CHAIN_SCHEMA)\ @@ -38,18 +38,23 @@ "{" \ SCHEMA_KW(dataset_id, "") "," \ SCHEMA_KW(experiment_id, "") "," \ - SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(associated_model_tags, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ "}" #define USE_DATASET_SCHEMA \ - "{" \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(model_ids_to_evaluate, "") "," \ SCHEMA_KW(kvstore_encryption_key, "") "," \ SCHEMA_KW(kvstore_root_block_hash, "") "," \ SCHEMA_KW(kvstore_input_key, "") "," \ "}" #define GET_CAPABILITY_SCHEMA \ - "{" \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ SCHEMA_KW(ledger_signature,"") \ "}" @@ -60,8 +65,15 @@ SCHEMA_KW(kvstore_root_block_hash, "") "," \ SCHEMA_KW(kvstore_input_key, "") "," \ SCHEMA_KW(dataset_id, "") "," \ - SCHEMA_KW(experiment_id, "") "," \ - SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(model_ids_to_evaluate, "") "," \ + "}" + +#define UPDATE_POLICY_SCHEMA \ + "{" \ + SCHEMA_KW(dataset_id, "") "," \ + SCHEMA_KW(experiment_id, "") "," \ + SCHEMA_KW(associated_model_ids, "") "," \ + SCHEMA_KW(max_use_count, 0) "," \ "}" @@ -74,7 +86,9 @@ namespace token_object { // methods bool initialize(const Message& msg, const Environment& env, Response& rsp); - + + bool update_policy(const Message &msg, const Environment &env, Response &rsp); + // reserved for testing bool get_dataset_info(const Message& msg, const Environment& env, Response& rsp); @@ -82,8 +96,18 @@ namespace token_object bool get_capability(const Message& msg, const Environment& env, Response& rsp); // reserved for testing - bool hello_world(const Message& msg, const Environment& env, Response& rsp); - bool hello_world_function(const Message& msg, const Environment& env, Response& rsp); + bool owner_test(const Message& msg, const Environment& env, Response& rsp); + bool update_policy(const Message& msg, const Environment& env, Response& rsp); + + // utility functions + const std::string mapToString(const std::map& myMap); + std::map stringToMap(const std::string& str); + std::vector stringToVector(const std::string& str); + std::string vectorToString(const std::vector& vec); + // ww::value::Array splitString(const std::string& str, char delimiter); + std::vector splitString(const std::string& str, char delimiter); + std::string joinString(const std::vector& vec, char delimiter); + std::vector splitForTags(const std::string& str); }; // token_object }; // medperf }; // ww diff --git a/medperf-contract/pdo/medperf/operations/use_dataset.py b/medperf-contract/pdo/medperf/operations/use_dataset.py index d0f9a3e..b277240 100644 --- a/medperf-contract/pdo/medperf/operations/use_dataset.py +++ b/medperf-contract/pdo/medperf/operations/use_dataset.py @@ -18,6 +18,10 @@ handling contract method invocation requests. """ +import hashlib +import random +import yaml +import subprocess from pdo.medperf.common.utility import ValidateJSON from pdo.common.key_value import KeyValueStore @@ -25,6 +29,7 @@ import requests import urllib.request import json +import os logger = logging.getLogger(__name__) @@ -40,9 +45,6 @@ class DataOperation(object) : "kvstore_input_key" : { "type" : "string" }, "dataset_id" : { "type" : "string" }, "experiment_id" : { "type" : "string" }, - # "payload_type" : { "type" : "string" }, - # "user_inputs" : { "type" : "string" }, - # "user_inputs_schema" : { "type" : "string" }, "associated_model_ids" : { "type" : "string" }, } } @@ -95,47 +97,56 @@ def __call__(self, params) : kvstore_root_block_hash = params['kvstore_root_block_hash'] kvstore_input_key = params['kvstore_input_key'] dataset_id = params['dataset_id'] - experiment_id = params['experiment_id'] - # payload_type = params['payload_type'] - # user_inputs = json.loads(params['user_inputs']) - # user_inputs_schema = json.loads(params['user_inputs_schema']) - associated_model_ids = params['associated_model_ids'] + model_ids_to_evaluate = params['model_ids_to_evaluate'] - - # # If payload type is binary, get the input data from the key-value store. - # if payload_type == 'binary': - # kv = KeyValueStore(kvstore_encryption_key, kvstore_root_block_hash) - # with kv: - # input_data = kv.get(kvstore_input_key, output_encoding='raw') - # payload = bytes(input_data) - # elif payload_type == 'json': - # # check schema of user inputs, and generate payload by merging user inputs with fixed model parameters - # if not ValidateJSON(user_inputs, user_inputs_schema) : - # logger.error("Invalid user inputs") - # return None - # payload = {**associated_model_ids, **user_inputs} - # else: - # logger.error("Invalid payload type. Supported types are 'json' and 'binary'") - # return None + mlcube_script_path = "/home/wenyi1/medperf/test_resource/test.sh" + result_path = "/home/wenyi1/medperf/test_resource/test_results" - # query the Hugging Face model and return the response - # return self.query_hf_model(experiment_id, dataset_id, payload, payload_type) - - test_message = "Hello from the guardian service. Assume the experiment has been run successfully." + test_message = "Hello from the guardian service." # put the parameters in test_message and return # for testing purpose + test_message += f"\nReceived order for:" test_message += f"\nDataset ID: {dataset_id}" - test_message += f"\nExperiment ID: {experiment_id}" - test_message += f"\nAssociated Model IDs: {associated_model_ids}" + test_message += f"\nModel IDs : {model_ids_to_evaluate}" + print(test_message) + print("================================================") + print("Evaluating models...") + # split model_ids_to_evaluate into a list of integers + model_ids = model_ids_to_evaluate.split(',') + model_ids = [int(i) for i in model_ids] + + for model in model_ids: + if model == 1 or model == 2: + print(f"Cubes available for model {model}, running the MLCube script...") + # run the MLCube script with parameter model + # os.system(f"bash {mlcube_script_path} {model}") + output_result = subprocess.run(["bash", mlcube_script_path, str(model)], capture_output=True, text=True) + print(output_result.stdout) + else: + print(f"Model {model} not available. Generating simulated data...") + # generate simulated data + sim_data = {'AUC': random.uniform(0.5, 1), 'Accuracy': random.uniform(0.5, 1)} + with open(f"{result_path}/{str(model)}.yaml", 'w') as file: + yaml.dump(sim_data, file) + print(f"Simulated data for model {model} generated successfully.") + + print("================================================") + print("Signing each file with simple SHA256 and challenge...") + # handle each model result + for model in model_ids: + try: + with open(f"{result_path}/{str(model)}.yaml", 'r+') as file: + result_data = yaml.safe_load(file) + hash_result = self.simpleSHA256withChallenge(kvstore_input_key, result_data) + new_data = {"hash": hash_result} + yaml.dump(new_data, file) + print(f"Model {model} result signed successfully.") + except Exception as e: + print(f"Error signing model {model} result: {e}") + continue return test_message - # to do: (Support for post processing) - # Add support for optionally encrypting response before sending back to the client - # encryption key specified by token object - # token object can implement policies for endorsing encryption keys - # for example: results encrypted for use within another PDO contract or even the token object itself - # Such a feature can be used for privacy-preserving post processing of the model outputs - # before sharing final results with the client - # If there are generic post processing steps that can be applied to a large class of models, - # the code can be added here itself. - \ No newline at end of file + def simpleSHA256withChallenge(self, challenge, result_data): + # parse result_data to string + result_data_string = json.dumps(result_data) + return hashlib.sha256(challenge.encode() + result_data_string.encode()).hexdigest() \ No newline at end of file diff --git a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py index d0bdddb..267586e 100644 --- a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py +++ b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py @@ -15,6 +15,7 @@ import json import logging import time +import sqlite3 from pdo.submitter.create import create_submitter from pdo.contract import invocation_request @@ -49,15 +50,16 @@ 'op_get_dataset_info', 'op_use_dataset', 'op_get_capability', - 'op_hello_world', - 'op_hello_world_function', + 'op_owner_test', + 'op_update_policy', 'cmd_mint_dataset_tokens', 'cmd_mint_tokens', 'cmd_transfer_assets', 'cmd_use_dataset', 'cmd_get_dataset_info', - 'cmd_hello_world', - 'cmd_hello_world_function', + 'cmd_owner_test', + 'cmd_experiment_order', + 'cmd_update_policy', 'do_medperf_token', 'do_medperf_token_contract', 'load_commands', @@ -103,12 +105,12 @@ def add_arguments(cls, subparser) : help='ledger verifying key', type=pbuilder.invocation_parameter, required=True) subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) - subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) - subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) - # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) - # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) - # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) - subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) + # subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) + # subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) + # # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) + # # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) + # # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) + # subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) @classmethod @@ -121,12 +123,12 @@ def invoke(cls, state, session_params, ledger_key, initialization_package, autho # Add params from kwargs params['dataset_id'] = kwargs.get('dataset_id') - params['experiment_id'] = kwargs.get('experiment_id') - params['associated_model_ids'] = kwargs.get('associated_model_ids') + # params['experiment_id'] = kwargs.get('experiment_id') + # params['associated_model_ids'] = kwargs.get('associated_model_ids') # params['user_inputs_schema'] = kwargs.get('user_inputs_schema') # params['payload_type'] = kwargs.get('payload_type', 'json') # params['medperf_usage_info'] = kwargs.get('medperf_usage_info') - params['max_use_count'] = kwargs.get('max_use_count', 1) + # params['max_use_count'] = kwargs.get('max_use_count', 1) # parse the message (python dict as the defined schema) and call @@ -151,7 +153,13 @@ def invoke(cls, state, session_params, **kwargs) : params = {} message = invocation_request('get_dataset_info', **params) + # try: + # result = pcontract_cmd.send_to_contract(state, message, **session_params) + # except Exception as e: + # cls.display_error("get_dataset_info method evaluation failed. {}".format(e)) + # return None result = pcontract_cmd.send_to_contract(state, message, **session_params) + cls.log_invocation(message, result) return result @@ -189,12 +197,17 @@ def add_arguments(cls, subparser) : type=str) # subparser.add_argument( - # '--user_inputs', - # help='User inputs for the model, used when the payload is JSON', + # '--dataset_id', + # help='Name of the contract class for the given dataset', + # type=str) + + # subparser.add_argument( + # '--model_ids_to_evaluate', + # help='Models to be tested on the dataset', # type=str) @classmethod - def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key, kvstore_root_block_hash, **kwargs) : + def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key, kvstore_root_block_hash, dataset_id, model_ids_to_evaluate, **kwargs) : session_params['commit'] = True # send the request to the contract to create a capability for the guardian @@ -202,13 +215,17 @@ def invoke(cls, state, session_params, kvstore_encryption_key, kvstore_input_key params['kvstore_encryption_key'] = kvstore_encryption_key params['kvstore_input_key'] = kvstore_input_key params['kvstore_root_block_hash'] = kvstore_root_block_hash + params['dataset_id'] = dataset_id + params['model_ids_to_evaluate'] = model_ids_to_evaluate # params['user_inputs'] = user_inputs message = invocation_request('use_dataset', **params) - try: - result = pcontract_cmd.send_to_contract(state, message, **session_params) - except Exception as e: - raise + result = pcontract_cmd.send_to_contract(state, message, **session_params) + + # try: + # result = pcontract_cmd.send_to_contract(state, message, **session_params) + # except Exception as e: + # raise cls.log_invocation(message, result) return result @@ -232,6 +249,11 @@ def add_arguments(cls, subparser) : '-l', '--ledger-attestation', help='attestation from the ledger that the current state of the token issuer is committed', type=pbuilder.invocation_parameter, required=True) + + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) @classmethod def invoke(cls, state, session_params, ledger_attestation, **kwargs) : @@ -239,6 +261,7 @@ def invoke(cls, state, session_params, ledger_attestation, **kwargs) : params = {} params['ledger_signature'] = ledger_attestation + params['dataset_id'] = kwargs.get('dataset_id') message = invocation_request('get_capability', **params) capability = pcontract_cmd.send_to_contract(state, message, **session_params) @@ -248,11 +271,11 @@ def invoke(cls, state, session_params, ledger_attestation, **kwargs) : # ----------------------------------------------------------------- # ----------------------------------------------------------------- -class op_hello_world(pcontract.contract_op_base) : - """op_hello_world implements a simple hello world operation +class op_owner_test(pcontract.contract_op_base) : + """op_owner_test implements a simple hello world operation """ - name = "hello_world" + name = "owner_test" help = "simple hello world operation" # # For this operation, we don't need any arguments @@ -267,7 +290,7 @@ def invoke(cls, state, session_params, **kwargs) : params = {} - message = invocation_request('hello_world', **params) + message = invocation_request('owner_test', **params) result = pcontract_cmd.send_to_contract(state, message, **session_params) cls.log_invocation(message, result) @@ -277,11 +300,11 @@ def invoke(cls, state, session_params, **kwargs) : ## ----------------------------------------------------------------- ## ----------------------------------------------------------------- -class cmd_hello_world(pcommand.contract_command_base) : - """cmd_hello_world implements a simple hello world command +class cmd_owner_test(pcommand.contract_command_base) : + """cmd_owner_test implements a simple hello world command """ - name = "hello_world" + name = "owner_test" help = "simple hello world command" @classmethod @@ -291,35 +314,64 @@ def invoke(cls, state, context, **kwargs) : raise ValueError("token has not been created") session = pbuilder.SessionParameters(save_file=save_file) - # invoke the hello_world operation - + # invoke the owner_test operation result = pcontract.invoke_contract_op( - op_hello_world, - state, context, session, - **kwargs) - + op_owner_test, + state, context, session, + **kwargs) + # try: + # result = pcontract.invoke_contract_op( + # op_owner_test, + # state, context, session, + # **kwargs) + # except Exception as e: + # cls.display_error("op_owner_test method evaluation failed. {}".format(e)) + # return None cls.display(result) ## ----------------------------------------------------------------- ## ----------------------------------------------------------------- -class op_hello_world_function(pcontract.contract_op_base) : - """op_hello_world_function implements a simple hello world operation +class op_update_policy(pcontract.contract_op_base) : + """op_update_policy implements a simple hello world operation """ - name = "hello_world_function" + name = "update_policy" help = "simple hello world operation" @classmethod def add_arguments(cls, subparser) : - pass + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--experiment_id', + help='Experiments to be teseted on the dataset', + type=str) + + + subparser.add_argument( + '--associated_model_ids', + help='Models to be tested on the dataset', + type=str) + + subparser.add_argument( + '--max_use_count', + help='Max number of times the dataset can be used', + type=int) @classmethod def invoke(cls, state, session_params, **kwargs) : session_params['commit'] = False params = {} + params['dataset_id'] = kwargs.get('dataset_id') + params['experiment_id'] = kwargs.get('experiment_id') + params['associated_model_ids'] = kwargs.get('associated_model_ids') + params['max_use_count'] = kwargs.get('max_use_count') - message = invocation_request('hello_world_function', **params) + message = invocation_request('update_policy', **params) result = pcontract_cmd.send_to_contract(state, message, **session_params) cls.log_invocation(message, result) @@ -335,22 +387,19 @@ class cmd_use_dataset(pcommand.contract_command_base) : name = "use_dataset" help = "run experiment on the dataset" - # @classmethod - # def add_arguments(cls, subparser) : - # # subparser.add_argument( - # # '--data_file', - # # help='Filename of the data_file to use as inference input. this is used only if payload is binary', - # # type=str) - - # # subparser.add_argument( - # # '--search-path', - # # help='Directories to search for the data file', - # # nargs='+', type=str, default=['.', './data']) + @classmethod + def add_arguments(cls, subparser) : + + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--model_ids_to_evaluate', + help='Models to be tested on the dataset', + type=str) - # subparser.add_argument( - # '--user_inputs', - # help='User inputs for the model, used when the payload is JSON', - # type=str) @classmethod def invoke(cls, state, context, **kwargs) : @@ -363,20 +412,17 @@ def invoke(cls, state, context, **kwargs) : # query the token object for model info, get the payload type, and ensure that for binary payloads # data file is provided, and for json payloads, user inputs are provided session = pbuilder.SessionParameters(save_file=save_file) - model_info_json = pcontract.invoke_contract_op( - op_get_dataset_info, - state, context, session, - **kwargs) + # model_info_json = pcontract.invoke_contract_op( + # op_get_dataset_info, + # state, context, session, + # **kwargs) kvstore_encryption_key = "not_used" - kvstore_input_key = "not_used" + kvstore_input_key = "this_is_a_test_string_as_challenge" kvstore_root_block_hash = "not_used" - - # invoke op_use_dataset to store the parameters required to generate the capability - session = pbuilder.SessionParameters(save_file=save_file) try: - _ = pcontract.invoke_contract_op( + result = pcontract.invoke_contract_op( op_use_dataset, state, context, session, kvstore_encryption_key, @@ -391,26 +437,28 @@ def invoke(cls, state, context, **kwargs) : time.sleep(2) # wait for the ledger to commit the transaction, not sure if any wait is needed or the # correct solution is to poll the ledger until the transaction is committed - to_contract = pcontract_cmd.get_contract(state, save_file) - ledger_submitter = create_submitter(state.get(['Ledger'])) - state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) - - # get capability from the token object - capability = pcontract.invoke_contract_op ( - op_get_capability, - state, context, session, - state_attestation['signature'], - **kwargs) - - # push data file to storage service associated with the guardian if payload is binary - guardian_context = context.get_context('data_guardian_context') - url = guardian_context['url'] - service_client = GuardianServiceClient(url) - # send capability to guardian service - capability = json.loads(capability) - result = service_client.process_capability(**capability) - cls.display(result) return result + + # to_contract = pcontract_cmd.get_contract(state, save_file) + # ledger_submitter = create_submitter(state.get(['Ledger'])) + # state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) + + # # get capability from the token object + # capability = pcontract.invoke_contract_op ( + # op_get_capability, + # state, context, session, + # state_attestation['signature'], + # **kwargs) + + # # push data file to storage service associated with the guardian if payload is binary + # guardian_context = context.get_context('data_guardian_context') + # url = guardian_context['url'] + # service_client = GuardianServiceClient(url) + # # send capability to guardian service + # capability = json.loads(capability) + # result = service_client.process_capability(**capability) + # cls.display(result) + # return result @@ -428,12 +476,6 @@ class cmd_mint_dataset_tokens(pcommand.contract_command_base) : @classmethod def add_arguments(cls, subparser) : subparser.add_argument('--dataset_id', help='Name of the contract class for the given dataset', type=str) - subparser.add_argument('--experiment_id', help='Experiments to be teseted on the dataset', type=str) - subparser.add_argument('--associated_model_ids', help='Models to be tested on the dataset', type=str) - # subparser.add_argument('--user_inputs_schema', help='Name of the provisioning service group to use', type=str) - # subparser.add_argument('--payload_type', help='Name of the storage service group to use', type=str) - # subparser.add_argument('--medperf_usage_info', help='File that contains contract source code', type=str) - subparser.add_argument('--max_use_count', help='File that contains contract source code', type=int) @classmethod def invoke(cls, state, context, **kwargs) : @@ -467,6 +509,121 @@ def invoke(cls, state, context, **kwargs) : return result +class cmd_update_policy(pcommand.contract_command_base) : + """cmd_update_policy implements a simple hello world command + """ + + name = "update_policy" + help = "simple hello world command" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + subparser.add_argument( + '--experiment_id', + help='Experiments to be teseted on the dataset', + type=str) + + subparser.add_argument( + '--associated_model_ids', + help='Models to be tested on the dataset', + type=str) + + subparser.add_argument( + '--max_use_count', + help='Max number of times the dataset can be used', + type=int) + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + # invoke the owner_test operation + # result = pcontract.invoke_contract_op( + # op_update_policy, + # state, context, session, + # **kwargs) + try: + result = pcontract.invoke_contract_op( + op_update_policy, + state, context, session, + **kwargs) + except Exception as e: + cls.display_error("op_update_policy method evaluation failed. {}".format(e)) + return None + + cls.display(result) + + +class cmd_experiment_order(pcommand.contract_command_base) : + """cmd_experiment_order implements a simple hello world command + """ + + name = "experiment_order" + help = "simple hello world command" + + @classmethod + def add_arguments(cls, subparser) : + subparser.add_argument( + '--dataset_id', + help='Name of the contract class for the given dataset', + type=str) + + + @classmethod + def invoke(cls, state, context, **kwargs) : + save_file = pcontract_cmd.get_contract_from_context(state, context) + if not save_file : + raise ValueError("token has not been created") + + session = pbuilder.SessionParameters(save_file=save_file) + + to_contract = pcontract_cmd.get_contract(state, save_file) + ledger_submitter = create_submitter(state.get(['Ledger'])) + state_attestation = ledger_submitter.get_current_state_hash(to_contract.contract_id) + + # get capability from the token object + capability = pcontract.invoke_contract_op ( + op_get_capability, + state, context, session, + state_attestation['signature'], + **kwargs) + try: + data_json = {} + data_json["order"] = json.dumps(capability) + print(data_json) + conn = sqlite3.connect("/home/wenyi1/medperf/server/db.sqlite3") + cur = conn.cursor() + # insert new result record to the table + for i in range(10): + cur.execute("update dataset_dataset set user_metadata = ? where id = ?", (json.dumps(data_json), 1)) + conn.commit() + conn.close() + except Exception as e: + print("Error in posting order to MedPerf server") + print(e) + conn.close() + + input("Posting the packed work order to MedPerf Server, now the dataset owner can download the encrypted workorder from Medperf.\n For simplicity, forwarding directly to the guardian.\nPress Enter to continue...") + + + guardian_context = context.get_context('data_guardian_context') + url = guardian_context['url'] + service_client = GuardianServiceClient(url) + # send capability to guardian service + capability = json.loads(capability) + result = service_client.process_capability(**capability) + cls.display(result) + return result + + ## ----------------------------------------------------------------- ## Create the generic, shell independent version of the aggregate command ## ----------------------------------------------------------------- @@ -485,8 +642,8 @@ def invoke(cls, state, context, **kwargs) : op_use_dataset, op_get_dataset_info, op_get_capability, - op_hello_world, - op_hello_world_function, + op_owner_test, + op_update_policy, ] do_medperf_token_contract = pcontract.create_shell_command('medperf_token_contract', __operations__) @@ -496,8 +653,9 @@ def invoke(cls, state, context, **kwargs) : cmd_transfer_assets, cmd_use_dataset, cmd_get_dataset_info, - cmd_hello_world, - # cmd_hello_world_function, + cmd_owner_test, + cmd_update_policy, + cmd_experiment_order, ] do_medperf_token = pcommand.create_shell_command('medperf_token', __commands__) diff --git a/medperf-contract/test/mp_test.sh b/medperf-contract/test/mp_test.sh new file mode 100755 index 0000000..4c78fb6 --- /dev/null +++ b/medperf-contract/test/mp_test.sh @@ -0,0 +1,495 @@ +#!/bin/bash + +# Copyright 2024 Intel Corporation +# +# Licensed 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. + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +: "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" +: "${PDO_HOME?Missing environment variable PDO_HOME}" +: "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +source ${PDO_HOME}/bin/lib/common.sh +check_python_version + +# note +# ./script_test.sh --host 10.54.66.43 --ledger http://10.54.66.43:6600 + +if ! command -v pdo-shell &> /dev/null ; then + die unable to locate pdo-shell +fi + +# ----------------------------------------------------------------- +# ----------------------------------------------------------------- +if [ "${PDO_LEDGER_TYPE}" == "ccf" ]; then + if [ ! -f "${PDO_LEDGER_KEY_ROOT}/networkcert.pem" ]; then + die "CCF ledger keys are missing, please copy and try again" + fi +fi + +# ----------------------------------------------------------------- +# Process command line arguments +# ----------------------------------------------------------------- +SCRIPTDIR="$(dirname $(readlink --canonicalize ${BASH_SOURCE}))" +SOURCE_ROOT="$(realpath ${SCRIPTDIR}/..)" + +F_SCRIPT=$(basename ${BASH_SOURCE[-1]} ) +F_SERVICE_HOST=${PDO_HOSTNAME} +F_GUARDIAN_HOST=10.54.66.42 +F_LEDGER_URL=${PDO_LEDGER_URL} +F_LOGLEVEL=${PDO_LOG_LEVEL:-info} +F_LOGFILE=${PDO_LOG_FILE:-__screen__} +F_CONTEXT_FILE=${SOURCE_ROOT}/test/test_context.toml +F_CONTEXT_TEMPLATES=${PDO_HOME}/contracts/medperf/context +F_EXCHANGE_TEMPLATES=${PDO_HOME}/contracts/exchange/context + +F_USAGE='--host service-host | --ledger url | --loglevel [debug|info|warn] | --logfile file' +SHORT_OPTS='h:l:' +LONG_OPTS='host:,ledger:,loglevel:,logfile:' + +TEMP=$(getopt -o ${SHORT_OPTS} --long ${LONG_OPTS} -n "${F_SCRIPT}" -- "$@") +if [ $? != 0 ] ; then echo "Usage: ${F_SCRIPT} ${F_USAGE}" >&2 ; exit 1 ; fi + +eval set -- "$TEMP" +while true ; do + case "$1" in + -h|--host) F_SERVICE_HOST="$2" ; shift 2 ;; + -1|--ledger) F_LEDGER_URL="$2" ; shift 2 ;; + --loglevel) F_LOGLEVEL="$2" ; shift 2 ;; + --logfile) F_LOGFILE="$2" ; shift 2 ;; + --help) echo "Usage: ${SCRIPT_NAME} ${F_USAGE}"; exit 0 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +F_SERVICE_SITE_FILE=${PDO_HOME}/etc/sites/${F_SERVICE_HOST}.toml +if [ ! -f ${F_SERVICE_SITE_FILE} ] ; then + die unable to locate the service information file ${F_SERVICE_SITE_FILE}; \ + please copy the site.toml file from the service host +fi + +F_SERVICE_GROUPS_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_groups_db +F_SERVICE_DB_FILE=${SOURCE_ROOT}/test/${F_SERVICE_HOST}_db + +_COMMON_=("--logfile ${F_LOGFILE}" "--loglevel ${F_LOGLEVEL}") +_COMMON_+=("--ledger ${F_LEDGER_URL}") +_COMMON_+=("--groups-db ${F_SERVICE_GROUPS_DB_FILE}") +_COMMON_+=("--service-db ${F_SERVICE_DB_FILE}") +SHORT_OPTS=${_COMMON_[@]} + +_COMMON_+=("--context-file ${F_CONTEXT_FILE}") +OPTS=${_COMMON_[@]} + +# ----------------------------------------------------------------- +# Make sure the keys and eservice database are created and up to date +# ----------------------------------------------------------------- +F_KEY_FILES=() +KEYGEN=${PDO_SOURCE_ROOT}/build/__tools__/make-keys +if [ ! -f ${PDO_HOME}/keys/red_type_private.pem ]; then + yell create keys for the contracts + for color in red green blue orange purple white ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_{type,vetting,issuer}_{private,public}.pem) + done + + for color in green1 green2 green3; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/${color}_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/${color}_issuer_{private,public}.pem) + done + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_type --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_vetting --format pem + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_issuer --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_{type,vetting,issuer}_{private,public}.pem) + for count in 1 2 3 4 5 ; do + ${KEYGEN} --keyfile ${PDO_HOME}/keys/token_holder${count} --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/token_holder${count}_{private,public}.pem) + done +fi + +if [ ! -f ${PDO_HOME}/keys/guardian_service.pem ]; then + yell create keys for the guardian service + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_service --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_service.pem) + + ${KEYGEN} --keyfile ${PDO_HOME}/keys/guardian_sservice --format pem + F_KEY_FILES+=(${PDO_HOME}/keys/guardian_sservice.pem) +fi + +# ----------------------------------------------------------------- +function cleanup { + rm -f ${F_SERVICE_GROUPS_DB_FILE} ${F_SERVICE_GROUPS_DB_FILE}-lock + rm -f ${F_SERVICE_DB_FILE} ${F_SERVICE_DB_FILE}-lock + rm -f ${F_CONTEXT_FILE} + for key_file in ${F_KEY_FILES[@]} ; do + rm -f ${key_file} + done + + yell "shutdown guardian and storage service" + ${PDO_HOME}/contracts/medperf/scripts/gs_stop.sh + ${PDO_HOME}/contracts/medperf/scripts/ss_stop.sh +} + +trap cleanup EXIT + +# ----------------------------------------------------------------- +# Start the guardian service and the storage service +# ----------------------------------------------------------------- +try ${PDO_HOME}/contracts/medperf/scripts/ss_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_sservice + +sleep 3 + +try ${PDO_HOME}/contracts/medperf/scripts/gs_start.sh -c -o ${PDO_HOME}/logs -- \ + --loglevel debug \ + --config guardian_service.toml \ + --config-dir ${PDO_HOME}/etc/contracts \ + --identity guardian_service \ + --bind host ${F_GUARDIAN_HOST} \ + --bind service_host ${F_SERVICE_HOST} + +# ----------------------------------------------------------------- +# create the service and groups databases from a site file; the site +# file is assumed to exist in ${PDO_HOME}/etc/sites/${SERVICE_HOST}.toml +# +# by default, the groups will include all available services from the +# service host +# ----------------------------------------------------------------- +yell create the service and groups database for host ${F_SERVICE_HOST} +try pdo-service-db import ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} +try pdo-eservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-pservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default +try pdo-sservice create_from_site ${SHORT_OPTS} --file ${F_SERVICE_SITE_FILE} --group default \ + --replicas 1 --duration 60 + +# ----------------------------------------------------------------- +# setup the contexts that will be used later for the tests, check this +# ----------------------------------------------------------------- +cd "${SOURCE_ROOT}" + +rm -f ${CONTEXT_FILE} + +try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/tokens.toml \ + --bind token test1 --bind url http://${F_GUARDIAN_HOST}:7900 + + +yell =========================================================================== +yell Start the tests for PDO and MedPerf +yell =========================================================================== + +GREEN='\033[0;32m' +BLUE='\033[0;34m' +YELLOW='\033[0;33m' +CYAN='\033[0;36m' +RED='\033[0;31m' +RESET='\033[0m' + +read -p "Press enter to continue >>" + +echo -e "${GREEN}===========================${RESET}" +echo -e "${GREEN}=== Preparing MedPerf ===${RESET}" +echo -e "${GREEN}===========================${RESET}" + +medperf_home="/home/wenyi1/medperf" +# deactivate virtual environment +deactivate + +source "/home/wenyi1/medperf/venv/bin/activate" +cd ${medperf_home} +rm -fr medperf_tutorial +cd server +sh reset_db.sh +rm -fr ~/.medperf/localhost_8000 +python seed.py --demo data +cd .. +sh tutorials_scripts/setup_data_tutorial.sh + +medperf dataset submit \ + --name "mytestdata" \ + --description "A tutorial dataset" \ + --location "My machine" \ + --data_path "medperf_tutorial/sample_raw_data/images" \ + --labels_path "medperf_tutorial/sample_raw_data/labels" \ + --benchmark 1 + + + +medperf dataset prepare --data_uid 1 +echo -e "${GREEN}Putting some dummy models assosciated with experiment..." +read -p "Press enter to continue >>" +python record_writer/fake_model.py + + + +echo -e "${RED}======================================================${RESET}" +echo -e "${RED}The next commmand will set the dataset as operational.${RESET}" +echo -e "${RED}This is a good time to mint a token for the dataset.${RESET}" +echo -e "${RED}======================================================${RESET}" +read -p "Press enter to continue >>" + +medperf dataset set_operational --data_uid 1 + +deactivate +source ${PDO_INSTALL_ROOT}'/bin/activate' + +# mint token here +yell create a token issuer and mint the tokens. This token does not have any policy +try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer +try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_object \ + --dataset_id "mytestdata" + + +deactivate +source "/home/wenyi1/medperf/venv/bin/activate" + +echo -e "${RED}=======================================================================${RESET}" +echo -e "${RED}The next command will associate the dataset with experiment.${RESET}" +echo -e "${RED}This is a good time to update the token policy for the experiment.${RESET}" +echo -e "${RED}The policy specifies the dataowner only allows 2 models to be evaluated${RESET}" +echo -e "${RED}========================================================================${RESET}" +read -p "Press enter to continue >>" + +medperf dataset associate --benchmark_uid 1 --data_uid 1 + +deactivate +source ${PDO_INSTALL_ROOT}'/bin/activate' + +# update token policy here +yell the owner of the dataset token updates the policy by adding an experiment id and a list of associated models with the experiment id +yell the experiment contains 10 models, but the dataset can be used only 5 times +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --dataset_id "mytestdata" \ + --experiment_id "chestxray" \ + --associated_model_ids "10" \ + --max_use_count 5 + +# check token policy here +# yell get dataset and policy info from the token object +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + + + +echo -e "${RED}============================================================${RESET}" +echo -e "${RED}The dataowner can transfer the token to experiment committee${RESET}" +echo -e "${RED}============================================================${RESET}" +read -p "Press enter to continue >>" + +# transfer token here +yell the owner transfers the token to token_holder1 +try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 + +echo -e "${RED} Update policy is not allowed from the experiment committee${RESET}" +# update token policy here +yell the experiment committee tries to update the policy to a maximum 10 uese, but fails +medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "chestxray" \ + --dataset_id "mytestdata" \ + --associated_model_ids "10" \ + --max_use_count 10 \ + --identity token_holder1 + +echo -e "${RED} Update policy is not allowed from the experiment committee${RESET}" +yell the token_issuer tries to update the policy to a maximum 2 uses +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "chestxray" \ + --dataset_id "mytestdata" \ + --associated_model_ids "10" \ + --max_use_count 2 + +yell Check the new policy +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + +echo -e "${RED}============================================================${RESET}" +echo -e "${RED} Experiment committee can choose which 2 models to use.${RESET}" +echo -e "${RED} These models are marked scheduled in PDO contract. ${RESET}" +echo -e "${RED}============================================================${RESET}" +read -p "Press enter to continue >>" + +yell Try to use model 3, it works +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '3' +# out of range not allowed + +read -p "Press enter to continue >>" +yell Try use a model 15 that is not associated with the experiment, but fails +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '15' + +# two +read -p "Press enter to continue >>" +yell Try to use model 5, it works +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '1' + +# three not allowed +read -p "Press enter to continue >>" +yell Try to use model 4, it fails because the maximum use count is 2 +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '4' + +read -p "Press enter to continue >>" +yell check the token policy status again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + +echo -e "${RED}============================================================${RESET}" +echo -e "${RED}The experiment committee can pack all scheduled models into ${RESET}" +echo -e "${RED}a single work order send it to the medperf server.${RESET}" +echo -e "${RED}============================================================${RESET}" +read -p "Press enter to continue >>" + +yell Experiment Committee gets a packed workorder and submit to the MedPerf server +medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + + + +yell Check the token policy status again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + + +read -p "Press enter to quit" + +exit + + + + + + + + + + + + + + + + + + +# # yell get dataset and policy info from the token object +# # try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + +# yell the owner of the token updates the policy by adding an experiment id and a list of associated models with the experiment id +# yell the experiment contains 5 models, but the dataset can be used only 5 times +# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ +# --dataset_id "test_dataset_1" \ +# --experiment_id "test_experiment_1" \ +# --associated_model_ids "5" \ +# --max_use_count 5 + +# yell get dataset and policy info from the token object +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + + +# yell the owner transfers the token to token_holder1 +# try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ +# --new-owner token_holder1 + +# yell the token_holder1 tries to update the policy to a maximum 10 uese, but fails +# medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ +# --experiment_id "test_experiment_1" \ +# --dataset_id "test_dataset_1" \ +# --associated_model_ids "10" \ +# --max_use_count 10 \ +# --identity token_holder1 + +# yell the token_issuer tries to update the policy to a maximum 2 uses +# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ +# --experiment_id "test_experiment_1" \ +# --dataset_id "test_dataset_1" \ +# --associated_model_ids "10" \ +# --max_use_count 2 + +# yell the token_holder1 tries to check the policy again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + + +# yell the token_holder1 tries to use the dataset with the first models, this time works +# medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ +# --model_ids_to_evaluate '3' + +# yell check the policy again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + + +# yell the token_holder1 tries to use a model that is not associated with the experiment, but fails +# medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ +# --model_ids_to_evaluate '15' + +# yell the token_holder1 tries to use the dataset with the second model, this time works +# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ +# --model_ids_to_evaluate '5' + +# # yell the token_holder1 tries to use the dataset with the third model, this time fails +# # try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# # --identity token_holder1 \ +# # --dataset_id "test_dataset_1" \ +# # --model_ids_to_evaluate '4' + +# # yell check the policy again +# # try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# # --identity token_holder1 + +# yell get experiment order and submit to the guardian service +# try medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ + +# yell check the policy again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + +# # yell a test call of the hello world method in the contract +# # try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +# # yell use the dataset for a model, this is the first usage prior to transfer +# # try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 +# # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' + +# # yell a test call of the hello world method in the contract +# # try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +# read -p "Press enter to quit" + +# exit diff --git a/medperf-contract/test/script_test.sh b/medperf-contract/test/script_test.sh index c0c6c96..5e1b3aa 100755 --- a/medperf-contract/test/script_test.sh +++ b/medperf-contract/test/script_test.sh @@ -194,38 +194,102 @@ try pdo-context load ${OPTS} --import-file ${F_CONTEXT_TEMPLATES}/tokens.toml \ # start the tests # ----------------------------------------------------------------- -yell create a token issuer and mint the tokens. Token configured so that asset can be used at most 3 times. +yell create a token issuer and mint the tokens. This token does not have any policy try ex_token_issuer create ${OPTS} --contract token.test1.token_issuer try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_object \ - --dataset_id "test_dataset_1" \ - --experiment_id "test_experiment_1" \ - --associated_model_ids "model_1" \ - --max_use_count 3 + --dataset_id "test_dataset_1" + +# yell get dataset and policy info from the token object +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +yell the owner of the token updates the policy by adding an experiment id and a list of associated models with the experiment id +yell the experiment contains 5 models, but the dataset can be used only 5 times +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --dataset_id "test_dataset_1" \ + --experiment_id "test_experiment_1" \ + --associated_model_ids "5" \ + --max_use_count 5 -yell get dataset info from the token object +yell get dataset and policy info from the token object try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ -yell a test call of the hello world method in the contract -try medperf_token hello_world ${OPTS} --contract token.test1.token_object.token_1 -yell use the dataset for a model, this is the first usage prior to transfer -try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 - # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' +yell the owner transfers the token to token_holder1 +try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ + --new-owner token_holder1 -# yell a test call of the hello world method in the contract -# try medperf_token hello_world ${OPTS} --contract token.test1.token_object.token_1 +yell the token_holder1 tries to update the policy to a maximum 10 uese, but fails +medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "test_experiment_1" \ + --dataset_id "test_dataset_1" \ + --associated_model_ids "10" \ + --max_use_count 10 \ + --identity token_holder1 -yell transfer the token to token_holder1 , only one more use permitted by the new owner -try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ - --new-owner token_holder1 +yell the token_issuer tries to update the policy to a maximum 2 uses +try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ + --experiment_id "test_experiment_1" \ + --dataset_id "test_dataset_1" \ + --associated_model_ids "10" \ + --max_use_count 2 + +yell the token_holder1 tries to check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + + +yell the token_holder1 tries to use the dataset with the first models, this time works +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '3' + +yell check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 -yell new owner uses the model, this is the second and last usage of the asset -try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ - # --user_inputs '{"inputs": "Do you know me ?"}' --identity token_holder1 -yell new owner attempts 3rd usage, and this should fail +yell the token_holder1 tries to use a model that is not associated with the experiment, but fails +medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '15' + +yell the token_holder1 tries to use the dataset with the second model, this time works try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ - # --user_inputs '{"inputs": "Am I lucky today ?"}' --identity token_holder1 + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + --model_ids_to_evaluate '5' + +# yell the token_holder1 tries to use the dataset with the third model, this time fails +# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 \ +# --dataset_id "test_dataset_1" \ +# --model_ids_to_evaluate '4' + +# yell check the policy again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 + +yell get experiment order and submit to the guardian service +try medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "test_dataset_1" \ + +yell check the policy again +try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 + +# yell a test call of the hello world method in the contract +# try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +# yell use the dataset for a model, this is the first usage prior to transfer +# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 + # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' + +# yell a test call of the hello world method in the contract +# try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 + +read -p "Press enter to quit" exit From f741b85ad10d6d181cdcaef763095c36554c0cb0 Mon Sep 17 00:00:00 2001 From: Wenyi Tang Date: Wed, 28 Aug 2024 15:10:28 +0000 Subject: [PATCH 3/3] small update for pr --- .gitignore | 4 +- medperf-contract/PROTOCOL.md | 124 +++++++++ medperf-contract/README.md | 130 +-------- medperf-contract/easy_test.sh | 6 + .../pdo/medperf/operations/use_dataset.py | 41 +-- .../medperf/plugins/medperf_token_object.py | 6 +- medperf-contract/test/mp_test.sh | 249 ++++++------------ 7 files changed, 252 insertions(+), 308 deletions(-) create mode 100644 medperf-contract/PROTOCOL.md create mode 100755 medperf-contract/easy_test.sh diff --git a/.gitignore b/.gitignore index ffb6d7e..5953e05 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,6 @@ dist *.egg-info *.ipynb_checkpoints *.pyc -quick_*.sh \ No newline at end of file +quick_*.sh +settings.sh +hfmodels-contract/ \ No newline at end of file diff --git a/medperf-contract/PROTOCOL.md b/medperf-contract/PROTOCOL.md new file mode 100644 index 0000000..fba77fa --- /dev/null +++ b/medperf-contract/PROTOCOL.md @@ -0,0 +1,124 @@ +## Workflow + +```mermaid +sequenceDiagram + participant E as Experiment Committee + participant PDO as PDO Contract (SGX) + participant G as Digital Guardian Service + participant D as Data Owner + participant M as MedPerf Server + %% participant MO as Model Owner + note over G, M: experiment-data association (assume finished) + note over G, M: model-data association (assume finished) + note over M, PDO: assume the states data are synchronized + loop Attestation + PDO -> G: + end + D ->> PDO: create token_issuer and call cmd_mint_token + PDO ->> PDO: generate new identity token_issuer and new contract token.token_object + D ->> PDO: transfer token.token_object to new owner token_experitment1 + PDO ->> PDO: generate new identity token_expertiment1 and new contract token.token_experiment1 + note over E, PDO: owership transferred to Experiment committee + note over M: notification about token/dataset availiability + box Assume Trusted + participant M + end + box SGX Enclave + participant G + end + E ->> PDO: request the inference on specific model model_id, experiment_id (cmd_use_dataset) + activate PDO + PDO ->> PDO: policy evaluation + note right of PDO: policy:
experiment_id = experiment_id'
model_id in {model_id} + PDO ->> E: return capabilities, encoded with urls for dockerfile and weights of the granted model.identity of + deactivate PDO + E ->> G: request access to service with capability (storage ) + E ->> D: + activate G + note over G: pull/download/deploy mlcube (the model) + G ->> G: run inference over the dataset + G ->> D: return result (basic verification with hash) + D ->> E: return results + deactivate G +``` + + +## Protocols + +From the dataset owner side: ++ The dataset is identified by its hash. ++ Tokens are minted based on hash-based binding. ++ Guardian service of the token exposes an api to the callers + + +## Core functions + ++ Allow data owner to tokenize a dataset (into a PDO contract) + + with default policy checking the registration information from MedPerf + + ownership transfer to experiment committee + + generate capability to grant access to the guardian service + ++ Allow experiment committee to initilize the use of data by interacting with PDO + + capabilities are published on MedPerf server (or PDO states/ledger?) for downloading + ++ Allow data owner to host a guardian service, which exposes interface (local) to initilize the test + + data owner downloads the capability from the server (or ledger) + + data owner feeds the capability to guardian service to initialize the experiment + + guardian service publish experiment results to the server (or ledger) + + allows access control? + + + +### Guardian service + +Datasets are hosted behind the service. + + + + +The guardian service is co-located with the dataset. guadian service provides a wsgi api to process the capability. Capability encodes `{model_id, dataset_id, url_to_docker, url_to_weights}`. After receiving capability, the guardian service: +1. pull/build docker images from `url_to_docker` +2. download weights from `url_to_weights` +3. model up and run over `dataset_id` + +no execution integrity for now. + + + + +### Contract methods + +new contract methods under the class `ww::medperf::token_object` + +`initialize`: public methode, mint token for the dataset, actual method behind `cmd_mint_token` + 1. kvs of the experiment/model/dataset info + 2. Store the registered metadata from medperf service (synthetic for PoC). + 3. set a `max_evaluation` + +| Keys | Values | +| ---- | ---- | +| experiment_id | identifier for the experimetn | +| model_id | identifier for the model | +| {urls} | url to model assets | +| dataset_id | hash of the dataset | +| max_evaluation | most models that allowed to evaluate | +| cur_evaluation | 0 | +| approved_capability| {} | +| TBA | + +`get_datasetinfo`: public method, get the non-secret kvs info of dataset token +return the information associated with the dataset. + +capability -links to- identity // invoked by the dataowner only + + + +`use_dataset(model_id, dataset_id, experiment_id)`: only invoked by TO + 1. check if model_id, dataset_id, experiment_id are in the kv storage + 2. cur_evaluation + 1, if cur_evaluation = max_evaluation, fail + 3. invoke `get_capability` and return secretly encoded {urls}. + +allows multiple models in one call + +`get_capability`: only invoked by TO, parse capability kv \ No newline at end of file diff --git a/medperf-contract/README.md b/medperf-contract/README.md index 0deeb68..ae03f51 100644 --- a/medperf-contract/README.md +++ b/medperf-contract/README.md @@ -10,130 +10,14 @@ This document includes an enhanced workflow for Federated Evaluation (FE) in mac The initial version of this project only covers the protection of dataset. Other digital assets, including the model (weights)and experiment results are not included for now. -## Testing Workflow -```mermaid -sequenceDiagram - participant E as Experiment Committee - participant PDO as PDO Contract (SGX) - participant G as Digital Guardian Service - participant D as Data Owner - participant M as MedPerf Server - %% participant MO as Model Owner - note over G, M: experiment-data association (assume finished) - note over G, M: model-data association (assume finished) - note over M, PDO: assume the states data are synchronized - loop Attestation - PDO -> G: - end - D ->> PDO: create token_issuer and call cmd_mint_token - PDO ->> PDO: generate new identity token_issuer and new contract token.token_object - D ->> PDO: transfer token.token_object to new owner token_experitment1 - PDO ->> PDO: generate new identity token_expertiment1 and new contract token.token_experiment1 - note over E, PDO: owership transferred to Experiment committee - note over M: notification about token/dataset availiability - box Assume Trusted - participant M - end - box SGX Enclave - participant G - end - E ->> PDO: request the inference on specific model model_id, experiment_id (cmd_use_dataset) - activate PDO - PDO ->> PDO: policy evaluation - note right of PDO: policy:
experiment_id = experiment_id'
model_id in {model_id} - PDO ->> E: return capabilities, encoded with urls for dockerfile and weights of the granted model.identity of - deactivate PDO - E ->> G: request access to service with capability (storage ) - E ->> D: - activate G - note over G: pull/download/deploy mlcube (the model) - G ->> G: run inference over the dataset - G ->> D: return result (basic verification with hash) - D ->> E: return results - deactivate G -``` +## Test with the MedPerf tutorial +The existing test simulates the pdo-enhanced workflow by running both components simultaneously. The pdo command hacks into the medperf database directly to simulate the information updates. +Set up PDO test environment and have the [MedPerf](https://github.com/mlcommons/medperf) installed and built. Then set up environment variables `MEDPERF_SQLITE_PATH`, `MEDPERF_VENV_PATH` and `MEDPERF_HOME`. -## Protocols +Simply run [easy_test.sh](./easy_test.sh) to start the test. -From the dataset owner side: -+ The dataset is identified by its hash. -+ Tokens are minted based on hash-based binding. -+ Guardian service of the token exposes an api to the callers - - -## Core functions - -+ Allow data owner to tokenize a dataset (into a PDO contract) - + with default policy checking the registration information from MedPerf - + ownership transfer to experiment committee - + generate capability to grant access to the guardian service - -+ Allow experiment committee to initilize the use of data by interacting with PDO - + capabilities are published on MedPerf server (or PDO states/ledger?) for downloading - -+ Allow data owner to host a guardian service, which exposes interface (local) to initilize the test - + data owner downloads the capability from the server (or ledger) - + data owner feeds the capability to guardian service to initialize the experiment - + guardian service publish experiment results to the server (or ledger) - + allows access control? - - - -### Guardian service - -Datasets are hosted behind the service. - - - - -The guardian service is co-located with the dataset. guadian service provides a wsgi api to process the capability. Capability encodes `{model_id, dataset_id, url_to_docker, url_to_weights}`. After receiving capability, the guardian service: -1. pull/build docker images from `url_to_docker` -2. download weights from `url_to_weights` -3. model up and run over `dataset_id` - -no execution integrity for now. - - - - -### Contract methods - -new contract methods under the class `ww::medperf::token_object` - -`initialize`: public methode, mint token for the dataset, actual method behind `cmd_mint_token` - 1. kvs of the experiment/model/dataset info - 2. Store the registered metadata from medperf service (synthetic for PoC). - 3. set a `max_evaluation` - -| Keys | Values | -| ---- | ---- | -| experiment_id | identifier for the experimetn | -| model_id | identifier for the model | -| {urls} | url to model assets | -| dataset_id | hash of the dataset | -| max_evaluation | most models that allowed to evaluate | -| cur_evaluation | 0 | -| approved_capability| {} | -| TBA | - -`get_datasetinfo`: public method, get the non-secret kvs info of dataset token -return the information associated with the dataset. - -capability -links to- identity // invoked by the dataowner only - - - -`use_dataset(model_id, dataset_id, experiment_id)`: only invoked by TO - 1. check if model_id, dataset_id, experiment_id are in the kv storage - 2. cur_evaluation + 1, if cur_evaluation = max_evaluation, fail - 3. invoke `get_capability` and return secretly encoded {urls}. - -allows multiple models in one call - -`get_capability`: only invoked by TO, parse capability kv @@ -144,11 +28,13 @@ ww::medperf::token_object::get ``` --> -## Testing cmd +## Testing workflow + create token issuer and mint the token for the dataset. + token issuer transfers token to model_owner1 + model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` + get capability for one model, increase the count + call service to run inference + model_owner1 invoke `cmd_use_dataset` with specific `model_id` and `dataset_id` - + max_evaluation exceeds, fail \ No newline at end of file + + max_evaluation exceeds, fail + +For detailed description of the whole protocol, check [here](./PROTOCOL.md). \ No newline at end of file diff --git a/medperf-contract/easy_test.sh b/medperf-contract/easy_test.sh new file mode 100755 index 0000000..af361cb --- /dev/null +++ b/medperf-contract/easy_test.sh @@ -0,0 +1,6 @@ +source ${PDO_SOURCE_ROOT}/build/common-config.sh +source ${PDO_INSTALL_ROOT}'/bin/activate' +make -C ${PDO_CONTRACTS_ROOT} +make -C ${PDO_CONTRACTS_ROOT} install +$PDO_CONTRACTS_ROOT/medperf-contract/test/mp_test.sh --host $PDO_HOSTNAME --ledger $PDO_LEDGER_URL +deactivate diff --git a/medperf-contract/pdo/medperf/operations/use_dataset.py b/medperf-contract/pdo/medperf/operations/use_dataset.py index b277240..658c977 100644 --- a/medperf-contract/pdo/medperf/operations/use_dataset.py +++ b/medperf-contract/pdo/medperf/operations/use_dataset.py @@ -95,12 +95,18 @@ def __call__(self, params) : # get the parameters kvstore_encryption_key = params['kvstore_encryption_key'] kvstore_root_block_hash = params['kvstore_root_block_hash'] + # used as a challenge to sign the result for now kvstore_input_key = params['kvstore_input_key'] dataset_id = params['dataset_id'] model_ids_to_evaluate = params['model_ids_to_evaluate'] + result_path = "~/.medperf_test_results" + # if result_path does not exist, create it + if not os.path.exists(result_path): + os.makedirs(result_path) - mlcube_script_path = "/home/wenyi1/medperf/test_resource/test.sh" - result_path = "/home/wenyi1/medperf/test_resource/test_results" + # path issue, turned off for now + # mlcube_script_path = "~/test_resource/test.sh" + test_message = "Hello from the guardian service." # put the parameters in test_message and return @@ -116,20 +122,21 @@ def __call__(self, params) : model_ids = [int(i) for i in model_ids] for model in model_ids: - if model == 1 or model == 2: - print(f"Cubes available for model {model}, running the MLCube script...") - # run the MLCube script with parameter model - # os.system(f"bash {mlcube_script_path} {model}") - output_result = subprocess.run(["bash", mlcube_script_path, str(model)], capture_output=True, text=True) - print(output_result.stdout) - else: - print(f"Model {model} not available. Generating simulated data...") - # generate simulated data - sim_data = {'AUC': random.uniform(0.5, 1), 'Accuracy': random.uniform(0.5, 1)} - with open(f"{result_path}/{str(model)}.yaml", 'w') as file: - yaml.dump(sim_data, file) - print(f"Simulated data for model {model} generated successfully.") - + # path issue, turned off for now + # if model == 1 or model == 2: + # print(f"Cubes available for model {model}, running the MLCube script...") + # # run the MLCube script with parameter model + # # os.system(f"bash {mlcube_script_path} {model}") + # output_result = subprocess.run(["bash", mlcube_script_path, str(model)], capture_output=True, text=True) + # print(output_result.stdout) + # else: + print(f"Model {model} not available. Generating simulated data...") + # generate simulated data + sim_data = {'AUC': random.uniform(0.5, 1), 'Accuracy': random.uniform(0.5, 1)} + with open(f"{result_path}/{str(model)}.yaml", 'w') as file: + yaml.dump(sim_data, file) + print(f"Simulated data for model {model} generated successfully.") + print("================================================") print("Signing each file with simple SHA256 and challenge...") # handle each model result @@ -138,7 +145,7 @@ def __call__(self, params) : with open(f"{result_path}/{str(model)}.yaml", 'r+') as file: result_data = yaml.safe_load(file) hash_result = self.simpleSHA256withChallenge(kvstore_input_key, result_data) - new_data = {"hash": hash_result} + new_data = {"signature": hash_result} yaml.dump(new_data, file) print(f"Model {model} result signed successfully.") except Exception as e: diff --git a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py index 267586e..77ad9dc 100644 --- a/medperf-contract/pdo/medperf/plugins/medperf_token_object.py +++ b/medperf-contract/pdo/medperf/plugins/medperf_token_object.py @@ -577,6 +577,7 @@ def add_arguments(cls, subparser) : type=str) + @classmethod def invoke(cls, state, context, **kwargs) : save_file = pcontract_cmd.get_contract_from_context(state, context) @@ -595,11 +596,14 @@ def invoke(cls, state, context, **kwargs) : state, context, session, state_attestation['signature'], **kwargs) + + medperf_sqlite_path = kwargs.get('medperf_sqlite_path') try: + # simulating the posting of the order to the MedPerf server data_json = {} data_json["order"] = json.dumps(capability) print(data_json) - conn = sqlite3.connect("/home/wenyi1/medperf/server/db.sqlite3") + conn = sqlite3.connect(medperf_sqlite_path) cur = conn.cursor() # insert new result record to the table for i in range(10): diff --git a/medperf-contract/test/mp_test.sh b/medperf-contract/test/mp_test.sh index 4c78fb6..e16b5fa 100755 --- a/medperf-contract/test/mp_test.sh +++ b/medperf-contract/test/mp_test.sh @@ -19,6 +19,9 @@ : "${PDO_LEDGER_URL?Missing environment variable PDO_LEDGER_URL}" : "${PDO_HOME?Missing environment variable PDO_HOME}" : "${PDO_SOURCE_ROOT?Missing environment variable PDO_SOURCE_ROOT}" +: "${MEDPERF_SQLITE_PATH?Missing environment variable MEDPERF_SQLITE_PATH}" +: "${MEDPERF_VENV_PATH?Missing environment variable MEDPERF_VENV_PATH}" +: "${MEDPERF_HOME?Missing environment variable MEDPERF_HOME}" # ----------------------------------------------------------------- # ----------------------------------------------------------------- source ${PDO_HOME}/bin/lib/common.sh @@ -208,12 +211,10 @@ echo -e "${GREEN}===========================${RESET}" echo -e "${GREEN}=== Preparing MedPerf ===${RESET}" echo -e "${GREEN}===========================${RESET}" -medperf_home="/home/wenyi1/medperf" -# deactivate virtual environment deactivate -source "/home/wenyi1/medperf/venv/bin/activate" -cd ${medperf_home} +source $MEDPERF_VENV_PATH'/bin/activate' +cd ${MEDPERF_HOME} rm -fr medperf_tutorial cd server sh reset_db.sh @@ -236,13 +237,12 @@ medperf dataset prepare --data_uid 1 echo -e "${GREEN}Putting some dummy models assosciated with experiment..." read -p "Press enter to continue >>" python record_writer/fake_model.py +# clear - - -echo -e "${RED}======================================================${RESET}" -echo -e "${RED}The next commmand will set the dataset as operational.${RESET}" -echo -e "${RED}This is a good time to mint a token for the dataset.${RESET}" -echo -e "${RED}======================================================${RESET}" +echo -e "${YELLOW}======================================================${RESET}" +echo -e "${YELLOW}The next commmand will set the dataset as operational.${RESET}" +echo -e "${YELLOW}This is a good time to mint a token for the dataset.${RESET}" +echo -e "${YELLOW}======================================================${RESET}" read -p "Press enter to continue >>" medperf dataset set_operational --data_uid 1 @@ -258,46 +258,49 @@ try medperf_token mint_dataset_tokens ${OPTS} --contract token.test1.token_objec deactivate -source "/home/wenyi1/medperf/venv/bin/activate" +source $MEDPERF_VENV_PATH'/bin/activate' + +clear -echo -e "${RED}=======================================================================${RESET}" -echo -e "${RED}The next command will associate the dataset with experiment.${RESET}" -echo -e "${RED}This is a good time to update the token policy for the experiment.${RESET}" -echo -e "${RED}The policy specifies the dataowner only allows 2 models to be evaluated${RESET}" -echo -e "${RED}========================================================================${RESET}" +echo -e "${YELLOW}=======================================================================${RESET}" +echo -e "${YELLOW}The next command will associate the dataset with experiment.${RESET}" +echo -e "${YELLOW}This is a good time to update the token policy for the dataset.${RESET}" +echo -e "${YELLOW}The experiment is associated with 10 models marked from 1-10 ${RESET}" +echo -e "${YELLOW}The policy specifies the dataowner only allows 3 models to be evaluated${RESET}" +echo -e "${YELLOW}========================================================================${RESET}" read -p "Press enter to continue >>" -medperf dataset associate --benchmark_uid 1 --data_uid 1 +medperf dataset associate --benchmark_uid 1 --data_uid 1 > /dev/null deactivate source ${PDO_INSTALL_ROOT}'/bin/activate' # update token policy here -yell the owner of the dataset token updates the policy by adding an experiment id and a list of associated models with the experiment id -yell the experiment contains 10 models, but the dataset can be used only 5 times +yell the owner of the dataset token updates the policy by adding an experiment id and a list of associated models +yell the experiment contains 10 models, but the dataset can be used only 3 times try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ --dataset_id "mytestdata" \ --experiment_id "chestxray" \ --associated_model_ids "10" \ - --max_use_count 5 + --max_use_count 3 # check token policy here # yell get dataset and policy info from the token object # try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# clear - -echo -e "${RED}============================================================${RESET}" -echo -e "${RED}The dataowner can transfer the token to experiment committee${RESET}" -echo -e "${RED}============================================================${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The dataowner can transfer the token to experiment committee${RESET}" +echo -e "${YELLOW}============================================================${RESET}" read -p "Press enter to continue >>" # transfer token here -yell the owner transfers the token to token_holder1 +yell the owner transfers the token to token_holder1 '(experiment committee)' try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ --new-owner token_holder1 -echo -e "${RED} Update policy is not allowed from the experiment committee${RESET}" +# echo -e "${YELLOW} Update policy is not allowed from the experiment committee${RESET}" # update token policy here yell the experiment committee tries to update the policy to a maximum 10 uese, but fails medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ @@ -307,23 +310,25 @@ medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 --max_use_count 10 \ --identity token_holder1 -echo -e "${RED} Update policy is not allowed from the experiment committee${RESET}" -yell the token_issuer tries to update the policy to a maximum 2 uses -try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ - --experiment_id "chestxray" \ - --dataset_id "mytestdata" \ - --associated_model_ids "10" \ - --max_use_count 2 +# echo -e "${YELLOW} Update policy is not allowed from the experiment committee${RESET}" +# yell the token_issuer tries to update the policy to a maximum 2 uses +# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ +# --experiment_id "chestxray" \ +# --dataset_id "mytestdata" \ +# --associated_model_ids "10" \ +# --max_use_count 2 -yell Check the new policy -try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ - --identity token_holder1 +# yell Check the new policy +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 -echo -e "${RED}============================================================${RESET}" -echo -e "${RED} Experiment committee can choose which 2 models to use.${RESET}" -echo -e "${RED} These models are marked scheduled in PDO contract. ${RESET}" -echo -e "${RED}============================================================${RESET}" -read -p "Press enter to continue >>" + +# clear +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW} Experiment committee can choose which models to use.${RESET}" +echo -e "${YELLOW} These models are marked scheduled in PDO contract. ${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +# read -p "Press enter to continue >>" yell Try to use model 3, it works medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ @@ -332,7 +337,7 @@ medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ --model_ids_to_evaluate '3' # out of range not allowed -read -p "Press enter to continue >>" +# read -p "Press enter to continue >>" yell Try use a model 15 that is not associated with the experiment, but fails medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ --identity token_holder1 \ @@ -340,36 +345,45 @@ medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ --model_ids_to_evaluate '15' # two -read -p "Press enter to continue >>" +# read -p "Press enter to continue >>" +yell Try to use model 4, it works +try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ + --identity token_holder1 \ + --dataset_id "mytestdata" \ + --model_ids_to_evaluate '4' + yell Try to use model 5, it works try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ --identity token_holder1 \ --dataset_id "mytestdata" \ - --model_ids_to_evaluate '1' + --model_ids_to_evaluate '5' # three not allowed -read -p "Press enter to continue >>" -yell Try to use model 4, it fails because the maximum use count is 2 +# read -p "Press enter to continue >>" +yell Try to use model 6, it fails because the maximum use count is 3 try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ --identity token_holder1 \ --dataset_id "mytestdata" \ - --model_ids_to_evaluate '4' + --model_ids_to_evaluate '6' -read -p "Press enter to continue >>" -yell check the token policy status again -try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ - --identity token_holder1 +# read -p "Press enter to continue >>" +# yell check the token policy status again +# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ +# --identity token_holder1 -echo -e "${RED}============================================================${RESET}" -echo -e "${RED}The experiment committee can pack all scheduled models into ${RESET}" -echo -e "${RED}a single work order send it to the medperf server.${RESET}" -echo -e "${RED}============================================================${RESET}" +read -p "Press enter to continue >>" +# clear +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The experiment committee can pack all scheduled models into ${RESET}" +echo -e "${YELLOW}a single experiment workorder send it to the medperf server.${RESET}" +echo -e "${YELLOW}============================================================${RESET}" read -p "Press enter to continue >>" yell Experiment Committee gets a packed workorder and submit to the MedPerf server medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ --identity token_holder1 \ --dataset_id "mytestdata" \ + --medperf_sqlite_path @@ -377,119 +391,20 @@ yell Check the token policy status again try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ --identity token_holder1 - +read -p "Press enter to check the results >>" + +echo -e "${YELLOW}============================================================${RESET}" +echo -e "${YELLOW}The results of the experiment look like this.${RESET}" +echo -e "${YELLOW}============================================================${RESET}" +# +for i in {3,4,5}; +do + # echo -e "${YELLOW}============================================================${RESET}" + echo -e "${YELLOW}Model $i${RESET}" + # echo -e "${YELLOW}============================================================${RESET}" + cat $MEDPERF_HOME/test_resource/test_results/$i* +done read -p "Press enter to quit" exit - - - - - - - - - - - - - - - - - -# # yell get dataset and policy info from the token object -# # try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ - -# yell the owner of the token updates the policy by adding an experiment id and a list of associated models with the experiment id -# yell the experiment contains 5 models, but the dataset can be used only 5 times -# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ -# --dataset_id "test_dataset_1" \ -# --experiment_id "test_experiment_1" \ -# --associated_model_ids "5" \ -# --max_use_count 5 - -# yell get dataset and policy info from the token object -# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ - - -# yell the owner transfers the token to token_holder1 -# try medperf_token transfer ${OPTS} --contract token.test1.token_object.token_1 \ -# --new-owner token_holder1 - -# yell the token_holder1 tries to update the policy to a maximum 10 uese, but fails -# medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ -# --experiment_id "test_experiment_1" \ -# --dataset_id "test_dataset_1" \ -# --associated_model_ids "10" \ -# --max_use_count 10 \ -# --identity token_holder1 - -# yell the token_issuer tries to update the policy to a maximum 2 uses -# try medperf_token update_policy ${OPTS} --contract token.test1.token_object.token_1 \ -# --experiment_id "test_experiment_1" \ -# --dataset_id "test_dataset_1" \ -# --associated_model_ids "10" \ -# --max_use_count 2 - -# yell the token_holder1 tries to check the policy again -# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 - - -# yell the token_holder1 tries to use the dataset with the first models, this time works -# medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 \ -# --dataset_id "test_dataset_1" \ -# --model_ids_to_evaluate '3' - -# yell check the policy again -# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 - - -# yell the token_holder1 tries to use a model that is not associated with the experiment, but fails -# medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 \ -# --dataset_id "test_dataset_1" \ -# --model_ids_to_evaluate '15' - -# yell the token_holder1 tries to use the dataset with the second model, this time works -# try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 \ -# --dataset_id "test_dataset_1" \ -# --model_ids_to_evaluate '5' - -# # yell the token_holder1 tries to use the dataset with the third model, this time fails -# # try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 \ -# # --identity token_holder1 \ -# # --dataset_id "test_dataset_1" \ -# # --model_ids_to_evaluate '4' - -# # yell check the policy again -# # try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ -# # --identity token_holder1 - -# yell get experiment order and submit to the guardian service -# try medperf_token experiment_order ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 \ -# --dataset_id "test_dataset_1" \ - -# yell check the policy again -# try medperf_token get_dataset_info ${OPTS} --contract token.test1.token_object.token_1 \ -# --identity token_holder1 - -# # yell a test call of the hello world method in the contract -# # try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 - -# # yell use the dataset for a model, this is the first usage prior to transfer -# # try medperf_token use_dataset ${OPTS} --contract token.test1.token_object.token_1 -# # --user_inputs '{"inputs": "Can you please let us know more details about yourself ?"}' - -# # yell a test call of the hello world method in the contract -# # try medperf_token owner_test ${OPTS} --contract token.test1.token_object.token_1 - -# read -p "Press enter to quit" - -# exit