From 7b43936e9d5db7ffb8f53e147470bc437c9c5104 Mon Sep 17 00:00:00 2001 From: Zhouhui Tian <125243011+zhouhuitian@users.noreply.github.com> Date: Wed, 31 Jan 2024 18:34:42 +0800 Subject: [PATCH 01/64] add $ prefix (#2430) Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- .../litentry/core/credentials/src/achainable/bab_holder.rs | 2 +- .../core/credentials/src/achainable/lit_holding_amount.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tee-worker/litentry/core/credentials/src/achainable/bab_holder.rs b/tee-worker/litentry/core/credentials/src/achainable/bab_holder.rs index 997d7bfa23..510ea0a1e0 100644 --- a/tee-worker/litentry/core/credentials/src/achainable/bab_holder.rs +++ b/tee-worker/litentry/core/credentials/src/achainable/bab_holder.rs @@ -21,7 +21,7 @@ use crate::{ const BAB_HOLDER_DESCRIPTIONS: &str = "You are a holder of a certain kind of NFT"; const BAB_HOLDER_TYPE: &str = "NFT Holder"; -const BAB_HOLDER_BREAKDOWN: &str = "is_bab_holder"; +const BAB_HOLDER_BREAKDOWN: &str = "$is_bab_holder"; pub trait UpdateBABHolder { fn update_bab_holder(&mut self, is_bab_holder: bool); diff --git a/tee-worker/litentry/core/credentials/src/achainable/lit_holding_amount.rs b/tee-worker/litentry/core/credentials/src/achainable/lit_holding_amount.rs index c311641c9c..18b5f23b2f 100644 --- a/tee-worker/litentry/core/credentials/src/achainable/lit_holding_amount.rs +++ b/tee-worker/litentry/core/credentials/src/achainable/lit_holding_amount.rs @@ -22,7 +22,7 @@ use std::vec::Vec; // Type / Info const LIT_HOLDING_AMOUNT_INFO: (&str, &str) = ("Token holding amount", "The amount of a particular token you are holding"); -const LIT_HOLDING_AMOUNT_BREAKDOWN: &str = "lit_holding_amount"; +const LIT_HOLDING_AMOUNT_BREAKDOWN: &str = "$lit_holding_amount"; const LIT_BALANCE_RANGE: [usize; 10] = [0, 1, 50, 100, 200, 500, 800, 1200, 1600, 3000]; pub struct LitHoldingAmount { From 2f77bf8375dd0e530f1f42976562ae59e82a2da4 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Wed, 31 Jan 2024 15:05:48 +0100 Subject: [PATCH 02/64] feature propagation and parent macros (#2455) --- bitacross-worker/Cargo.lock | 53 ++++++++++++------- bitacross-worker/Cargo.toml | 1 - .../app-libs/parentchain-interface/Cargo.toml | 1 + .../src/integritee/event_handler.rs | 2 +- .../src/target_a/event_handler.rs | 2 +- bitacross-worker/app-libs/stf/Cargo.toml | 4 ++ bitacross-worker/app-libs/stf/src/getter.rs | 3 +- bitacross-worker/app-libs/stf/src/helpers.rs | 34 +++++++----- .../core-primitives/utils/Cargo.toml | 1 + .../core-primitives/utils/src/hex.rs | 46 ++-------------- .../core-primitives/utils/src/lib.rs | 1 - .../core-primitives/utils/src/macros.rs | 35 ------------ bitacross-worker/enclave-runtime/Cargo.lock | 31 ++++++----- bitacross-worker/enclave-runtime/Cargo.toml | 12 ++++- bitacross-worker/enclave-runtime/src/lib.rs | 5 +- .../src/rpc/worker_api_direct.rs | 12 ++--- .../enclave-runtime/src/top_pool_execution.rs | 6 ++- .../litentry/primitives/Cargo.toml | 5 +- .../litentry/primitives/src/lib.rs | 2 +- bitacross-worker/service/Cargo.toml | 9 +++- bitacross-worker/service/src/main_impl.rs | 2 +- bitacross-worker/service/src/teeracle/mod.rs | 2 +- .../sidechain/consensus/aura/Cargo.toml | 1 + .../sidechain/consensus/aura/src/lib.rs | 2 +- 24 files changed, 122 insertions(+), 150 deletions(-) delete mode 100644 bitacross-worker/core-primitives/utils/src/macros.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 9e85f8180c..f2c237e02a 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -735,6 +735,8 @@ dependencies = [ "its-test", "jsonrpsee 0.2.0", "lazy_static", + "litentry-hex-utils", + "litentry-macros", "litentry-primitives", "log 0.4.20", "mockall", @@ -1083,9 +1085,9 @@ dependencies = [ [[package]] name = "cargo_toml" -version = "0.16.3" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3f9629bc6c4388ea699781dc988c2b99766d7679b151c81990b4fa1208fafd3" +checksum = "3dc9f7a067415ab5058020f04c60ec7b557084dbec0e021217bbabc7a8d38d14" dependencies = [ "serde 1.0.193", "toml 0.8.2", @@ -1421,7 +1423,7 @@ version = "0.9.12" dependencies = [ "frame-support", "litentry-hex-utils", - "litentry-macros 0.9.12", + "litentry-macros", "litentry-proc-macros", "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", "parity-scale-codec", @@ -1432,8 +1434,8 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-std 5.0.0", - "strum 0.25.0", - "strum_macros 0.25.3", + "strum 0.26.1", + "strum_macros 0.26.1", ] [[package]] @@ -4632,6 +4634,7 @@ dependencies = [ "itp-types", "itp-utils", "lc-scheduled-enclave", + "litentry-hex-utils", "litentry-primitives", "log 0.4.20", "parity-scale-codec", @@ -4683,6 +4686,7 @@ dependencies = [ "itp-storage", "itp-types", "itp-utils", + "litentry-macros", "litentry-primitives", "log 0.4.20", "pallet-balances", @@ -5558,6 +5562,7 @@ name = "itp-utils" version = "0.9.0" dependencies = [ "hex", + "litentry-hex-utils", "parity-scale-codec", ] @@ -5639,6 +5644,7 @@ dependencies = [ "its-test", "its-validateer-fetch", "lc-scheduled-enclave", + "litentry-hex-utils", "log 0.4.20", "parity-scale-codec", "sgx_tstd", @@ -6857,14 +6863,6 @@ dependencies = [ "hex", ] -[[package]] -name = "litentry-macros" -version = "0.1.0" -dependencies = [ - "cargo_toml", - "quote", -] - [[package]] name = "litentry-macros" version = "0.9.12" @@ -6880,6 +6878,7 @@ dependencies = [ "hex", "itp-sgx-crypto", "itp-utils", + "litentry-hex-utils", "log 0.4.20", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "parity-scale-codec", @@ -6894,8 +6893,8 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-std 5.0.0", - "strum 0.25.0", - "strum_macros 0.25.3", + "strum 0.26.1", + "strum_macros 0.26.1", "teerex-primitives", ] @@ -8252,6 +8251,21 @@ dependencies = [ "libm 0.1.4", ] +[[package]] +name = "pallet-account-fix" +version = "0.1.0" +dependencies = [ + "frame-support", + "frame-system", + "pallet-balances", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "sp-runtime", + "sp-std 5.0.0", +] + [[package]] name = "pallet-asset-manager" version = "0.1.0" @@ -10802,6 +10816,7 @@ dependencies = [ "orml-tokens", "orml-traits", "orml-xtokens", + "pallet-account-fix", "pallet-asset-manager", "pallet-aura", "pallet-authorship", @@ -13809,9 +13824,9 @@ dependencies = [ [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" [[package]] name = "strum_macros" @@ -13828,9 +13843,9 @@ dependencies = [ [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 2ad3682233..40caaaa98d 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -72,7 +72,6 @@ members = [ "sidechain/state", "sidechain/validateer-fetch", "litentry/primitives", - "litentry/macros", ] [patch."https://github.com/apache/teaclave-sgx-sdk.git"] diff --git a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml index 39e4827588..59ec735302 100644 --- a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml +++ b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml @@ -29,6 +29,7 @@ sp-runtime = { default-features = false, git = "https://github.com/paritytech/su # litentry lc-scheduled-enclave = { path = "../../litentry/core/scheduled-enclave", default-features = false, optional = true } +litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } diff --git a/bitacross-worker/app-libs/parentchain-interface/src/integritee/event_handler.rs b/bitacross-worker/app-libs/parentchain-interface/src/integritee/event_handler.rs index 1cc6cd3d0e..c8111afae0 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/integritee/event_handler.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/integritee/event_handler.rs @@ -22,7 +22,7 @@ use ita_stf::{Getter, TrustedCall, TrustedCallSigned}; use itc_parentchain_indirect_calls_executor::error::Error; use itp_stf_primitives::{traits::IndirectExecutor, types::TrustedOperation}; use itp_types::parentchain::{AccountId, FilterEvents, HandleParentchainEvents, ParentchainError}; -use itp_utils::hex::hex_encode; +use litentry_hex_utils::hex_encode; use log::*; pub struct ParentchainEventHandler {} diff --git a/bitacross-worker/app-libs/parentchain-interface/src/target_a/event_handler.rs b/bitacross-worker/app-libs/parentchain-interface/src/target_a/event_handler.rs index 7ea752aa55..cb02df3e2a 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/target_a/event_handler.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/target_a/event_handler.rs @@ -22,7 +22,7 @@ use ita_stf::{Getter, TrustedCall, TrustedCallSigned}; use itc_parentchain_indirect_calls_executor::error::Error; use itp_stf_primitives::{traits::IndirectExecutor, types::TrustedOperation}; use itp_types::parentchain::{AccountId, FilterEvents, HandleParentchainEvents, ParentchainError}; -use itp_utils::hex::hex_encode; +use litentry_hex_utils::hex_encode; use log::*; pub struct ParentchainEventHandler {} diff --git a/bitacross-worker/app-libs/stf/Cargo.toml b/bitacross-worker/app-libs/stf/Cargo.toml index ddec15630a..759b6d7bee 100644 --- a/bitacross-worker/app-libs/stf/Cargo.toml +++ b/bitacross-worker/app-libs/stf/Cargo.toml @@ -40,6 +40,7 @@ sp-std = { default-features = false, git = "https://github.com/paritytech/substr # litentry itp-node-api-metadata-provider = { path = "../../core-primitives/node-api/metadata-provider", default-features = false } +litentry-macros = { path = "../../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } pallet-parentchain = { path = "../../../pallets/parentchain", default-features = false } @@ -87,3 +88,6 @@ std = [ "itp-node-api-metadata-provider/std", ] test = [] +production = [ + "litentry-macros/production", +] diff --git a/bitacross-worker/app-libs/stf/src/getter.rs b/bitacross-worker/app-libs/stf/src/getter.rs index de8d466189..9a4319b962 100644 --- a/bitacross-worker/app-libs/stf/src/getter.rs +++ b/bitacross-worker/app-libs/stf/src/getter.rs @@ -19,7 +19,8 @@ use codec::{Decode, Encode}; use ita_sgx_runtime::System; use itp_stf_interface::ExecuteGetter; use itp_stf_primitives::{traits::GetterAuthorization, types::KeyPair}; -use itp_utils::{if_production_or, stringify::account_id_to_string}; +use itp_utils::stringify::account_id_to_string; +use litentry_macros::if_production_or; use litentry_primitives::{Identity, LitentryMultiSignature}; use log::*; use sp_std::vec; diff --git a/bitacross-worker/app-libs/stf/src/helpers.rs b/bitacross-worker/app-libs/stf/src/helpers.rs index 0c6fd39896..a0d3fa75b9 100644 --- a/bitacross-worker/app-libs/stf/src/helpers.rs +++ b/bitacross-worker/app-libs/stf/src/helpers.rs @@ -17,7 +17,6 @@ use crate::ENCLAVE_ACCOUNT_KEY; use codec::{Decode, Encode}; use frame_support::ensure; -use hex_literal::hex; use itp_stf_primitives::error::{StfError, StfResult}; use itp_storage::{storage_double_map_key, storage_map_key, storage_value_key, StorageHasher}; use itp_types::Index; @@ -25,11 +24,10 @@ use itp_utils::stringify::account_id_to_string; use litentry_primitives::{ErrorDetail, Identity, Web3ValidationData}; use log::*; use sp_core::blake2_256; -use sp_runtime::AccountId32; use std::prelude::v1::*; -pub const ALICE_ACCOUNTID32: AccountId32 = - AccountId32::new(hex!["d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"]); +#[cfg(not(feature = "production"))] +pub use non_prod::*; pub fn get_storage_value( storage_prefix: &'static str, @@ -128,16 +126,6 @@ pub fn ensure_enclave_signer_or_self( } } -#[cfg(not(feature = "production"))] -pub fn ensure_alice(signer: &AccountId32) -> bool { - signer == &ALICE_ACCOUNTID32 -} - -#[cfg(not(feature = "production"))] -pub fn ensure_enclave_signer_or_alice(signer: &AccountId32) -> bool { - signer == &enclave_signer_account::() || ensure_alice(signer) -} - // verification message format: // ``` // blake2_256( + + ) @@ -173,3 +161,21 @@ pub fn verify_web3_identity( Ok(()) } + +#[cfg(not(feature = "production"))] +mod non_prod { + use super::*; + use hex_literal::hex; + use sp_runtime::AccountId32; + + pub const ALICE_ACCOUNTID32: AccountId32 = + AccountId32::new(hex!["d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"]); + + pub fn ensure_alice(signer: &AccountId32) -> bool { + signer == &ALICE_ACCOUNTID32 + } + + pub fn ensure_enclave_signer_or_alice(signer: &AccountId32) -> bool { + signer == &enclave_signer_account::() || ensure_alice(signer) + } +} diff --git a/bitacross-worker/core-primitives/utils/Cargo.toml b/bitacross-worker/core-primitives/utils/Cargo.toml index 7c293aa011..1e8bd059ae 100644 --- a/bitacross-worker/core-primitives/utils/Cargo.toml +++ b/bitacross-worker/core-primitives/utils/Cargo.toml @@ -10,6 +10,7 @@ edition = "2021" [dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } +litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } [features] default = ["std"] diff --git a/bitacross-worker/core-primitives/utils/src/hex.rs b/bitacross-worker/core-primitives/utils/src/hex.rs index 4c167af6f3..3b6ff8a8a8 100644 --- a/bitacross-worker/core-primitives/utils/src/hex.rs +++ b/bitacross-worker/core-primitives/utils/src/hex.rs @@ -20,8 +20,9 @@ // Todo: merge with hex_display use crate::error::{Error, Result}; -use alloc::{string::String, vec::Vec}; +use alloc::string::String; use codec::{Decode, Encode}; +use litentry_hex_utils::{decode_hex, hex_encode}; /// Trait to encode a given value to a hex string, prefixed with "0x". pub trait ToHexPrefixed { @@ -45,57 +46,16 @@ impl FromHexPrefixed for T { type Output = T; fn from_hex(msg: &str) -> Result { - let byte_array = decode_hex(msg)?; + let byte_array = decode_hex(msg).map_err(Error::Hex)?; Decode::decode(&mut byte_array.as_slice()).map_err(Error::Codec) } } -/// Hex encodes given data and preappends a "0x". -pub fn hex_encode(data: &[u8]) -> String { - let mut hex_str = hex::encode(data); - hex_str.insert_str(0, "0x"); - hex_str -} - -/// Helper method for decoding hex. -pub fn decode_hex>(message: T) -> Result> { - let message = message.as_ref(); - let message = match message { - [b'0', b'x', hex_value @ ..] => hex_value, - _ => message, - }; - - let decoded_message = hex::decode(message).map_err(Error::Hex)?; - Ok(decoded_message) -} - #[cfg(test)] mod tests { use super::*; use alloc::string::ToString; - #[test] - fn hex_encode_decode_works() { - let data = "Hello World!".to_string(); - - let hex_encoded_data = hex_encode(&data.encode()); - let decoded_data = - String::decode(&mut decode_hex(hex_encoded_data).unwrap().as_slice()).unwrap(); - - assert_eq!(data, decoded_data); - } - - #[test] - fn hex_encode_decode_works_empty_input() { - let data = String::new(); - - let hex_encoded_data = hex_encode(&data.encode()); - let decoded_data = - String::decode(&mut decode_hex(hex_encoded_data).unwrap().as_slice()).unwrap(); - - assert_eq!(data, decoded_data); - } - #[test] fn hex_encode_decode_works_empty_input_for_decode() { let data = String::new(); diff --git a/bitacross-worker/core-primitives/utils/src/lib.rs b/bitacross-worker/core-primitives/utils/src/lib.rs index d03767e6c6..297ff5090e 100644 --- a/bitacross-worker/core-primitives/utils/src/lib.rs +++ b/bitacross-worker/core-primitives/utils/src/lib.rs @@ -25,7 +25,6 @@ pub mod buffer; pub mod error; pub mod hex; pub mod hex_display; -pub mod macros; pub mod stringify; // Public re-exports. diff --git a/bitacross-worker/core-primitives/utils/src/macros.rs b/bitacross-worker/core-primitives/utils/src/macros.rs deleted file mode 100644 index 69783ff727..0000000000 --- a/bitacross-worker/core-primitives/utils/src/macros.rs +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2020-2023 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -#[macro_export] -macro_rules! if_production_or { - ($prod_variant:expr, $non_prod_variant:expr) => { - if cfg!(feature = "production") { - $prod_variant - } else { - $non_prod_variant - } - }; -} - -#[macro_export] -macro_rules! if_not_production { - ($expression:expr) => { - if cfg!(not(feature = "production")) { - $expression - } - }; -} diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index ce53c8dc84..8261ba97b2 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -471,9 +471,9 @@ checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cargo_toml" -version = "0.16.3" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3f9629bc6c4388ea699781dc988c2b99766d7679b151c81990b4fa1208fafd3" +checksum = "3dc9f7a067415ab5058020f04c60ec7b557084dbec0e021217bbabc7a8d38d14" dependencies = [ "serde 1.0.193", "toml", @@ -590,7 +590,7 @@ version = "0.9.12" dependencies = [ "frame-support", "litentry-hex-utils", - "litentry-macros 0.9.12", + "litentry-macros", "litentry-proc-macros", "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", "parity-scale-codec", @@ -890,8 +890,10 @@ dependencies = [ "jsonrpc-core", "lazy_static", "lc-scheduled-enclave", - "litentry-macros 0.1.0", + "litentry-hex-utils", + "litentry-macros", "litentry-primitives", + "litentry-proc-macros", "log", "multibase", "once_cell 1.4.0", @@ -1840,6 +1842,7 @@ dependencies = [ "itp-types", "itp-utils", "lc-scheduled-enclave", + "litentry-hex-utils", "litentry-primitives", "log", "parity-scale-codec", @@ -1891,6 +1894,7 @@ dependencies = [ "itp-storage", "itp-types", "itp-utils", + "litentry-macros", "litentry-primitives", "log", "pallet-balances", @@ -2589,6 +2593,7 @@ name = "itp-utils" version = "0.9.0" dependencies = [ "hex", + "litentry-hex-utils", "parity-scale-codec", ] @@ -2660,6 +2665,7 @@ dependencies = [ "its-state", "its-validateer-fetch", "lc-scheduled-enclave", + "litentry-hex-utils", "log", "parity-scale-codec", "sgx_tstd", @@ -2933,14 +2939,6 @@ dependencies = [ "hex", ] -[[package]] -name = "litentry-macros" -version = "0.1.0" -dependencies = [ - "cargo_toml", - "quote 1.0.33", -] - [[package]] name = "litentry-macros" version = "0.9.12" @@ -2954,6 +2952,7 @@ dependencies = [ "hex", "itp-sgx-crypto", "itp-utils", + "litentry-hex-utils", "log", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "parity-scale-codec", @@ -4760,15 +4759,15 @@ checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" [[package]] name = "strum" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +checksum = "723b93e8addf9aa965ebe2d11da6d7540fa2283fcea14b3371ff055f7ba13f5f" [[package]] name = "strum_macros" -version = "0.25.3" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +checksum = "7a3417fc93d76740d974a01654a09777cb500428cc874ca9f45edfe0c4d4cd18" dependencies = [ "heck", "proc-macro2", diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index c6eae4b549..0c25d6ded4 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -17,7 +17,13 @@ evm = [ "ita-sgx-runtime/evm", "ita-stf/evm", ] -production = ["itp-settings/production", "itp-attestation-handler/production"] +production = [ + "ita-stf/production", + "itp-settings/production", + "itp-attestation-handler/production", + "litentry-primitives/production", + "litentry-macros/production", +] sidechain = ["itp-settings/sidechain", "itp-top-pool-author/sidechain"] offchain-worker = [ "itp-settings/offchain-worker", @@ -134,8 +140,10 @@ its-sidechain = { path = "../sidechain/sidechain-crate", default-features = fals # litentry lc-scheduled-enclave = { path = "../litentry/core/scheduled-enclave", default-features = false, features = ["sgx"] } -litentry-macros = { path = "../litentry/macros" } +litentry-hex-utils = { path = "../../primitives/hex", default-features = false } +litentry-macros = { path = "../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../litentry/primitives", default-features = false, features = ["sgx"] } +litentry-proc-macros = { path = "../../primitives/core/proc-macros", default-features = false } # substrate deps frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index 9c3b078558..208dcb50f5 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -75,7 +75,8 @@ use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvide use itp_sgx_crypto::key_repository::AccessPubkey; use itp_storage::{StorageProof, StorageProofChecker}; use itp_types::{ShardIdentifier, SignedBlock}; -use itp_utils::{if_production_or, write_slice_and_whitespace_pad}; +use itp_utils::write_slice_and_whitespace_pad; +use litentry_macros::if_production_or; use log::*; use once_cell::sync::OnceCell; use sgx_types::sgx_status_t; @@ -133,7 +134,7 @@ pub unsafe extern "C" fn init( // Initialize the logging environment in the enclave. if_production_or!( { - let module_names = litentry_macros::local_modules!(); + let module_names = litentry_proc_macros::local_modules!(); println!( "Initializing logger to filter only following local modules: {:?}", module_names diff --git a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs index 4133bccca6..93b9370d57 100644 --- a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs +++ b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs @@ -42,16 +42,15 @@ use itp_stf_executor::{getter_executor::ExecuteGetter, traits::StfShardVaultQuer use itp_stf_primitives::types::AccountId; use itp_stf_state_handler::handle_state::HandleState; use itp_top_pool_author::traits::AuthorApi; -use itp_types::{ - DirectRequestStatus, Index, MrEnclave, RsaRequest, ShardIdentifier, SidechainBlockNumber, H256, -}; -use itp_utils::{if_not_production, FromHexPrefixed, ToHexPrefixed}; +use itp_types::{DirectRequestStatus, Index, RsaRequest, ShardIdentifier, H256}; +use itp_utils::{FromHexPrefixed, ToHexPrefixed}; use its_primitives::types::block::SignedBlock; use its_sidechain::rpc_handler::{ direct_top_pool_api, direct_top_pool_api::decode_shard_from_base58, import_block_api, }; use jsonrpc_core::{serde_json::json, IoHandler, Params, Value}; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; +use litentry_macros::if_not_production; use litentry_primitives::DecryptableRequest; use log::debug; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; @@ -339,6 +338,7 @@ where }); if_not_production!({ + use itp_types::{MrEnclave, SidechainBlockNumber}; // state_updateScheduledEnclave, params: sidechainBlockNumber, hex encoded mrenclave io.add_sync_method("state_updateScheduledEnclave", move |params: Params| { match params.parse::<(SidechainBlockNumber, String)>() { @@ -489,7 +489,7 @@ fn forward_dcap_quote_inner(params: Params) -> Result { let param = &hex_encoded_params.get(0).ok_or("Could not get first param")?; let encoded_quote_to_forward: Vec = - itp_utils::hex::decode_hex(param).map_err(|e| format!("{:?}", e))?; + litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); let ext = generate_dcap_ra_extrinsic_from_quote_internal(url, &encoded_quote_to_forward) @@ -519,7 +519,7 @@ fn attesteer_forward_ias_attestation_report_inner( let param = &hex_encoded_params.get(0).ok_or("Could not get first param")?; let ias_attestation_report = - itp_utils::hex::decode_hex(param).map_err(|e| format!("{:?}", e))?; + litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); let ext = generate_ias_ra_extrinsic_from_der_cert_internal(url, &ias_attestation_report) diff --git a/bitacross-worker/enclave-runtime/src/top_pool_execution.rs b/bitacross-worker/enclave-runtime/src/top_pool_execution.rs index a8168864e3..e80611707b 100644 --- a/bitacross-worker/enclave-runtime/src/top_pool_execution.rs +++ b/bitacross-worker/enclave-runtime/src/top_pool_execution.rs @@ -15,11 +15,12 @@ */ +#[cfg(not(feature = "production"))] +use crate::initialization::global_components::GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT; use crate::{ error::Result, initialization::global_components::{ GLOBAL_OCALL_API_COMPONENT, GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT, - GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT, GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT, GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, GLOBAL_STATE_HANDLER_COMPONENT, GLOBAL_TOP_POOL_AUTHOR_COMPONENT, }, @@ -55,7 +56,6 @@ use itp_sgx_externalities::SgxExternalities; use itp_stf_state_handler::{handle_state::HandleState, query_shard_state::QueryShardState}; use itp_time_utils::duration_now; use itp_types::{parentchain::ParentchainCall, Block, OpaqueCall, H256}; -use itp_utils::if_not_production; use its_primitives::{ traits::{ Block as SidechainBlockTrait, Header as HeaderTrait, ShardIdentifierFor, SignedBlock, @@ -69,6 +69,7 @@ use its_sidechain::{ validateer_fetch::ValidateerFetch, }; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; +use litentry_macros::if_not_production; use log::*; use sgx_types::sgx_status_t; use sp_core::{crypto::UncheckedFrom, Pair}; @@ -172,6 +173,7 @@ fn execute_top_pool_trusted_calls_internal() -> Result<()> { let authority = GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT.get()?.retrieve_key()?; + #[cfg(not(feature = "production"))] let fail_on_demand = GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT.get()?; match yield_next_slot( diff --git a/bitacross-worker/litentry/primitives/Cargo.toml b/bitacross-worker/litentry/primitives/Cargo.toml index 86242bc003..076b5aab29 100644 --- a/bitacross-worker/litentry/primitives/Cargo.toml +++ b/bitacross-worker/litentry/primitives/Cargo.toml @@ -30,6 +30,7 @@ sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "m # internal dependencies itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } itp-utils = { path = "../../core-primitives/utils", default-features = false } +litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../primitives/core", default-features = false } teerex-primitives = { path = "../../../primitives/teerex", default-features = false } @@ -38,7 +39,9 @@ base64 = { version = "0.13", features = ["alloc"] } [features] default = ["std"] -production = [] +production = [ + "parentchain-primitives/production", +] sgx = [ "sgx_tstd", "rand-sgx", diff --git a/bitacross-worker/litentry/primitives/src/lib.rs b/bitacross-worker/litentry/primitives/src/lib.rs index b439190a30..baceb51c69 100644 --- a/bitacross-worker/litentry/primitives/src/lib.rs +++ b/bitacross-worker/litentry/primitives/src/lib.rs @@ -41,7 +41,7 @@ pub use validation_data::*; use bitcoin::sign_message::{signed_msg_hash, MessageSignature}; use codec::{Decode, Encode, MaxEncodedLen}; use itp_sgx_crypto::ShieldingCryptoDecrypt; -use itp_utils::hex::hex_encode; +use litentry_hex_utils::hex_encode; use log::error; pub use parentchain_primitives::{ all_bitcoin_web3networks, all_evm_web3networks, all_substrate_web3networks, all_web3networks, diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index f076e26cf5..49e1f4e61d 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -71,6 +71,8 @@ sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "po # litentry config = "0.13.3" ita-stf = { path = "../app-libs/stf", default-features = false } +litentry-hex-utils = { path = "../../primitives/hex", default-features = false } +litentry-macros = { path = "../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } sgx-verify = { path = "../../pallets/teerex/sgx-verify", default-features = false } @@ -81,7 +83,12 @@ default = [] evm = [] sidechain = ["itp-settings/sidechain"] offchain-worker = ["itp-settings/offchain-worker"] -production = ["itp-settings/production"] +production = [ + "ita-stf/production", + "itp-settings/production", + "litentry-macros/production", + "litentry-primitives/production", +] teeracle = ["itp-settings/teeracle"] dcap = [] attesteer = ["dcap"] diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index a4f58a459b..77b1c6a984 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -44,12 +44,12 @@ use itp_node_api::{ node_api_factory::{CreateNodeApi, NodeApiFactory}, }; use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; -use itp_utils::if_production_or; use its_peer_fetch::{ block_fetch_client::BlockFetcher, untrusted_peer_fetch::UntrustedPeerFetcher, }; use its_primitives::types::block::SignedBlock as SignedSidechainBlock; use its_storage::{interface::FetchBlocks, BlockPruner, SidechainStorageLock}; +use litentry_macros::if_production_or; use log::*; use my_node_runtime::{Hash, Header, RuntimeEvent}; use regex::Regex; diff --git a/bitacross-worker/service/src/teeracle/mod.rs b/bitacross-worker/service/src/teeracle/mod.rs index 420a175b26..415f7c461d 100644 --- a/bitacross-worker/service/src/teeracle/mod.rs +++ b/bitacross-worker/service/src/teeracle/mod.rs @@ -20,7 +20,7 @@ use codec::{Decode, Encode}; use itp_enclave_api::teeracle_api::TeeracleApi; use itp_node_api::api_client::ParentchainApi; use itp_types::parentchain::Hash; -use itp_utils::hex::hex_encode; +use litentry_hex_utils::hex_encode; use log::*; use sp_runtime::OpaqueExtrinsic; use std::time::Duration; diff --git a/bitacross-worker/sidechain/consensus/aura/Cargo.toml b/bitacross-worker/sidechain/consensus/aura/Cargo.toml index a7a52de35e..edb3ed9916 100644 --- a/bitacross-worker/sidechain/consensus/aura/Cargo.toml +++ b/bitacross-worker/sidechain/consensus/aura/Cargo.toml @@ -42,6 +42,7 @@ its-validateer-fetch = { path = "../../validateer-fetch", default-features = fal # litentry itp-utils = { path = "../../../core-primitives/utils", default-features = false } lc-scheduled-enclave = { path = "../../../litentry/core/scheduled-enclave", default-features = false } +litentry-hex-utils = { path = "../../../../primitives/hex", default-features = false } [dev-dependencies] codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } diff --git a/bitacross-worker/sidechain/consensus/aura/src/lib.rs b/bitacross-worker/sidechain/consensus/aura/src/lib.rs index 193eb0501c..dbbe099e4e 100644 --- a/bitacross-worker/sidechain/consensus/aura/src/lib.rs +++ b/bitacross-worker/sidechain/consensus/aura/src/lib.rs @@ -37,7 +37,6 @@ use itp_sgx_externalities::SgxExternalities; use itp_stf_state_handler::handle_state::HandleState; use itp_time_utils::duration_now; -use itp_utils::hex::hex_encode; use its_block_verification::slot::slot_author; use its_consensus_common::{Environment, Error as ConsensusError, Proposer}; use its_consensus_slots::{SimpleSlotWorker, Slot, SlotInfo}; @@ -47,6 +46,7 @@ use its_primitives::{ }; use its_validateer_fetch::ValidateerFetch; use lc_scheduled_enclave::ScheduledEnclaveUpdater; +use litentry_hex_utils::hex_encode; use sp_core::ByteArray; use sp_runtime::{ app_crypto::{sp_core::H256, Pair}, From d44a9565438428ad30782a70f6e09b0ec2fa9c0f Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Wed, 31 Jan 2024 17:15:42 +0100 Subject: [PATCH 03/64] bitacross remove teeracle feature (#2456) --- .../.github/workflows/build_and_test.yml | 37 --- .../.github/workflows/check_labels.yml | 2 +- .../.github/workflows/delete-release.yml | 2 +- .../workflows/publish-docker-teeracle.yml | 43 --- bitacross-worker/Cargo.lock | 19 -- bitacross-worker/Cargo.toml | 3 - bitacross-worker/app-libs/oracle/Cargo.toml | 48 --- .../src/certificates/amazon_root_ca_a.pem | 20 -- .../baltimore_cyber_trust_root_v3.pem | 21 -- .../certificates/lets_encrypt_root_cert.pem | 31 -- .../src/certificates/open_meteo_root.pem | 31 -- bitacross-worker/app-libs/oracle/src/error.rs | 39 --- bitacross-worker/app-libs/oracle/src/lib.rs | 84 ------ .../app-libs/oracle/src/metrics_exporter.rs | 104 ------- bitacross-worker/app-libs/oracle/src/mock.rs | 120 -------- .../oracle/src/oracle_sources/coin_gecko.rs | 220 -------------- .../src/oracle_sources/coin_market_cap.rs | 242 --------------- .../app-libs/oracle/src/oracle_sources/mod.rs | 19 -- .../oracle_sources/weather_oracle_source.rs | 120 -------- .../src/oracles/exchange_rate_oracle.rs | 154 ---------- .../app-libs/oracle/src/oracles/mod.rs | 18 -- .../oracle/src/oracles/weather_oracle.rs | 83 ------ bitacross-worker/app-libs/oracle/src/test.rs | 125 -------- .../app-libs/oracle/src/traits.rs | 55 ---- bitacross-worker/app-libs/oracle/src/types.rs | 61 ---- bitacross-worker/cli/Cargo.toml | 1 - bitacross-worker/cli/demo_teeracle_generic.sh | 136 --------- .../cli/demo_teeracle_whitelist.sh | 157 ---------- bitacross-worker/cli/src/commands.rs | 13 - bitacross-worker/cli/src/lib.rs | 3 - .../src/oracle/commands/add_to_whitelist.rs | 67 ----- .../src/oracle/commands/listen_to_exchange.rs | 79 ----- .../src/oracle/commands/listen_to_oracle.rs | 91 ------ .../cli/src/oracle/commands/mod.rs | 25 -- bitacross-worker/cli/src/oracle/mod.rs | 49 --- .../enclave-api/ffi/src/lib.rs | 24 -- .../core-primitives/enclave-api/src/lib.rs | 1 - .../enclave-api/src/teeracle_api.rs | 114 ------- .../enclave-metrics/src/lib.rs | 23 -- .../node-api/api-client-extensions/src/lib.rs | 2 - .../src/pallet_teeracle.rs | 19 -- .../node-api/metadata/src/lib.rs | 1 - .../node-api/metadata/src/pallet_teeracle.rs | 46 --- .../core-primitives/settings/Cargo.toml | 1 - .../core-primitives/settings/src/lib.rs | 21 +- .../settings/src/worker_mode.rs | 10 +- .../top-pool-author/Cargo.toml | 1 - .../top-pool-author/src/author.rs | 9 +- bitacross-worker/docker/README.md | 10 - .../docker/demo-teeracle-generic.yml | 68 ----- bitacross-worker/docker/demo-teeracle.yml | 71 ----- bitacross-worker/enclave-runtime/Cargo.lock | 55 ---- bitacross-worker/enclave-runtime/Cargo.toml | 6 - bitacross-worker/enclave-runtime/Enclave.edl | 14 - .../enclave-runtime/src/empty_impls.rs | 32 -- .../parentchain/integritee_parachain.rs | 2 - .../parentchain/integritee_solochain.rs | 2 - .../parentchain/target_a_parachain.rs | 2 - .../parentchain/target_a_solochain.rs | 2 - .../parentchain/target_b_parachain.rs | 2 - .../parentchain/target_b_solochain.rs | 2 - bitacross-worker/enclave-runtime/src/lib.rs | 14 +- .../enclave-runtime/src/teeracle/mod.rs | 279 ------------------ .../enclave-runtime/src/test/mod.rs | 3 - .../src/test/teeracle_tests.rs | 50 ---- .../enclave-runtime/src/test/tests_main.rs | 14 - .../src/tls_ra/tls_ra_server.rs | 3 +- bitacross-worker/samples/teeracle/README.md | 58 ---- .../samples/teeracle/install-teeracle.sh | 7 - .../samples/teeracle/kubernetes/Chart.yaml | 7 - .../kubernetes/templates/teeracle.yaml | 73 ----- .../samples/teeracle/kubernetes/values.yaml | 14 - .../changelog/templates/changes.md.tera | 1 - .../templates/changes_teeracle.md.tera | 17 -- bitacross-worker/scripts/teeracle.sh | 19 -- bitacross-worker/service/Cargo.toml | 1 - bitacross-worker/service/src/cli.yml | 11 - bitacross-worker/service/src/config.rs | 49 +-- bitacross-worker/service/src/main.rs | 2 - bitacross-worker/service/src/main_impl.rs | 52 +--- .../service/src/prometheus_metrics.rs | 9 - .../service/src/teeracle/schedule_periodic.rs | 46 --- .../service/src/teeracle/teeracle_metrics.rs | 76 ----- scripts/pre-commit.sh | 13 +- 84 files changed, 33 insertions(+), 3647 deletions(-) delete mode 100644 bitacross-worker/.github/workflows/publish-docker-teeracle.yml delete mode 100644 bitacross-worker/app-libs/oracle/Cargo.toml delete mode 100644 bitacross-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem delete mode 100644 bitacross-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem delete mode 100644 bitacross-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem delete mode 100644 bitacross-worker/app-libs/oracle/src/certificates/open_meteo_root.pem delete mode 100644 bitacross-worker/app-libs/oracle/src/error.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/lib.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/metrics_exporter.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/mock.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracle_sources/mod.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracles/mod.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/oracles/weather_oracle.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/test.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/traits.rs delete mode 100644 bitacross-worker/app-libs/oracle/src/types.rs delete mode 100755 bitacross-worker/cli/demo_teeracle_generic.sh delete mode 100755 bitacross-worker/cli/demo_teeracle_whitelist.sh delete mode 100644 bitacross-worker/cli/src/oracle/commands/add_to_whitelist.rs delete mode 100644 bitacross-worker/cli/src/oracle/commands/listen_to_exchange.rs delete mode 100644 bitacross-worker/cli/src/oracle/commands/listen_to_oracle.rs delete mode 100644 bitacross-worker/cli/src/oracle/commands/mod.rs delete mode 100644 bitacross-worker/cli/src/oracle/mod.rs delete mode 100644 bitacross-worker/core-primitives/enclave-api/src/teeracle_api.rs delete mode 100644 bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs delete mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs delete mode 100644 bitacross-worker/docker/demo-teeracle-generic.yml delete mode 100644 bitacross-worker/docker/demo-teeracle.yml delete mode 100644 bitacross-worker/enclave-runtime/src/teeracle/mod.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/teeracle_tests.rs delete mode 100644 bitacross-worker/samples/teeracle/README.md delete mode 100755 bitacross-worker/samples/teeracle/install-teeracle.sh delete mode 100644 bitacross-worker/samples/teeracle/kubernetes/Chart.yaml delete mode 100644 bitacross-worker/samples/teeracle/kubernetes/templates/teeracle.yaml delete mode 100644 bitacross-worker/samples/teeracle/kubernetes/values.yaml delete mode 100644 bitacross-worker/scripts/changelog/templates/changes_teeracle.md.tera delete mode 100644 bitacross-worker/scripts/teeracle.sh delete mode 100644 bitacross-worker/service/src/teeracle/schedule_periodic.rs delete mode 100644 bitacross-worker/service/src/teeracle/teeracle_metrics.rs diff --git a/bitacross-worker/.github/workflows/build_and_test.yml b/bitacross-worker/.github/workflows/build_and_test.yml index d007fe1eb4..b9f6467b86 100644 --- a/bitacross-worker/.github/workflows/build_and_test.yml +++ b/bitacross-worker/.github/workflows/build_and_test.yml @@ -43,11 +43,6 @@ jobs: host: integritee-builder-sgx sgx_mode: HW additional_features: dcap - - flavor_id: teeracle - mode: teeracle - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - flavor_id: sidechain-evm mode: sidechain additional_features: evm,dcap @@ -135,11 +130,6 @@ jobs: host: integritee-builder-sgx sgx_mode: HW additional_features: dcap - - flavor_id: teeracle - mode: teeracle - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - flavor_id: sidechain-evm mode: sidechain additional_features: evm,dcap @@ -211,14 +201,12 @@ jobs: cargo clippy --release -- -D warnings, cargo clippy --release --features evm -- -D warnings, cargo clippy --release --features sidechain -- -D warnings, - cargo clippy --release --features teeracle -- -D warnings, cargo clippy --release --features offchain-worker -- -D warnings, # Enclave cd enclave-runtime && cargo clippy -- -D warnings, cd enclave-runtime && cargo clippy --features evm -- -D warnings, cd enclave-runtime && cargo clippy --features sidechain -- -D warnings, - cd enclave-runtime && cargo clippy --features teeracle -- -D warnings, cd enclave-runtime && cargo clippy --features offchain-worker -- -D warnings, # Fmt @@ -261,10 +249,6 @@ jobs: env: WORKER_IMAGE_TAG: integritee-worker:dev CLIENT_IMAGE_TAG: integritee-cli:dev - COINMARKETCAP_KEY: ${{ secrets.COINMARKETCAP_KEY }} - # IAS_EPID_SPID: ${{ secrets.IAS_SPID }} - # IAS_EPID_KEY: ${{ secrets.IAS_PRIMARY_KEY }} - TEERACLE_INTERVAL_SECONDS: 10 strategy: fail-fast: false @@ -290,16 +274,6 @@ jobs: demo_name: demo-shielding-unshielding-multiworker host: test-runner-sgx sgx_mode: HW - - test: Teeracle - flavor_id: teeracle - demo_name: demo-teeracle - host: test-runner-sgx - sgx_mode: HW - - test: Teeracle - flavor_id: teeracle - demo_name: demo-teeracle-generic - host: test-runner-sgx - sgx_mode: HW - test: Benchmark flavor_id: sidechain demo_name: sidechain-benchmark @@ -440,10 +414,6 @@ jobs: fail-fast: false matrix: include: - - flavor_id: teeracle - mode: teeracle - sgx_mode: HW - additional_features: dcap - flavor_id: sidechain mode: sidechain sgx_mode: HW @@ -537,12 +507,6 @@ jobs: steps: - uses: actions/checkout@v3 - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-teeracle-${{ github.ref_name }}.tar.gz - path: . - - name: Download Worker Image uses: actions/download-artifact@v3 with: @@ -592,7 +556,6 @@ jobs: draft: true name: Docker ${{ github.ref_name }} files: | - integritee-worker-teeracle-${{ github.ref_name }}.tar.gz integritee-worker-sidechain-${{ github.ref_name }}.tar.gz integritee-client integritee-demo-validateer diff --git a/bitacross-worker/.github/workflows/check_labels.yml b/bitacross-worker/.github/workflows/check_labels.yml index 9511ed0b93..fdab3e9736 100644 --- a/bitacross-worker/.github/workflows/check_labels.yml +++ b/bitacross-worker/.github/workflows/check_labels.yml @@ -6,7 +6,7 @@ jobs: A-label-check: uses: ./.github/workflows/label-checker.yml with: - predefined_labels: "A0-core,A1-cli,A2-applibs,A3-sidechain,A4-offchain,A5-teeracle,A6-evm,A7-somethingelse" + predefined_labels: "A0-core,A1-cli,A2-applibs,A3-sidechain,A4-offchain,A6-evm,A7-somethingelse" B-label-check: uses: ./.github/workflows/label-checker.yml diff --git a/bitacross-worker/.github/workflows/delete-release.yml b/bitacross-worker/.github/workflows/delete-release.yml index 53fbdbb0f3..7029a5eb1f 100644 --- a/bitacross-worker/.github/workflows/delete-release.yml +++ b/bitacross-worker/.github/workflows/delete-release.yml @@ -11,7 +11,7 @@ jobs: strategy: matrix: #binary: ["integritee-client", "integritee-demo-validateer"] - binary: ["teeracle"] + binary: [] steps: - uses: actions/checkout@v2 diff --git a/bitacross-worker/.github/workflows/publish-docker-teeracle.yml b/bitacross-worker/.github/workflows/publish-docker-teeracle.yml deleted file mode 100644 index 01a9a6f8b0..0000000000 --- a/bitacross-worker/.github/workflows/publish-docker-teeracle.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish Docker image for new teeracle release - -on: - release: - types: - - published - -jobs: - main: - name: Push Integritee Teeracle to Dockerhub - runs-on: [ self-hosted ] - steps: - - uses: actions/checkout@v3 - - - name: Download teeracle from release - uses: dsaltares/fetch-gh-release-asset@master - with: - version: "tags/${{ github.event.release.tag_name }}" - file: integritee-worker-teeracle-${{ github.event.release.tag_name }}.tar.gz - target: "integritee-worker-teeracle.tar.gz" - token: ${{ secrets.GITHUB_TOKEN }} - - - - name: Login to Dockerhub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Load Worker & Push - env: - DOCKER_BUILDKIT: 1 - run: | - docker image load --input integritee-worker-teeracle.tar.gz - docker images --all - docker push integritee/teeracle:${{ github.event.release.tag_name }} - - - name: Delete images - run: | - if [[ "$(docker images -q integritee/teeracle:${{ github.event.release.tag_name }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee/teeracle:${{ github.event.release.tag_name }} 2>/dev/null - fi - docker images --all diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index f2c237e02a..a0631f3ce6 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -4595,25 +4595,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ita-oracle" -version = "0.9.0" -dependencies = [ - "itc-rest-client", - "itp-enclave-metrics", - "itp-ocall-api", - "lazy_static", - "log 0.4.20", - "parity-scale-codec", - "serde 1.0.193", - "sgx_tstd", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", - "thiserror 1.0.44", - "thiserror 1.0.9", - "url 2.1.1", - "url 2.4.0", -] - [[package]] name = "ita-parentchain-interface" version = "0.9.0" diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 40caaaa98d..4f43662fa1 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ - "app-libs/oracle", "app-libs/parentchain-interface", "app-libs/sgx-runtime", "app-libs/stf", @@ -102,10 +101,8 @@ ring = { git = "https://github.com/betrusted-io/ring-xous", branch = "0.16.20-cl #pallet-teerex = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #pallet-sidechain = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #sgx-verify = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-teeracle = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #test-utils = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #claims-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #enclave-bridge-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #teerex-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#teeracle-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } #common-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } diff --git a/bitacross-worker/app-libs/oracle/Cargo.toml b/bitacross-worker/app-libs/oracle/Cargo.toml deleted file mode 100644 index eb8fa135e2..0000000000 --- a/bitacross-worker/app-libs/oracle/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "ita-oracle" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] - -# std dependencies -thiserror = { version = "1.0.26", optional = true } -url = { version = "2.0.0", optional = true } - -# sgx dependencies -sgx_tstd = { rev = "v1.1.3", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } -thiserror_sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } -url_sgx = { package = "url", git = "https://github.com/mesalock-linux/rust-url-sgx", tag = "sgx_1.1.3", optional = true } - -# no_std dependencies -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -lazy_static = { version = "1.1.0", features = ["spin_no_std"] } -log = { version = "0.4", default-features = false } -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } -substrate-fixed = { default-features = false, git = "https://github.com/encointer/substrate-fixed", tag = "v0.5.9" } - -# internal dependencies -itc-rest-client = { path = "../../core/rest-client", default-features = false } -itp-enclave-metrics = { path = "../../core-primitives/enclave-metrics", default-features = false } -itp-ocall-api = { path = "../../core-primitives/ocall-api", default-features = false } - -[features] -default = ["std"] -std = [ - "itc-rest-client/std", - "itp-enclave-metrics/std", - "itp-ocall-api/std", - "log/std", - "serde/std", - "substrate-fixed/std", - "thiserror", - "url", -] -sgx = [ - "itc-rest-client/sgx", - "itp-enclave-metrics/sgx", - "sgx_tstd", - "thiserror_sgx", - "url_sgx", -] diff --git a/bitacross-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem b/bitacross-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem deleted file mode 100644 index a6f3e92af5..0000000000 --- a/bitacross-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- diff --git a/bitacross-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem b/bitacross-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem deleted file mode 100644 index 519028c63b..0000000000 --- a/bitacross-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ -RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD -VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX -DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y -ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy -VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr -mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr -IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK -mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu -XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy -dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye -jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 -BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 -DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 -9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx -jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 -Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz -ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS -R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE----- diff --git a/bitacross-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem b/bitacross-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem deleted file mode 100644 index 57d4a3766c..0000000000 --- a/bitacross-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- \ No newline at end of file diff --git a/bitacross-worker/app-libs/oracle/src/certificates/open_meteo_root.pem b/bitacross-worker/app-libs/oracle/src/certificates/open_meteo_root.pem deleted file mode 100644 index b85c8037f6..0000000000 --- a/bitacross-worker/app-libs/oracle/src/certificates/open_meteo_root.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- diff --git a/bitacross-worker/app-libs/oracle/src/error.rs b/bitacross-worker/app-libs/oracle/src/error.rs deleted file mode 100644 index df72280f34..0000000000 --- a/bitacross-worker/app-libs/oracle/src/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::types::TradingPair; -use std::{boxed::Box, string::String}; - -/// Exchange rate error -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Rest client error")] - RestClient(#[from] itc_rest_client::error::Error), - #[error("Could not retrieve any data from {0} for {1}")] - NoValidData(String, String), - #[error("Value for exchange rate is null")] - EmptyExchangeRate(TradingPair), - #[error("Invalid id for crypto currency")] - InvalidCryptoCurrencyId, - #[error("Invalid id for fiat currency")] - InvalidFiatCurrencyId, - #[error(transparent)] - Other(#[from] Box), -} diff --git a/bitacross-worker/app-libs/oracle/src/lib.rs b/bitacross-worker/app-libs/oracle/src/lib.rs deleted file mode 100644 index 6faee79a63..0000000000 --- a/bitacross-worker/app-libs/oracle/src/lib.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(test, feature(assert_matches))] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -// re-export module to properly feature gate sgx and regular std environment -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub mod sgx_reexport_prelude { - pub use thiserror_sgx as thiserror; - pub use url_sgx as url; -} - -use crate::{error::Error, metrics_exporter::MetricsExporter}; -use itp_ocall_api::EnclaveMetricsOCallApi; -use std::sync::Arc; - -pub mod error; -pub mod metrics_exporter; -pub mod traits; -pub mod types; - -pub mod oracles; -pub use oracles::{exchange_rate_oracle::ExchangeRateOracle, weather_oracle::WeatherOracle}; - -pub mod oracle_sources; -pub use oracle_sources::{ - coin_gecko::CoinGeckoSource, coin_market_cap::CoinMarketCapSource, - weather_oracle_source::WeatherOracleSource, -}; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod test; - -pub type CoinGeckoExchangeRateOracle = - ExchangeRateOracle>; - -pub type CoinMarketCapExchangeRateOracle = - ExchangeRateOracle>; - -pub type OpenMeteoWeatherOracle = - WeatherOracle>; - -pub fn create_coin_gecko_oracle( - ocall_api: Arc, -) -> CoinGeckoExchangeRateOracle { - ExchangeRateOracle::new(CoinGeckoSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} - -pub fn create_coin_market_cap_oracle( - ocall_api: Arc, -) -> CoinMarketCapExchangeRateOracle { - ExchangeRateOracle::new(CoinMarketCapSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} - -pub fn create_open_meteo_weather_oracle( - ocall_api: Arc, -) -> OpenMeteoWeatherOracle { - WeatherOracle::new(WeatherOracleSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} diff --git a/bitacross-worker/app-libs/oracle/src/metrics_exporter.rs b/bitacross-worker/app-libs/oracle/src/metrics_exporter.rs deleted file mode 100644 index aa10516fd1..0000000000 --- a/bitacross-worker/app-libs/oracle/src/metrics_exporter.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::types::{ExchangeRate, TradingPair}; -use itp_enclave_metrics::{EnclaveMetric, ExchangeRateOracleMetric, OracleMetric}; -use itp_ocall_api::EnclaveMetricsOCallApi; -use log::error; -use std::{string::String, sync::Arc, time::Instant}; - -/// Trait to export metrics for any Teeracle. -pub trait ExportMetrics { - fn increment_number_requests(&self, source: String); - - fn record_response_time(&self, source: String, timer: Instant); - - fn update_exchange_rate( - &self, - source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ); - - fn update_weather(&self, source: String, metrics_info: MetricsInfo); -} - -pub trait UpdateMetric { - fn update_metric(&self, metric: OracleMetric); -} - -/// Metrics exporter implementation. -pub struct MetricsExporter { - ocall_api: Arc, -} - -impl UpdateMetric for MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - fn update_metric(&self, _metric: OracleMetric) { - // TODO: Implement me - } -} - -impl MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - pub fn new(ocall_api: Arc) -> Self { - MetricsExporter { ocall_api } - } - - fn update_metric(&self, metric: ExchangeRateOracleMetric) { - if let Err(e) = self.ocall_api.update_metric(EnclaveMetric::ExchangeRateOracle(metric)) { - error!("Failed to update enclave metric, sgx_status_t: {}", e) - } - } -} - -impl ExportMetrics for MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - fn increment_number_requests(&self, source: String) { - self.update_metric(ExchangeRateOracleMetric::NumberRequestsIncrement(source)); - } - - fn record_response_time(&self, source: String, timer: Instant) { - self.update_metric(ExchangeRateOracleMetric::ResponseTime( - source, - timer.elapsed().as_millis(), - )); - } - - fn update_exchange_rate( - &self, - source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ) { - self.update_metric(ExchangeRateOracleMetric::ExchangeRate( - source, - trading_pair.key(), - exchange_rate, - )); - } - - fn update_weather(&self, _source: String, _metrics_info: MetricsInfo) { - // TODO: Implement me - } -} diff --git a/bitacross-worker/app-libs/oracle/src/mock.rs b/bitacross-worker/app-libs/oracle/src/mock.rs deleted file mode 100644 index f12224b0ea..0000000000 --- a/bitacross-worker/app-libs/oracle/src/mock.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(feature = "sgx")] -use std::sync::SgxRwLock as RwLock; - -#[cfg(feature = "std")] -use std::sync::RwLock; - -use crate::{ - error::Error, - metrics_exporter::ExportMetrics, - traits::OracleSource, - types::{ExchangeRate, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use std::{ - time::{Duration, Instant}, - vec, - vec::Vec, -}; -use url::Url; - -/// Mock metrics exporter. -#[derive(Default)] -pub(crate) struct MetricsExporterMock { - number_requests: RwLock, - response_times: RwLock>, - exchange_rates: RwLock>, -} - -impl MetricsExporterMock { - pub fn get_number_request(&self) -> u64 { - *self.number_requests.read().unwrap() - } - - pub fn get_response_times(&self) -> Vec { - self.response_times.read().unwrap().clone() - } - - pub fn get_exchange_rates(&self) -> Vec<(TradingPair, ExchangeRate)> { - self.exchange_rates.read().unwrap().clone() - } -} - -impl ExportMetrics for MetricsExporterMock { - fn increment_number_requests(&self, _source: String) { - (*self.number_requests.write().unwrap()) += 1; - } - - fn record_response_time(&self, _source: String, timer: Instant) { - self.response_times.write().unwrap().push(timer.elapsed().as_millis()); - } - - fn update_exchange_rate( - &self, - _source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ) { - self.exchange_rates.write().unwrap().push((trading_pair, exchange_rate)); - } - - fn update_weather(&self, _source: String, _metrics_info: MetricsInfo) {} -} - -/// Mock oracle source. -#[derive(Default)] -pub(crate) struct OracleSourceMock; - -impl OracleSource for OracleSourceMock { - type OracleRequestResult = Result; - - fn metrics_id(&self) -> String { - "source_mock".to_string() - } - - fn request_timeout(&self) -> Option { - None - } - - fn base_url(&self) -> Result { - Url::parse("https://mock.base.url").map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec!["MOCK_CERTIFICATE".to_string()] - } - fn execute_exchange_rate_request( - &self, - _rest_client: &mut RestClient>, - _trading_pair: TradingPair, - ) -> Result { - Ok(ExchangeRate::from_num(42.3f32)) - } - - fn execute_request( - _rest_client: &mut RestClient>, - _source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - Ok(42.3f32) - } -} diff --git a/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs b/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs deleted file mode 100644 index d9b8ad91ee..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs +++ /dev/null @@ -1,220 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use lazy_static::lazy_static; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const COINGECKO_URL: &str = "https://api.coingecko.com"; -const COINGECKO_PARAM_CURRENCY: &str = "vs_currency"; -const COINGECKO_PARAM_COIN: &str = "ids"; -const COINGECKO_PATH: &str = "api/v3/coins/markets"; -const COINGECKO_TIMEOUT: Duration = Duration::from_secs(20u64); -const COINGECKO_ROOT_CERTIFICATE_BALTIMORE: &str = - include_str!("../certificates/baltimore_cyber_trust_root_v3.pem"); -const COINGECKO_ROOT_CERTIFICATE_LETSENCRYPT: &str = - include_str!("../certificates/lets_encrypt_root_cert.pem"); - -lazy_static! { - static ref SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = HashMap::from([ - ("DOT", "polkadot"), - ("TEER", "integritee"), - ("KSM", "kusama"), - ("BTC", "bitcoin"), - ]); -} - -/// CoinGecko oracle source. -#[derive(Default)] -pub struct CoinGeckoSource; - -impl CoinGeckoSource { - fn map_crypto_currency_id(trading_pair: &TradingPair) -> Result { - let key = &trading_pair.crypto_currency; - match SYMBOL_ID_MAP.get(key.as_str()) { - Some(v) => Ok(v.to_string()), - None => Err(Error::InvalidCryptoCurrencyId), - } - } -} - -impl> OracleSource for CoinGeckoSource { - type OracleRequestResult = Result<(), Error>; - - fn metrics_id(&self) -> String { - "coin_gecko".to_string() - } - - fn request_timeout(&self) -> Option { - Some(COINGECKO_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(COINGECKO_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec![ - COINGECKO_ROOT_CERTIFICATE_LETSENCRYPT.to_string(), - COINGECKO_ROOT_CERTIFICATE_BALTIMORE.to_string(), - ] - } - - fn execute_request( - _rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let _trading_info: TradingInfo = source_info.into(); - // TODO Implement me - Ok(()) - } - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result { - let fiat_id = trading_pair.fiat_currency.clone(); - let crypto_id = Self::map_crypto_currency_id(&trading_pair)?; - - let response = rest_client.get_with::( - COINGECKO_PATH.to_string(), - &[(COINGECKO_PARAM_CURRENCY, &fiat_id), (COINGECKO_PARAM_COIN, &crypto_id)], - ); - - let response = match response { - Ok(response) => response, - Err(e) => { - error!("coingecko execute_exchange_rate_request() failed with: {:?}", &e); - return Err(Error::RestClient(e)) - }, - }; - - debug!("coingecko received response: {:?}", &response); - let list = response.0; - if list.is_empty() { - return Err(Error::NoValidData(COINGECKO_URL.to_string(), trading_pair.key())) - } - - match list[0].current_price { - Some(r) => Ok(ExchangeRate::from_num(r)), - None => Err(Error::EmptyExchangeRate(trading_pair)), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinGeckoMarketStruct { - id: String, - symbol: String, - name: String, - current_price: Option, - last_updated: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinGeckoMarket(pub Vec); - -impl RestPath for CoinGeckoMarket { - fn get_path(path: String) -> Result { - Ok(path) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - mock::MetricsExporterMock, - oracles::exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - }; - use core::assert_matches::assert_matches; - use std::sync::Arc; - - type TestCoinGeckoClient = ExchangeRateOracle; - - fn get_coin_gecko_crypto_currency_id(crypto_currency: &str) -> Result { - let trading_pair = TradingPair { - crypto_currency: crypto_currency.to_string(), - fiat_currency: "USD".to_string(), - }; - CoinGeckoSource::map_crypto_currency_id(&trading_pair) - } - - #[test] - fn crypto_currency_id_works_for_dot() { - let coin_id = get_coin_gecko_crypto_currency_id("DOT").unwrap(); - assert_eq!(&coin_id, "polkadot"); - } - - #[test] - fn crypto_currency_id_works_for_teer() { - let coin_id = get_coin_gecko_crypto_currency_id("TEER").unwrap(); - assert_eq!(&coin_id, "integritee"); - } - - #[test] - fn crypto_currency_id_works_for_ksm() { - let coin_id = get_coin_gecko_crypto_currency_id("KSM").unwrap(); - assert_eq!(&coin_id, "kusama"); - } - - #[test] - fn crypto_currency_id_works_for_btc() { - let coin_id = get_coin_gecko_crypto_currency_id("BTC").unwrap(); - assert_eq!(&coin_id, "bitcoin"); - } - - #[test] - fn crypto_currency_id_fails_for_undefined_crypto_currency() { - let result = get_coin_gecko_crypto_currency_id("Undefined"); - assert_matches!(result, Err(Error::InvalidCryptoCurrencyId)); - } - - #[test] - fn get_exchange_rate_for_undefined_fiat_currency_fails() { - let coin_gecko_client = create_coin_gecko_client(); - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CH".to_string() }; - let result = coin_gecko_client.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::RestClient(_))); - } - - fn create_coin_gecko_client() -> TestCoinGeckoClient { - TestCoinGeckoClient::new(CoinGeckoSource {}, Arc::new(MetricsExporterMock::default())) - } -} diff --git a/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs b/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs deleted file mode 100644 index a0e053b8e6..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs +++ /dev/null @@ -1,242 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap}, - env, - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const COINMARKETCAP_URL: &str = "https://pro-api.coinmarketcap.com"; -const COINMARKETCAP_KEY_PARAM: &str = "CMC_PRO_API_KEY"; -const FIAT_CURRENCY_PARAM: &str = "convert_id"; -const CRYPTO_CURRENCY_PARAM: &str = "id"; -const COINMARKETCAP_PATH: &str = "v2/cryptocurrency/quotes/latest"; // API endpoint to get the exchange rate with a basic API plan (free) -const COINMARKETCAP_TIMEOUT: Duration = Duration::from_secs(3u64); -const COINMARKETCAP_ROOT_CERTIFICATE: &str = include_str!("../certificates/amazon_root_ca_a.pem"); - -lazy_static! { - static ref CRYPTO_SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = - HashMap::from([("DOT", "6636"), ("TEER", "13323"), ("KSM", "5034"), ("BTC", "1"),]); - static ref COINMARKETCAP_KEY: String = env::var("COINMARKETCAP_KEY").unwrap_or_default(); -} - -lazy_static! { - static ref FIAT_SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = - HashMap::from([("USD", "2781"), ("EUR", "2790"), ("CHF", "2785"), ("JPY", "2797"),]); -} - -#[derive(Default)] -pub struct CoinMarketCapSource; - -impl CoinMarketCapSource { - fn map_crypto_currency_id(trading_pair: &TradingPair) -> Result { - CRYPTO_SYMBOL_ID_MAP - .get(trading_pair.crypto_currency.as_str()) - .map(|v| v.to_string()) - .ok_or(Error::InvalidCryptoCurrencyId) - } - - fn map_fiat_currency_id(trading_pair: &TradingPair) -> Result { - FIAT_SYMBOL_ID_MAP - .get(trading_pair.fiat_currency.as_str()) - .map(|v| v.to_string()) - .ok_or(Error::InvalidFiatCurrencyId) - } -} - -impl> OracleSource for CoinMarketCapSource { - // TODO Change this to return something useful? - type OracleRequestResult = Result<(), Error>; - - fn metrics_id(&self) -> String { - "coin_market_cap".to_string() - } - - fn request_timeout(&self) -> Option { - Some(COINMARKETCAP_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(COINMARKETCAP_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec![COINMARKETCAP_ROOT_CERTIFICATE.to_string()] - } - - fn execute_request( - _rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let trading_info: TradingInfo = source_info.into(); - let _fiat_currency = trading_info.trading_pair.fiat_currency; - let _crypto_currency = trading_info.trading_pair.crypto_currency; - // TODO Implement me - Ok(()) - } - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result { - let fiat_id = Self::map_fiat_currency_id(&trading_pair)?; - let crypto_id = Self::map_crypto_currency_id(&trading_pair)?; - - let response = rest_client - .get_with::( - COINMARKETCAP_PATH.to_string(), - &[ - (FIAT_CURRENCY_PARAM, &fiat_id), - (CRYPTO_CURRENCY_PARAM, &crypto_id), - (COINMARKETCAP_KEY_PARAM, &COINMARKETCAP_KEY), - ], - ) - .map_err(Error::RestClient)?; - - let data_struct = response.0; - - let data = match data_struct.data.get(&crypto_id) { - Some(d) => d, - None => - return Err(Error::NoValidData( - COINMARKETCAP_URL.to_string(), - trading_pair.crypto_currency, - )), - }; - - let quote = match data.quote.get(&fiat_id) { - Some(q) => q, - None => - return Err(Error::NoValidData(COINMARKETCAP_URL.to_string(), trading_pair.key())), - }; - match quote.price { - Some(r) => Ok(ExchangeRate::from_num(r)), - None => Err(Error::EmptyExchangeRate(trading_pair)), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct DataStruct { - id: Option, - name: String, - symbol: String, - quote: BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug)] -struct QuoteStruct { - price: Option, - last_updated: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinMarketCapMarketStruct { - data: BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinMarketCapMarket(pub CoinMarketCapMarketStruct); - -impl RestPath for CoinMarketCapMarket { - fn get_path(path: String) -> Result { - Ok(path) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - mock::MetricsExporterMock, - oracles::exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - }; - use core::assert_matches::assert_matches; - use std::sync::Arc; - - type TestClient = ExchangeRateOracle; - - fn get_coin_market_cap_crypto_currency_id(crypto_currency: &str) -> Result { - let trading_pair = TradingPair { - crypto_currency: crypto_currency.to_string(), - fiat_currency: "USD".to_string(), - }; - CoinMarketCapSource::map_crypto_currency_id(&trading_pair) - } - - #[test] - fn crypto_currency_id_works_for_dot() { - let coin_id = get_coin_market_cap_crypto_currency_id("DOT").unwrap(); - assert_eq!(&coin_id, "6636"); - } - - #[test] - fn crypto_currency_id_works_for_teer() { - let coin_id = get_coin_market_cap_crypto_currency_id("TEER").unwrap(); - assert_eq!(&coin_id, "13323"); - } - - #[test] - fn crypto_currency_id_works_for_ksm() { - let coin_id = get_coin_market_cap_crypto_currency_id("KSM").unwrap(); - assert_eq!(&coin_id, "5034"); - } - - #[test] - fn crypto_currency_id_works_for_btc() { - let coin_id = get_coin_market_cap_crypto_currency_id("BTC").unwrap(); - assert_eq!(&coin_id, "1"); - } - - #[test] - fn crypto_currency_id_fails_for_undefined_crypto_currency() { - let coin_id = get_coin_market_cap_crypto_currency_id("Undefined"); - assert_matches!(coin_id, Err(Error::InvalidCryptoCurrencyId)); - } - - #[test] - fn get_exchange_rate_for_undefined_fiat_currency_fails() { - let coin_market_cap_client = create_client(); - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CH".to_string() }; - let result = coin_market_cap_client.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::InvalidFiatCurrencyId)); - } - - fn create_client() -> TestClient { - TestClient::new(CoinMarketCapSource {}, Arc::new(MetricsExporterMock::default())) - } -} diff --git a/bitacross-worker/app-libs/oracle/src/oracle_sources/mod.rs b/bitacross-worker/app-libs/oracle/src/oracle_sources/mod.rs deleted file mode 100644 index d2d88153c3..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracle_sources/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -pub mod coin_gecko; -pub mod coin_market_cap; -pub mod weather_oracle_source; diff --git a/bitacross-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs b/bitacross-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs deleted file mode 100644 index 9f199be5dc..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingPair, WeatherInfo}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use serde::{Deserialize, Serialize}; -use std::{ - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const WEATHER_URL: &str = "https://api.open-meteo.com"; -const WEATHER_PARAM_LONGITUDE: &str = "longitude"; -const WEATHER_PARAM_LATITUDE: &str = "latitude"; -// const WEATHER_PARAM_HOURLY: &str = "hourly"; // TODO: Add to Query -const WEATHER_PATH: &str = "v1/forecast"; -const WEATHER_TIMEOUT: Duration = Duration::from_secs(3u64); -const WEATHER_ROOT_CERTIFICATE: &str = include_str!("../certificates/open_meteo_root.pem"); - -// TODO: Change f32 types to appropriate Substrate Fixed Type -#[derive(Default)] -pub struct WeatherOracleSource; - -impl> OracleSource for WeatherOracleSource { - type OracleRequestResult = Result; // TODO: Change from f32 type - - fn metrics_id(&self) -> String { - "weather".to_string() - } - - fn request_timeout(&self) -> Option { - Some(WEATHER_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(WEATHER_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - /// The server's root certificate. A valid certificate is required to open a tls connection - fn root_certificates_content(&self) -> Vec { - vec![WEATHER_ROOT_CERTIFICATE.to_string()] - } - - fn execute_exchange_rate_request( - &self, - _rest_client: &mut RestClient>, - _trading_pair: TradingPair, - ) -> Result { - Err(Error::NoValidData("None".into(), "None".into())) - } - - // TODO: Make this take a variant perhaps or a Closure so that it is more generic - fn execute_request( - rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let weather_info: WeatherInfo = source_info.into(); - let query = weather_info.weather_query; - - // TODO: - // This part is opinionated towards a hard coded query need to make more generic - let response = rest_client - .get_with::( - WEATHER_PATH.into(), - &[ - (WEATHER_PARAM_LATITUDE, &query.latitude), - (WEATHER_PARAM_LONGITUDE, &query.longitude), - //(WEATHER_PARAM_HOURLY), &query.hourly), - ], - ) - .map_err(Error::RestClient)?; - - let open_meteo_weather_struct = response.0; - - Ok(open_meteo_weather_struct.longitude) - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct OpenMeteoWeatherStruct { - latitude: f32, - longitude: f32, - //hourly: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct OpenMeteo(pub OpenMeteoWeatherStruct); - -impl RestPath for OpenMeteo { - fn get_path(path: String) -> Result { - Ok(path) - } -} diff --git a/bitacross-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs b/bitacross-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs deleted file mode 100644 index 0198a5fe1b..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs +++ /dev/null @@ -1,154 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - metrics_exporter::ExportMetrics, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, - Error, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use log::*; -use std::{ - sync::Arc, - thread, - time::{Duration, Instant}, -}; -use url::Url; - -#[allow(unused)] -pub struct ExchangeRateOracle { - oracle_source: OracleSourceType, - metrics_exporter: Arc, -} - -impl ExchangeRateOracle { - pub fn new(oracle_source: OracleSourceType, metrics_exporter: Arc) -> Self { - ExchangeRateOracle { oracle_source, metrics_exporter } - } -} - -pub trait GetExchangeRate { - /// Get the cryptocurrency/fiat_currency exchange rate - fn get_exchange_rate(&self, trading_pair: TradingPair) -> Result<(ExchangeRate, Url), Error>; -} - -impl GetExchangeRate - for ExchangeRateOracle -where - OracleSourceType: OracleSource, - MetricsExporter: ExportMetrics, -{ - fn get_exchange_rate(&self, trading_pair: TradingPair) -> Result<(ExchangeRate, Url), Error> { - let source_id = self.oracle_source.metrics_id(); - self.metrics_exporter.increment_number_requests(source_id.clone()); - - let base_url = self.oracle_source.base_url()?; - let root_certificates = self.oracle_source.root_certificates_content(); - let request_timeout = self.oracle_source.request_timeout(); - - debug!("Get exchange rate from URI: {}, trading pair: {:?}", base_url, trading_pair); - - let http_client = HttpClient::new( - SendWithCertificateVerification::new(root_certificates), - true, - request_timeout, - None, - None, - ); - let mut rest_client = RestClient::new(http_client, base_url.clone()); - - // Due to possible failures that may be temporarily this function tries to fetch the exchange rates `number_of_tries` times. - // If it still fails for the last attempt, then only in that case will it be considered a non-recoverable error. - let number_of_tries = 3; - let timer_start = Instant::now(); - - let mut tries = 0; - let result = loop { - tries += 1; - let exchange_result = self - .oracle_source - .execute_exchange_rate_request(&mut rest_client, trading_pair.clone()); - - match exchange_result { - Ok(exchange_rate) => { - self.metrics_exporter.record_response_time(source_id.clone(), timer_start); - self.metrics_exporter.update_exchange_rate( - source_id, - exchange_rate, - trading_pair, - ); - - debug!("Successfully executed exchange rate request"); - break Ok((exchange_rate, base_url)) - }, - Err(e) => - if tries < number_of_tries { - error!( - "Getting exchange rate from {} failed with {}, trying again in {:?}.", - &base_url, e, request_timeout - ); - debug!("Check that the API endpoint is available, for coingecko: https://status.coingecko.com/"); - thread::sleep( - request_timeout.unwrap_or_else(|| Duration::from_secs(number_of_tries)), - ); - } else { - error!( - "Getting exchange rate from {} failed {} times, latest error is: {}.", - &base_url, number_of_tries, &e - ); - break Err(e) - }, - } - }; - result - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::mock::{MetricsExporterMock, OracleSourceMock}; - - type TestOracle = ExchangeRateOracle; - - #[test] - fn get_exchange_rate_updates_metrics() { - let metrics_exporter = Arc::new(MetricsExporterMock::default()); - let test_client = TestOracle::new(OracleSourceMock {}, metrics_exporter.clone()); - - let trading_pair = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "USD".to_string() }; - let _bit_usd = test_client.get_exchange_rate(trading_pair.clone()).unwrap(); - - assert_eq!(1, metrics_exporter.get_number_request()); - assert_eq!(1, metrics_exporter.get_response_times().len()); - assert_eq!(1, metrics_exporter.get_exchange_rates().len()); - - let (metric_trading_pair, exchange_rate) = - metrics_exporter.get_exchange_rates().first().unwrap().clone(); - - assert_eq!(trading_pair, metric_trading_pair); - assert_eq!(ExchangeRate::from_num(42.3f32), exchange_rate); - } -} diff --git a/bitacross-worker/app-libs/oracle/src/oracles/mod.rs b/bitacross-worker/app-libs/oracle/src/oracles/mod.rs deleted file mode 100644 index d6100d2469..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracles/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -pub mod exchange_rate_oracle; -pub mod weather_oracle; diff --git a/bitacross-worker/app-libs/oracle/src/oracles/weather_oracle.rs b/bitacross-worker/app-libs/oracle/src/oracles/weather_oracle.rs deleted file mode 100644 index 66809f7f3a..0000000000 --- a/bitacross-worker/app-libs/oracle/src/oracles/weather_oracle.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{metrics_exporter::ExportMetrics, traits::OracleSource, types::WeatherInfo, Error}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use log::*; -use std::sync::Arc; -use url::Url; - -#[allow(unused)] -pub struct WeatherOracle { - oracle_source: OracleSourceType, - metrics_exporter: Arc, -} - -impl WeatherOracle -where - OracleSourceType: OracleSource, -{ - pub fn new(oracle_source: OracleSourceType, metrics_exporter: Arc) -> Self { - WeatherOracle { oracle_source, metrics_exporter } - } - - pub fn get_base_url(&self) -> Result { - self.oracle_source.base_url() - } -} - -pub trait GetLongitude { - type LongitudeResult; - fn get_longitude(&self, weather_info: WeatherInfo) -> Self::LongitudeResult; -} - -impl GetLongitude - for WeatherOracle -where - OracleSourceType: OracleSource>, - MetricsExporter: ExportMetrics, -{ - type LongitudeResult = Result; - - fn get_longitude(&self, weather_info: WeatherInfo) -> Self::LongitudeResult { - let query = weather_info.weather_query.clone(); - - let base_url = self.oracle_source.base_url()?; - let root_certificates = self.oracle_source.root_certificates_content(); - - debug!("Get longitude from URI: {}, query: {:?}", base_url, query); - - let http_client = HttpClient::new( - SendWithCertificateVerification::new(root_certificates), - true, - self.oracle_source.request_timeout(), - None, - None, - ); - let mut rest_client = RestClient::new(http_client, base_url); - >::execute_request( - &mut rest_client, - weather_info, - ) - } -} diff --git a/bitacross-worker/app-libs/oracle/src/test.rs b/bitacross-worker/app-libs/oracle/src/test.rs deleted file mode 100644 index 8d083a18a0..0000000000 --- a/bitacross-worker/app-libs/oracle/src/test.rs +++ /dev/null @@ -1,125 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Integration tests for concrete exchange rate oracle implementations. -//! Uses real HTTP requests, so the sites must be available for these tests. - -use crate::{ - error::Error, - mock::MetricsExporterMock, - oracle_sources::{ - coin_gecko::CoinGeckoSource, coin_market_cap::CoinMarketCapSource, - weather_oracle_source::WeatherOracleSource, - }, - oracles::{ - exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - weather_oracle::{GetLongitude, WeatherOracle}, - }, - traits::OracleSource, - types::{TradingInfo, TradingPair, WeatherInfo, WeatherQuery}, -}; -use core::assert_matches::assert_matches; -use std::sync::Arc; -use substrate_fixed::transcendental::ZERO; - -type TestOracle = ExchangeRateOracle; -type TestWeatherOracle = WeatherOracle; - -#[test] -#[ignore = "requires API key for CoinMarketCap"] -fn get_exchange_rate_from_coin_market_cap_works() { - test_suite_exchange_rates::(); -} - -#[test] -#[ignore = "requires external coin gecko service, disabled temporarily"] -fn get_exchange_rate_from_coin_gecko_works() { - test_suite_exchange_rates::(); -} - -#[test] -fn get_longitude_from_open_meteo_works() { - let oracle = create_weather_oracle::(); - let weather_query = - WeatherQuery { latitude: "52.52".into(), longitude: "13.41".into(), hourly: "none".into() }; - // Todo: hourly param is temperature_2m to get temp or relativehumidity_2m to get humidity - let weather_info = WeatherInfo { weather_query }; - let expected_longitude = 13.41f32; - let response_longitude = - oracle.get_longitude(weather_info).expect("Can grab longitude from oracle"); - assert!((response_longitude - expected_longitude) < 0.5); -} - -#[test] -fn get_exchange_rate_for_undefined_coin_market_cap_crypto_currency_fails() { - get_exchange_rate_for_undefined_crypto_currency_fails::(); -} - -#[test] -fn get_exchange_rate_for_undefined_coin_gecko_crypto_currency_fails() { - get_exchange_rate_for_undefined_crypto_currency_fails::(); -} - -fn create_weather_oracle>( -) -> TestWeatherOracle { - let oracle_source = OracleSourceType::default(); - WeatherOracle::new(oracle_source, Arc::new(MetricsExporterMock::default())) -} - -fn create_exchange_rate_oracle>( -) -> TestOracle { - let oracle_source = OracleSourceType::default(); - ExchangeRateOracle::new(oracle_source, Arc::new(MetricsExporterMock::default())) -} - -fn get_exchange_rate_for_undefined_crypto_currency_fails< - OracleSourceType: OracleSource, ->() { - let oracle = create_exchange_rate_oracle::(); - let trading_pair = TradingPair { - crypto_currency: "invalid_coin".to_string(), - fiat_currency: "USD".to_string(), - }; - let result = oracle.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::InvalidCryptoCurrencyId)); -} - -fn test_suite_exchange_rates>() { - let oracle = create_exchange_rate_oracle::(); - let dot_to_usd = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - let dot_usd = oracle.get_exchange_rate(dot_to_usd).unwrap().0; - assert!(dot_usd > 0f32); - let btc_to_usd = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "USD".to_string() }; - let bit_usd = oracle.get_exchange_rate(btc_to_usd).unwrap().0; - assert!(bit_usd > 0f32); - let dot_to_chf = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CHF".to_string() }; - let dot_chf = oracle.get_exchange_rate(dot_to_chf).unwrap().0; - assert!(dot_chf > 0f32); - let bit_to_chf = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "CHF".to_string() }; - let bit_chf = oracle.get_exchange_rate(bit_to_chf).unwrap().0; - - // Ensure that get_exchange_rate returns a positive rate - assert!(dot_usd > ZERO); - - // Ensure that get_exchange_rate returns a valid value by checking - // that the values obtained for DOT/BIT from different exchange rates are the same - assert_eq!((dot_usd / bit_usd).round(), (dot_chf / bit_chf).round()); -} diff --git a/bitacross-worker/app-libs/oracle/src/traits.rs b/bitacross-worker/app-libs/oracle/src/traits.rs deleted file mode 100644 index 1ca1d21428..0000000000 --- a/bitacross-worker/app-libs/oracle/src/traits.rs +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - types::{ExchangeRate, TradingPair}, - Error, -}; -use core::time::Duration; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use std::{string::String, vec::Vec}; -use url::Url; - -pub trait OracleSource: Default { - type OracleRequestResult; - - fn metrics_id(&self) -> String; - - fn request_timeout(&self) -> Option; - - fn base_url(&self) -> Result; - - /// The server's root certificate(s). A valid certificate is required to open a tls connection - fn root_certificates_content(&self) -> Vec; - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result; - - fn execute_request( - rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult; -} diff --git a/bitacross-worker/app-libs/oracle/src/types.rs b/bitacross-worker/app-libs/oracle/src/types.rs deleted file mode 100644 index ef969ccb90..0000000000 --- a/bitacross-worker/app-libs/oracle/src/types.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use codec::{Decode, Encode}; -use std::string::String; -use substrate_fixed::types::U32F32; - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct WeatherInfo { - pub weather_query: WeatherQuery, -} - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct WeatherQuery { - pub longitude: String, - pub latitude: String, - pub hourly: String, -} - -impl WeatherQuery { - pub fn key(self) -> String { - format!("{}/{}", self.latitude, self.longitude) - } -} - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct TradingInfo { - pub trading_pair: TradingPair, - pub exchange_rate: ExchangeRate, -} -/// Market identifier for order -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct TradingPair { - pub crypto_currency: String, - pub fiat_currency: String, -} - -impl TradingPair { - pub fn key(self) -> String { - format!("{}/{}", self.crypto_currency, self.fiat_currency) - } -} - -/// TODO Fix https://github.com/integritee-network/pallets/issues/71 and get it from https://github.com/integritee-network/pallets.git -/// Teeracle types -pub type ExchangeRate = U32F32; -// pub type Coordinate = U32F32; diff --git a/bitacross-worker/cli/Cargo.toml b/bitacross-worker/cli/Cargo.toml index 6e42a13d52..1aa70df536 100644 --- a/bitacross-worker/cli/Cargo.toml +++ b/bitacross-worker/cli/Cargo.toml @@ -61,7 +61,6 @@ sp-core-hashing = "6.0.0" [features] default = [] evm = ["ita-stf/evm_std", "pallet-evm"] -teeracle = [] sidechain = [] offchain-worker = [] production = [] diff --git a/bitacross-worker/cli/demo_teeracle_generic.sh b/bitacross-worker/cli/demo_teeracle_generic.sh deleted file mode 100755 index 8c2de3bf87..0000000000 --- a/bitacross-worker/cli/demo_teeracle_generic.sh +++ /dev/null @@ -1,136 +0,0 @@ -#!/bin/bash -set -euo pipefail - -trap "echo The demo is terminated (SIGINT); exit 1" SIGINT -trap "echo The demo is terminated (SIGTERM); exit 1" SIGTERM - -# Registers a teeracle with the parentchain, and publish some oracle data. -# -# Demo to show that an enclave can update the exchange rate only when -# 1. the enclave is registered at the pallet-teerex. -# 2. and that the code used is reliable -> the enclave has been put the teeracle whitelist via a governance or sudo -# call. -# -# The teeracle's whitelist has to be empty at the start. So the script needs to run with a clean node state. -# A registered mrenclave will be added in the whitelist by a sudo account. Here //Alice -# -# setup: -# run all on localhost: -# integritee-node purge-chain --dev -# integritee-node --dev -lpallet_teeracle=debug,parity_ws=error,aura=error,sc_basic_authorship=error -# integritee-service --clean-reset run (--skip-ra --dev) -# -# then run this script -# -# usage: -# demo_teeracle_generic.sh -p -P -d -i -u -V -C - -while getopts ":p:P:d:i:u:V:C:" opt; do - case $opt in - p) - LITENTRY_RPC_PORT=$OPTARG - ;; - P) - WORKER_1_PORT=$OPTARG - ;; - d) - DURATION=$OPTARG - ;; - i) - INTERVAL=$OPTARG - ;; - u) - LITENTRY_RPC_URL=$OPTARG - ;; - V) - WORKER_1_URL=$OPTARG - ;; - C) - CLIENT_BIN=$OPTARG - ;; - *) - echo "invalid arg ${OPTARG}" - exit 1 - esac -done - -# using default port if none given as arguments -LITENTRY_RPC_PORT=${LITENTRY_RPC_PORT:-9944} -LITENTRY_RPC_URL=${LITENTRY_RPC_URL:-"ws://127.0.0.1"} - -WORKER_1_PORT=${WORKER_1_PORT:-2000} -WORKER_1_URL=${WORKER_1_URL:-"wss://127.0.0.1"} - -CLIENT_BIN=${CLIENT_BIN:-"./../bin/bitacross-cli"} - -DURATION=${DURATION:-48} -INTERVAL=${INTERVAL:-86400} - -LISTEN_TO_ORACLE_EVENTS_CMD="oracle listen-to-oracle-events" -ADD_TO_WHITELIST_CMD="oracle add-to-whitelist" - -echo "Using client binary ${CLIENT_BIN}" -${CLIENT_BIN} --version -echo "Using node uri ${LITENTRY_RPC_URL}:${LITENTRY_RPC_PORT}" -echo "Using trusted-worker uri ${WORKER_1_URL}:${WORKER_1_PORT}" -echo "Using worker data update interval ${INTERVAL}" -echo "Count the update events for ${DURATION}" -echo "" - -OPEN_METEO="https://api.open-meteo.com/" -let "MIN_EXPECTED_NUM_OF_EVENTS=$DURATION/$INTERVAL-3" -echo "Minimum expected number of events with a single oracle source: ${MIN_EXPECTED_NUM_OF_EVENTS}" - -# let "MIN_EXPECTED_NUM_OF_EVENTS_2 = 2*$MIN_EXPECTED_NUM_OF_EVENTS" -# echo "Minimum expected number of events with two oracle sources: ${MIN_EXPECTED_NUM_OF_EVENTS_2}" - -CLIENT="${CLIENT_BIN} -p ${LITENTRY_RPC_PORT} -P ${WORKER_1_PORT} -u ${LITENTRY_RPC_URL} -U ${WORKER_1_URL}" - -echo "* Query on-chain enclave registry:" -${CLIENT} list-workers -echo "" - -# this will always take the first MRENCLAVE found in the registry !! -read MRENCLAVE <<< $($CLIENT list-workers | awk '/ MRENCLAVE: / { print $2; exit }') -echo "Reading MRENCLAVE from worker list: ${MRENCLAVE}" - -[[ -z $MRENCLAVE ]] && { echo "MRENCLAVE is empty. cannot continue" ; exit 1; } -echo "" - -echo "Listen to OracleUpdated events for ${DURATION} seconds. There should be no trusted oracle source!" - -read NO_EVENTS <<< $(${CLIENT} ${LISTEN_TO_ORACLE_EVENTS_CMD} ${DURATION} | awk '/ EVENTS_COUNT: / { print $2; exit }') -echo "Got ${NO_EVENTS} oracle updates when no trusted oracle source is in the whitelist" -echo "" - -echo "Add ${OPEN_METEO} for ${MRENCLAVE} as trusted oracle source" -${CLIENT} ${ADD_TO_WHITELIST_CMD} //Alice ${OPEN_METEO} ${MRENCLAVE} -echo "MRENCLAVE in whitelist for ${OPEN_METEO}" -echo "" - -echo "Listen to OracleUpdated events for ${DURATION} seconds, after a trusted oracle source has been added to the whitelist." -#${CLIENT} ${LISTEN_TO_ORACLE_EVENTS_CMD} ${DURATION} -#echo "" - -read EVENTS_COUNT <<< $($CLIENT ${LISTEN_TO_ORACLE_EVENTS_CMD} ${DURATION} | awk '/ EVENTS_COUNT: / { print $2; exit }') -echo "Got ${EVENTS_COUNT} oracle updates from the trusted oracle source in ${DURATION} second(s)" -echo "" - -echo "Results :" - -# the following test is for automated CI -# it only works if the teeracle's whitelist is empty at the start (run it from genesis) -if [ $EVENTS_COUNT -ge $MIN_EXPECTED_NUM_OF_EVENTS ]; then - if [ 0 -eq $NO_EVENTS ]; then - echo "test passed" - exit 0 - else - echo "The test ran through but we received OracleUpdated events before the enclave was added to the whitelist. Was the enclave previously whitelisted? Perhaps by another teeracle?" - exit 1 - fi -else -echo "test failed: Not enough events received for single oracle source: $EVENTS_COUNT. Should be greater than $MIN_EXPECTED_NUM_OF_EVENTS" -exit 1 -fi - -exit 1 diff --git a/bitacross-worker/cli/demo_teeracle_whitelist.sh b/bitacross-worker/cli/demo_teeracle_whitelist.sh deleted file mode 100755 index cfe48f8545..0000000000 --- a/bitacross-worker/cli/demo_teeracle_whitelist.sh +++ /dev/null @@ -1,157 +0,0 @@ -#!/bin/bash -set -euo pipefail - -trap "echo The demo is terminated (SIGINT); exit 1" SIGINT -trap "echo The demo is terminated (SIGTERM); exit 1" SIGTERM - -# Registers a teeracle with the parentchain, and publish some oracle data. -# -# Demo to show that an enclave can update the exchange rate only when -# 1. the enclave is registered at the pallet-teerex. -# 2. and that the code used is reliable -> the enclave has been put the teeracle whitelist via a governance or sudo -# call. -# -# The teeracle's whitelist has to be empty at the start. So the script needs to run with a clean node state. -# A registered mrenclave will be added in the whitelist by a sudo account. Here //Alice -# -# setup: -# run all on localhost: -# integritee-node purge-chain --dev -# integritee-node --dev -lpallet_teeracle=debug,parity_ws=error,aura=error,sc_basic_authorship=error -# integritee-service --clean-reset run (--skip-ra --dev) -# -# then run this script -# -# usage: -# demo_teeracle_whitelist.sh -p -P -d -i -u -V -C - -while getopts ":p:P:d:i:u:V:C:" opt; do - case $opt in - p) - LITENTRY_RPC_PORT=$OPTARG - ;; - P) - WORKER_1_PORT=$OPTARG - ;; - d) - DURATION=$OPTARG - ;; - i) - INTERVAL=$OPTARG - ;; - u) - LITENTRY_RPC_URL=$OPTARG - ;; - V) - WORKER_1_URL=$OPTARG - ;; - C) - CLIENT_BIN=$OPTARG - ;; - *) - echo "invalid arg ${OPTARG}" - exit 1 - esac -done - -# using default port if none given as arguments -LITENTRY_RPC_PORT=${LITENTRY_RPC_PORT:-9944} -LITENTRY_RPC_URL=${LITENTRY_RPC_URL:-"ws://127.0.0.1"} - -WORKER_1_PORT=${WORKER_1_PORT:-2000} -WORKER_1_URL=${WORKER_1_URL:-"wss://127.0.0.1"} - -CLIENT_BIN=${CLIENT_BIN:-"./../bin/bitacross-cli"} - -DURATION=${DURATION:-48} -INTERVAL=${INTERVAL:-86400} - -LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD="oracle listen-to-exchange-rate-events" -ADD_TO_WHITELIST_CMD="oracle add-to-whitelist" - -echo "Using client binary ${CLIENT_BIN}" -${CLIENT_BIN} --version -echo "Using node uri ${LITENTRY_RPC_URL}:${LITENTRY_RPC_PORT}" -echo "Using trusted-worker uri ${WORKER_1_URL}:${WORKER_1_PORT}" -echo "Using worker market data update interval ${INTERVAL}" -echo "Count the update events for ${DURATION} blocks" -echo "" - -COIN_GECKO="https://api.coingecko.com/" -COIN_MARKET_CAP="https://pro-api.coinmarketcap.com/" -let "MIN_EXPECTED_NUM_OF_EVENTS=$DURATION*6/$INTERVAL-3" -echo "Minimum expected number of events with a single oracle source: ${MIN_EXPECTED_NUM_OF_EVENTS}" - -let "MIN_EXPECTED_NUM_OF_EVENTS_2 = 2*$MIN_EXPECTED_NUM_OF_EVENTS" -echo "Minimum expected number of events with two oracle sources: ${MIN_EXPECTED_NUM_OF_EVENTS_2}" - -CLIENT="${CLIENT_BIN} -p ${LITENTRY_RPC_PORT} -P ${WORKER_1_PORT} -u ${LITENTRY_RPC_URL} -U ${WORKER_1_URL}" - -echo "* Query on-chain enclave registry:" -${CLIENT} list-workers -echo "" - -# this will always take the first MRENCLAVE found in the registry !! -read MRENCLAVE <<< $($CLIENT list-workers | awk '/ MRENCLAVE: / { print $2; exit }') -echo "Reading MRENCLAVE from worker list: ${MRENCLAVE}" - -[[ -z $MRENCLAVE ]] && { echo "MRENCLAVE is empty. cannot continue" ; exit 1; } -echo "" - -echo "Listen to ExchangeRateUpdated events for ${DURATION} blocks. There should be no trusted oracle source!" -#${CLIENT} ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} -#echo "" - -read NO_EVENTS <<< $(${CLIENT} ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} | awk '/ EVENTS_COUNT: / { print $2; exit }') -echo "Got ${NO_EVENTS} exchange rate updates when no trusted oracle source is in the whitelist" -echo "" - -echo "Add ${COIN_GECKO} for ${MRENCLAVE} as trusted oracle source" -${CLIENT} ${ADD_TO_WHITELIST_CMD} //Alice ${COIN_GECKO} ${MRENCLAVE} -echo "MRENCLAVE in whitelist for ${COIN_GECKO}" -echo "" - -echo "Listen to ExchangeRateUpdated events for ${DURATION} blocks, after a trusted oracle source has been added to the whitelist." -#${CLIENT} ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} -#echo "" - -read EVENTS_COUNT <<< $($CLIENT ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} | awk '/ EVENTS_COUNT: / { print $2; exit }') -echo "Got ${EVENTS_COUNT} exchange rate updates from the trusted oracle source in ${DURATION} blocks(s)" -echo "" - -echo "Add ${COIN_MARKET_CAP} for ${MRENCLAVE} as trusted oracle source" -${CLIENT} ${ADD_TO_WHITELIST_CMD} //Alice ${COIN_MARKET_CAP} ${MRENCLAVE} -echo "MRENCLAVE in whitelist for ${COIN_MARKET_CAP}" -echo "" - -echo "Listen to ExchangeRateUpdated events for ${DURATION} blocks, after a second trusted oracle source has been added to the whitelist." -#${CLIENT} ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} -#echo "" - -read EVENTS_COUNT_2 <<< $($CLIENT ${LISTEN_TO_EXCHANGE_RATE_EVENTS_CMD} ${DURATION} | awk '/ EVENTS_COUNT: / { print $2; exit }') -echo "Got ${EVENTS_COUNT_2} exchange rate updates from 2 trusted oracle sources in ${DURATION} blocks(s)" -echo "" - -echo "Results :" - -# the following test is for automated CI -# it only works if the teeracle's whitelist is empty at the start (run it from genesis) -if [ $EVENTS_COUNT_2 -ge $MIN_EXPECTED_NUM_OF_EVENTS_2 ]; then - if [ $EVENTS_COUNT -ge $MIN_EXPECTED_NUM_OF_EVENTS ]; then - if [ 0 -eq $NO_EVENTS ]; then - echo "test passed" - exit 0 - else - echo "The test ran through but we received ExchangeRateUpdated events before the enclave was added to the whitelist. Was the enclave previously whitelisted? Perhaps by another teeracle?" - exit 1 - fi - else - echo "test failed: Not enough events received for single oracle source: $EVENTS_COUNT. Should be greater than $MIN_EXPECTED_NUM_OF_EVENTS" - exit 1 - fi -else - echo "test failed: Not enough events received for 2 oracle sources: $EVENTS_COUNT_2. Should be greater than $MIN_EXPECTED_NUM_OF_EVENTS_2" - exit 1 -fi - -exit 1 diff --git a/bitacross-worker/cli/src/commands.rs b/bitacross-worker/cli/src/commands.rs index e01a79d930..17b5ea42c4 100644 --- a/bitacross-worker/cli/src/commands.rs +++ b/bitacross-worker/cli/src/commands.rs @@ -19,9 +19,6 @@ extern crate chrono; use crate::{base_cli::BaseCommand, trusted_cli::TrustedCli, Cli, CliResult, CliResultOk}; use clap::Subcommand; -#[cfg(feature = "teeracle")] -use crate::oracle::OracleCommand; - use crate::attesteer::AttesteerCommand; #[derive(Subcommand)] @@ -33,11 +30,6 @@ pub enum Commands { #[clap(after_help = "stf subcommands depend on the stf crate this has been built against")] Trusted(TrustedCli), - /// Subcommands for the oracle. - #[cfg(feature = "teeracle")] - #[clap(subcommand)] - Oracle(OracleCommand), - /// Subcommand for the attesteer. #[clap(subcommand)] Attesteer(AttesteerCommand), @@ -47,11 +39,6 @@ pub fn match_command(cli: &Cli) -> CliResult { match &cli.command { Commands::Base(cmd) => cmd.run(cli), Commands::Trusted(trusted_cli) => trusted_cli.run(cli), - #[cfg(feature = "teeracle")] - Commands::Oracle(cmd) => { - cmd.run(cli); - Ok(CliResultOk::None) - }, Commands::Attesteer(cmd) => { cmd.run(cli); Ok(CliResultOk::None) diff --git a/bitacross-worker/cli/src/lib.rs b/bitacross-worker/cli/src/lib.rs index 0738cc6dd4..8acf785bb3 100644 --- a/bitacross-worker/cli/src/lib.rs +++ b/bitacross-worker/cli/src/lib.rs @@ -35,8 +35,6 @@ mod command_utils; mod error; #[cfg(feature = "evm")] mod evm; -#[cfg(feature = "teeracle")] -mod oracle; mod trusted_base_cli; mod trusted_cli; mod trusted_command_utils; @@ -61,7 +59,6 @@ pub(crate) const ED25519_KEY_TYPE: KeyTypeId = KeyTypeId(*b"ed25"); #[clap(version = VERSION)] #[clap(author = "Trust Computing GmbH ")] #[clap(about = "cli tool to interact with litentry-parachain and workers", long_about = None)] -#[cfg_attr(feature = "teeracle", clap(about = "interact with litentry-parachain and teeracle", long_about = None))] #[cfg_attr(feature = "sidechain", clap(about = "interact with litentry-parachain and sidechain", long_about = None))] #[cfg_attr(feature = "offchain-worker", clap(about = "interact with litentry-parachain and offchain-worker", long_about = None))] #[clap(after_help = "stf subcommands depend on the stf crate this has been built against")] diff --git a/bitacross-worker/cli/src/oracle/commands/add_to_whitelist.rs b/bitacross-worker/cli/src/oracle/commands/add_to_whitelist.rs deleted file mode 100644 index 98afeb801d..0000000000 --- a/bitacross-worker/cli/src/oracle/commands/add_to_whitelist.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - command_utils::{get_chain_api, get_pair_from_str, mrenclave_from_base58}, - Cli, -}; -use itp_node_api::api_client::{ADD_TO_WHITELIST, TEERACLE}; -use substrate_api_client::{ - ac_compose_macros::{compose_call, compose_extrinsic}, - SubmitAndWatch, XtStatus, -}; - -/// Add a trusted market data source to the on-chain whitelist. -#[derive(Debug, Clone, Parser)] -pub struct AddToWhitelistCmd { - /// Sender's on-chain AccountId in ss58check format. - /// - /// It has to be a sudo account. - from: String, - - /// Market data URL - source: String, - - /// MRENCLAVE of the oracle worker base58 encoded. - mrenclave: String, -} - -impl AddToWhitelistCmd { - pub fn run(&self, cli: &Cli) { - let mut api = get_chain_api(cli); - let mrenclave = mrenclave_from_base58(&self.mrenclave); - let from = get_pair_from_str(&self.from); - - let market_data_source = self.source.clone(); - - api.set_signer(from.into()); - - let call = compose_call!( - api.metadata(), - TEERACLE, - ADD_TO_WHITELIST, - market_data_source, - mrenclave - ); - - // compose the extrinsic - let xt = compose_extrinsic!(api, "Sudo", "sudo", call); - - let report = api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).unwrap(); - println!("[+] Add to whitelist got finalized. Hash: {:?}\n", report.extrinsic_hash); - } -} diff --git a/bitacross-worker/cli/src/oracle/commands/listen_to_exchange.rs b/bitacross-worker/cli/src/oracle/commands/listen_to_exchange.rs deleted file mode 100644 index 181be4febd..0000000000 --- a/bitacross-worker/cli/src/oracle/commands/listen_to_exchange.rs +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{command_utils::get_chain_api, Cli}; -use itp_node_api::api_client::ParentchainApi; -use itp_time_utils::{duration_now, remaining_time}; -use log::{debug, info, trace}; -use my_node_runtime::{Hash, RuntimeEvent}; -use std::time::Duration; -use substrate_api_client::SubscribeEvents; - -/// Listen to exchange rate events. -#[derive(Debug, Clone, Parser)] -pub struct ListenToExchangeRateEventsCmd { - /// Listen for `duration` in seconds. - duration: u64, -} - -impl ListenToExchangeRateEventsCmd { - pub fn run(&self, cli: &Cli) { - let api = get_chain_api(cli); - let duration = Duration::from_secs(self.duration); - - let count = count_exchange_rate_update_events(&api, duration); - - println!("Number of ExchangeRateUpdated events received : "); - println!(" EVENTS_COUNT: {}", count); - } -} - -pub fn count_exchange_rate_update_events(api: &ParentchainApi, duration: Duration) -> u32 { - let stop = duration_now() + duration; - - //subscribe to events - let mut subscription = api.subscribe_events().unwrap(); - let mut count = 0; - - while remaining_time(stop).unwrap_or_default() > Duration::ZERO { - let events_result = subscription.next_events::().unwrap(); - if let Ok(events) = events_result { - for event_record in &events { - info!("received event {:?}", event_record.event); - if let RuntimeEvent::Teeracle(event) = &event_record.event { - match &event { - my_node_runtime::pallet_teeracle::Event::ExchangeRateUpdated( - data_source, - trading_pair, - exchange_rate, - ) => { - count += 1; - debug!("Received ExchangeRateUpdated event"); - println!( - "ExchangeRateUpdated: TRADING_PAIR : {}, SRC : {}, VALUE :{:?}", - trading_pair, data_source, exchange_rate - ); - }, - _ => trace!("ignoring teeracle event: {:?}", event), - } - } - } - } - } - debug!("Received {} ExchangeRateUpdated event(s) in total", count); - count -} diff --git a/bitacross-worker/cli/src/oracle/commands/listen_to_oracle.rs b/bitacross-worker/cli/src/oracle/commands/listen_to_oracle.rs deleted file mode 100644 index 87cc334040..0000000000 --- a/bitacross-worker/cli/src/oracle/commands/listen_to_oracle.rs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{command_utils::get_chain_api, Cli}; -use itp_node_api::api_client::ParentchainApi; -use itp_time_utils::{duration_now, remaining_time}; -use log::{debug, info}; -use my_node_runtime::{Hash, RuntimeEvent}; -use std::time::Duration; -use substrate_api_client::{ac_node_api::EventRecord, SubscribeEvents}; - -/// Listen to exchange rate events. -#[derive(Debug, Clone, Parser)] -pub struct ListenToOracleEventsCmd { - /// Listen for `duration` in seconds. - duration: u64, -} - -type EventCount = u32; -type Event = EventRecord; - -impl ListenToOracleEventsCmd { - pub fn run(&self, cli: &Cli) { - let api = get_chain_api(cli); - let duration = Duration::from_secs(self.duration); - let count = count_oracle_update_events(&api, duration); - println!("Number of Oracle events received : "); - println!(" EVENTS_COUNT: {}", count); - } -} - -fn count_oracle_update_events(api: &ParentchainApi, duration: Duration) -> EventCount { - let stop = duration_now() + duration; - - //subscribe to events - let mut subscription = api.subscribe_events().unwrap(); - let mut count = 0; - - while remaining_time(stop).unwrap_or_default() > Duration::ZERO { - let events_result = subscription.next_events::(); - let event_count = match events_result { - Some(Ok(event_records)) => { - debug!("Could not successfully decode event_bytes {:?}", event_records); - report_event_count(event_records) - }, - _ => 0, - }; - count += event_count; - } - debug!("Received {} ExchangeRateUpdated event(s) in total", count); - count -} - -fn report_event_count(event_records: Vec) -> EventCount { - let mut count = 0; - event_records.iter().for_each(|event_record| { - info!("received event {:?}", event_record.event); - if let RuntimeEvent::Teeracle(event) = &event_record.event { - match &event { - my_node_runtime::pallet_teeracle::Event::OracleUpdated( - oracle_data_name, - data_source, - ) => { - count += 1; - debug!("Received OracleUpdated event"); - println!( - "OracleUpdated: ORACLE_NAME : {}, SRC : {}", - oracle_data_name, data_source - ); - }, - // Can just remove this and ignore handling this case - _ => debug!("ignoring teeracle event: {:?}", event), - } - } - }); - count -} diff --git a/bitacross-worker/cli/src/oracle/commands/mod.rs b/bitacross-worker/cli/src/oracle/commands/mod.rs deleted file mode 100644 index 22b0a326c6..0000000000 --- a/bitacross-worker/cli/src/oracle/commands/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -mod add_to_whitelist; -mod listen_to_exchange; -mod listen_to_oracle; - -pub use self::{ - add_to_whitelist::AddToWhitelistCmd, listen_to_exchange::ListenToExchangeRateEventsCmd, - listen_to_oracle::ListenToOracleEventsCmd, -}; diff --git a/bitacross-worker/cli/src/oracle/mod.rs b/bitacross-worker/cli/src/oracle/mod.rs deleted file mode 100644 index e12f117cd4..0000000000 --- a/bitacross-worker/cli/src/oracle/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Add cli commands for the oracle -//! -//! Todo: This shall be a standalone crate in app-libs/oracle. However, this needs: -//! https://github.com/integritee-network/worker/issues/852 - -use crate::Cli; -use commands::{AddToWhitelistCmd, ListenToExchangeRateEventsCmd, ListenToOracleEventsCmd}; - -mod commands; - -/// Oracle subcommands for the cli. -#[derive(Debug, clap::Subcommand)] -pub enum OracleCommand { - /// Add a market source to the teeracle's whitelist. - AddToWhitelist(AddToWhitelistCmd), - - /// Listen to exchange rate events - ListenToExchangeRateEvents(ListenToExchangeRateEventsCmd), - - /// Listen to all oracles event updates - ListenToOracleEvents(ListenToOracleEventsCmd), -} - -impl OracleCommand { - pub fn run(&self, cli: &Cli) { - match self { - OracleCommand::AddToWhitelist(cmd) => cmd.run(cli), - OracleCommand::ListenToExchangeRateEvents(cmd) => cmd.run(cli), - OracleCommand::ListenToOracleEvents(cmd) => cmd.run(cli), - } - } -} diff --git a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs index 2dbb8fb016..f70a36d54b 100644 --- a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs @@ -215,30 +215,6 @@ extern "C" { response_len: u32, ) -> sgx_status_t; - pub fn update_market_data_xt( - eid: sgx_enclave_id_t, - retval: *mut sgx_status_t, - crypto_currency: *const u8, - crypto_currency_size: u32, - fiat_currency: *const u8, - fiat_currency_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, - ) -> sgx_status_t; - - pub fn update_weather_data_xt( - eid: sgx_enclave_id_t, - retval: *mut sgx_status_t, - weather_info_longitude: *const u8, - weather_info_longitude_size: u32, - weather_info_latitude: *const u8, - weather_info_latitude_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, - ) -> sgx_status_t; - pub fn run_state_provisioning_server( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, diff --git a/bitacross-worker/core-primitives/enclave-api/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/src/lib.rs index 38c810624f..463608e111 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/lib.rs @@ -19,7 +19,6 @@ pub mod enclave_test; pub mod error; pub mod remote_attestation; pub mod sidechain; -pub mod teeracle_api; pub mod utils; #[cfg(feature = "implement-ffi")] diff --git a/bitacross-worker/core-primitives/enclave-api/src/teeracle_api.rs b/bitacross-worker/core-primitives/enclave-api/src/teeracle_api.rs deleted file mode 100644 index 530e2ff127..0000000000 --- a/bitacross-worker/core-primitives/enclave-api/src/teeracle_api.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::EnclaveResult; - -pub trait TeeracleApi: Send + Sync + 'static { - /// Update the currency market data for the token oracle. - fn update_market_data_xt( - &self, - crypto_currency: &str, - fiat_currency: &str, - ) -> EnclaveResult>; - - /// Update weather data for the corresponding coordinates. - fn update_weather_data_xt(&self, longitude: &str, latitude: &str) -> EnclaveResult>; -} - -#[cfg(feature = "implement-ffi")] -mod impl_ffi { - use super::TeeracleApi; - use crate::{error::Error, Enclave, EnclaveResult}; - use codec::Encode; - use frame_support::ensure; - use itp_enclave_api_ffi as ffi; - use log::*; - use sgx_types::*; - impl TeeracleApi for Enclave { - fn update_market_data_xt( - &self, - crypto_currency: &str, - fiat_currency: &str, - ) -> EnclaveResult> { - info!( - "TeeracleApi update_market_data_xt in with crypto {} and fiat {}", - crypto_currency, fiat_currency - ); - let mut retval = sgx_status_t::SGX_SUCCESS; - let response_max_len = 8192; - let mut response: Vec = vec![0u8; response_max_len as usize]; - let mut response_len: u32 = 0; - - let crypto_curr = crypto_currency.encode(); - let fiat_curr = fiat_currency.encode(); - - let res = unsafe { - ffi::update_market_data_xt( - self.eid, - &mut retval, - crypto_curr.as_ptr(), - crypto_curr.len() as u32, - fiat_curr.as_ptr(), - fiat_curr.len() as u32, - response.as_mut_ptr(), - response_max_len, - &mut response_len as *mut u32, - ) - }; - - ensure!(res == sgx_status_t::SGX_SUCCESS, Error::Sgx(res)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - - Ok(Vec::from(&response[..response_len as usize])) - } - fn update_weather_data_xt( - &self, - longitude: &str, - latitude: &str, - ) -> EnclaveResult> { - info!( - "TeeracleApi update_weather_data_xt in with latitude: {}, longitude: {}", - latitude, longitude - ); - let mut retval = sgx_status_t::SGX_SUCCESS; - let response_max_len = 8192; - let mut response: Vec = vec![0u8; response_max_len as usize]; - let mut response_len: u32 = 0; - - let longitude_encoded: Vec = longitude.encode(); - let latitude_encoded: Vec = latitude.encode(); - - let res = unsafe { - ffi::update_weather_data_xt( - self.eid, - &mut retval, - longitude_encoded.as_ptr(), - longitude_encoded.len() as u32, - latitude_encoded.as_ptr(), - latitude_encoded.len() as u32, - response.as_mut_ptr(), - response_max_len, - &mut response_len as *mut u32, - ) - }; - - ensure!(res == sgx_status_t::SGX_SUCCESS, Error::Sgx(res)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - Ok(Vec::from(&response[..response_len as usize])) - } - } -} diff --git a/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs b/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs index ae7f253adc..9d080b1243 100644 --- a/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs @@ -27,10 +27,6 @@ extern crate sgx_tstd as std; use codec::{Decode, Encode}; use core::time::Duration; use std::string::String; -use substrate_fixed::types::U32F32; - -// FIXME: Copied from ita-oracle because of cyclic deps. Should be removed after integritee-network/pallets#71 -pub type ExchangeRate = U32F32; #[derive(Encode, Decode, Debug)] pub enum EnclaveMetric { @@ -38,7 +34,6 @@ pub enum EnclaveMetric { TopPoolSizeSet(u64), TopPoolSizeIncrement, TopPoolSizeDecrement, - ExchangeRateOracle(ExchangeRateOracleMetric), SuccessfulTrustedOperationIncrement(String), FailedTrustedOperationIncrement(String), ParentchainBlockImportTime(Duration), @@ -47,22 +42,4 @@ pub enum EnclaveMetric { SidechainSlotStfExecutionTime(Duration), SidechainSlotBlockCompositionTime(Duration), SidechainBlockBroadcastingTime(Duration), - // OracleMetric(OracleMetric), -} - -#[derive(Encode, Decode, Debug)] -pub enum ExchangeRateOracleMetric { - /// Exchange Rate from CoinGecko - (Source, TradingPair, ExchangeRate) - ExchangeRate(String, String, ExchangeRate), - /// Response time of the request in [ms]. (Source, ResponseTime) - ResponseTime(String, u128), - /// Increment the number of requests (Source) - NumberRequestsIncrement(String), -} - -#[derive(Encode, Decode, Debug)] -pub enum OracleMetric { - OracleSpecificMetric(MetricsInfo), - ResponseTime(String, u128), - NumberRequestsIncrement(String), } diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index 2829b53c1c..f56b639e19 100644 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -21,12 +21,10 @@ pub use substrate_api_client::{api::Error as ApiClientError, rpc::TungsteniteRpc pub mod account; pub mod chain; -pub mod pallet_teeracle; pub mod pallet_teerex; pub use account::*; pub use chain::*; -pub use pallet_teeracle::*; pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs deleted file mode 100644 index 3f1ad2d198..0000000000 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub const TEERACLE: &str = "Teeracle"; -pub const ADD_TO_WHITELIST: &str = "add_to_whitelist"; diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs index 0a069c0277..4bbe919a87 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs @@ -37,7 +37,6 @@ pub mod pallet_imp; pub mod pallet_proxy; pub mod pallet_sidechain; pub mod pallet_system; -pub mod pallet_teeracle; pub mod pallet_teerex; pub mod pallet_utility; pub mod pallet_vcmp; diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs deleted file mode 100644 index 0d10003514..0000000000 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, NodeMetadata}; - -/// Pallet' name: -pub const TEERACLE: &str = "Teeracle"; - -pub trait TeeracleCallIndexes { - fn add_to_whitelist_call_indexes(&self) -> Result<[u8; 2]>; - fn remove_from_whitelist_call_indexes(&self) -> Result<[u8; 2]>; - fn update_exchange_rate_call_indexes(&self) -> Result<[u8; 2]>; - fn update_oracle_call_indexes(&self) -> Result<[u8; 2]>; -} - -impl TeeracleCallIndexes for NodeMetadata { - fn add_to_whitelist_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "add_to_whitelist") - } - - fn remove_from_whitelist_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "remove_from_whitelist") - } - - fn update_exchange_rate_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "update_exchange_rate") - } - - fn update_oracle_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "update_oracle") - } -} diff --git a/bitacross-worker/core-primitives/settings/Cargo.toml b/bitacross-worker/core-primitives/settings/Cargo.toml index bf48cd4ec2..f430a521be 100644 --- a/bitacross-worker/core-primitives/settings/Cargo.toml +++ b/bitacross-worker/core-primitives/settings/Cargo.toml @@ -11,4 +11,3 @@ edition = "2021" production = [] sidechain = [] offchain-worker = [] -teeracle = [] diff --git a/bitacross-worker/core-primitives/settings/src/lib.rs b/bitacross-worker/core-primitives/settings/src/lib.rs index bc3ca98dcf..1ca8960c68 100644 --- a/bitacross-worker/core-primitives/settings/src/lib.rs +++ b/bitacross-worker/core-primitives/settings/src/lib.rs @@ -19,14 +19,8 @@ #![no_std] -#[cfg(any( - all(feature = "sidechain", feature = "offchain-worker"), - all(feature = "sidechain", feature = "teeracle"), - all(feature = "teeracle", feature = "offchain-worker") -))] -compile_error!( - "feature \"sidechain\" , \"offchain-worker\" or \"teeracle\" cannot be enabled at the same time" -); +#[cfg(any(all(feature = "sidechain", feature = "offchain-worker"),))] +compile_error!("feature \"sidechain\" or \"offchain-worker\" cannot be enabled at the same time"); pub mod worker_mode; @@ -107,14 +101,3 @@ pub mod sidechain { /// Settings concerning the enclave pub mod enclave {} - -/// Settings for the Teeracle -pub mod teeracle { - use core::time::Duration; - // Send extrinsic to update market exchange rate on the parentchain once per day - pub static DEFAULT_MARKET_DATA_UPDATE_INTERVAL: Duration = ONE_DAY; - - pub static ONE_DAY: Duration = Duration::from_secs(86400); - - pub static THIRTY_MINUTES: Duration = Duration::from_secs(1800); -} diff --git a/bitacross-worker/core-primitives/settings/src/worker_mode.rs b/bitacross-worker/core-primitives/settings/src/worker_mode.rs index 7eef1144fa..9ce3ba1c5a 100644 --- a/bitacross-worker/core-primitives/settings/src/worker_mode.rs +++ b/bitacross-worker/core-primitives/settings/src/worker_mode.rs @@ -19,7 +19,6 @@ pub enum WorkerMode { OffChainWorker, Sidechain, - Teeracle, } pub trait ProvideWorkerMode { @@ -36,13 +35,6 @@ impl ProvideWorkerMode for WorkerModeProvider { } } -#[cfg(feature = "teeracle")] -impl ProvideWorkerMode for WorkerModeProvider { - fn worker_mode() -> WorkerMode { - WorkerMode::Teeracle - } -} - #[cfg(feature = "sidechain")] impl ProvideWorkerMode for WorkerModeProvider { fn worker_mode() -> WorkerMode { @@ -51,7 +43,7 @@ impl ProvideWorkerMode for WorkerModeProvider { } // Default to `Sidechain` worker mode when no cargo features are set. -#[cfg(not(any(feature = "sidechain", feature = "teeracle", feature = "offchain-worker")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] impl ProvideWorkerMode for WorkerModeProvider { fn worker_mode() -> WorkerMode { WorkerMode::Sidechain diff --git a/bitacross-worker/core-primitives/top-pool-author/Cargo.toml b/bitacross-worker/core-primitives/top-pool-author/Cargo.toml index 191ae19ea0..b5f8d264e4 100644 --- a/bitacross-worker/core-primitives/top-pool-author/Cargo.toml +++ b/bitacross-worker/core-primitives/top-pool-author/Cargo.toml @@ -75,4 +75,3 @@ test = ["itp-test/sgx", "itp-top-pool/mocks"] mocks = ["lazy_static"] sidechain = [] offchain-worker = [] -teeracle = [] diff --git a/bitacross-worker/core-primitives/top-pool-author/src/author.rs b/bitacross-worker/core-primitives/top-pool-author/src/author.rs index 08cbd61ff7..a123d72491 100644 --- a/bitacross-worker/core-primitives/top-pool-author/src/author.rs +++ b/bitacross-worker/core-primitives/top-pool-author/src/author.rs @@ -68,15 +68,10 @@ pub type AuthorTopFilter = crate::top_filter::IndirectCallsOnlyFilter = crate::top_filter::DenyAllFilter; -#[cfg(feature = "teeracle")] // Teeracle currently does not process any trusted operations -pub type AuthorTopFilter = crate::top_filter::DenyAllFilter; -#[cfg(feature = "teeracle")] -pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; - -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker", feature = "teeracle")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] pub type AuthorTopFilter = crate::top_filter::CallsOnlyFilter; -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker", feature = "teeracle")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; /// Currently we treat all RPC operations as externals. diff --git a/bitacross-worker/docker/README.md b/bitacross-worker/docker/README.md index 7f9ddb7a86..09ee9bb415 100644 --- a/bitacross-worker/docker/README.md +++ b/bitacross-worker/docker/README.md @@ -55,16 +55,6 @@ Run docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < demo-sidechain.yml) up demo-sidechain --exit-code-from demo-sidechain ``` -### Demo Teeracle -Build -``` -COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < demo-teeracle.yml) build --build-arg WORKER_MODE_ARG=teeracle -``` -Run -``` -docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < demo-teeracle.yml) up demo-teeracle --exit-code-from demo-teeracle -``` - ## Run the benchmarks Build with diff --git a/bitacross-worker/docker/demo-teeracle-generic.yml b/bitacross-worker/docker/demo-teeracle-generic.yml deleted file mode 100644 index 4ff30dafdf..0000000000 --- a/bitacross-worker/docker/demo-teeracle-generic.yml +++ /dev/null @@ -1,68 +0,0 @@ -# Teeracle Demo Setup -# -# The demo is parameterized with the interval that the teeracle uses to query its sources. -# Set the `TEERACLE_INTERVAL_SECONDS` variable when invoking, e.g. `TEERACLE_INTERVAL_SECONDS=4 docker compose -f docker-compose.yml -f demo-teeracle-generic.yml up --exit-code-from demo-teeracle-generic` -# Set the `ADDITIONAL_RUNTIME_FLAGS` variable to for additional flags. -# To skip remote attestation: `export ADDITIONAL_RUNTIME_FLAG="--skip-ra"` -services: - integritee-teeracle-worker-${VERSION}: - image: integritee-worker:${VERSION:-dev} - hostname: integritee-teeracle-worker - devices: - - "${SGX_PROVISION:-/dev/null}:/dev/sgx/provision" - - "${SGX_ENCLAVE:-/dev/null}:/dev/sgx/enclave" - volumes: - - "${AESMD:-/dev/null}:/var/run/aesmd" - - "${SGX_QCNL:-/dev/null}:/etc/sgx_default_qcnl.conf" - build: - context: ${PWD}/.. - dockerfile: build.Dockerfile - target: deployed-worker - depends_on: - integritee-node-${VERSION}: - condition: service_healthy - environment: - - RUST_LOG=warn,ws=warn,sp_io=warn,substrate_api_client=warn,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=warn,integritee_service=info,integritee_service::teeracle=debug,ita_stf=warn,ita_oracle=debug - networks: - - integritee-test-network - healthcheck: - test: curl -s -f http://integritee-teeracle-worker:4645/is_initialized || exit 1 - interval: 10s - timeout: 10s - retries: 25 - command: - "--clean-reset --ws-external -M integritee-teeracle-worker -T wss://integritee-teeracle-worker - -u ws://integritee-node -U ws://integritee-teeracle-worker -P 2011 -w 2101 -p 9912 -h 4645 - run --dev ${ADDITIONAL_RUNTIME_FLAGS} --teeracle-interval ${TEERACLE_INTERVAL_SECONDS}s" - restart: always - demo-teeracle-generic: - image: bitacross-cli:${VERSION:-dev} - devices: - - "${SGX_PROVISION:-/dev/null}:/dev/sgx/provision" - - "${SGX_ENCLAVE:-/dev/null}:/dev/sgx/enclave" - volumes: - - "${AESMD:-/dev/null}:/var/run/aesmd" - - "${SGX_QCNL:-/dev/null}:/etc/sgx_default_qcnl.conf" - build: - context: ${PWD}/.. - dockerfile: build.Dockerfile - target: deployed-client - depends_on: - integritee-node-${VERSION}: - condition: service_healthy - integritee-teeracle-worker-${VERSION}: - condition: service_healthy - environment: - - RUST_LOG=warn,sp_io=warn,integritee_cli::exchange_oracle=debug - networks: - - integritee-test-network - entrypoint: - "/usr/local/worker-cli/demo_teeracle_generic.sh - -u ws://integritee-node -p 9912 - -V wss://integritee-teeracle-worker -P 2011 - -d 21 -i ${TEERACLE_INTERVAL_SECONDS} - -C /usr/local/bin/bitacross-cli 2>&1" - restart: "no" -networks: - integritee-test-network: - driver: bridge diff --git a/bitacross-worker/docker/demo-teeracle.yml b/bitacross-worker/docker/demo-teeracle.yml deleted file mode 100644 index d71b36df6b..0000000000 --- a/bitacross-worker/docker/demo-teeracle.yml +++ /dev/null @@ -1,71 +0,0 @@ -# Teeracle Demo Setup -# -# The demo is parameterized with the interval that the teeracle uses to query its sources. -# Set the `TEERACLE_INTERVAL_SECONDS` variable when invoking, e.g. `TEERACLE_INTERVAL_SECONDS=4 docker compose -f docker-compose.yml -f demo-teeracle.yml up --exit-code-from demo-teeracle` -# This setup requires an API key for CoinMarketCap -# Add the API key to the environment variable `COINMARKETCAP_KEY`, with `export COINMARKETCAP_KEY=` -# Set the `ADDITIONAL_RUNTIME_FLAGS` variable to for additional flags. -# To skip remote attestation: `export ADDITIONAL_RUNTIME_FLAG="--skip-ra"` -services: - integritee-teeracle-worker-${VERSION}: - image: integritee-worker:${VERSION:-dev} - hostname: integritee-teeracle-worker - devices: - - "${SGX_PROVISION:-/dev/null}:/dev/sgx/provision" - - "${SGX_ENCLAVE:-/dev/null}:/dev/sgx/enclave" - volumes: - - "${AESMD:-/dev/null}:/var/run/aesmd" - - "${SGX_QCNL:-/dev/null}:/etc/sgx_default_qcnl.conf" - build: - context: ${PWD}/.. - dockerfile: build.Dockerfile - target: deployed-worker - depends_on: - integritee-node-${VERSION}: - condition: service_healthy - environment: - - RUST_LOG=warn,ws=warn,sp_io=warn,substrate_api_client=warn,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=warn,integritee_service=info,integritee_service::teeracle=debug,ita_stf=warn,ita_exchange_oracle=debug - - COINMARKETCAP_KEY - networks: - - integritee-test-network - healthcheck: - test: curl -s -f http://integritee-teeracle-worker:4645/is_initialized || exit 1 - interval: 10s - timeout: 10s - retries: 25 - command: - "--clean-reset --ws-external -M integritee-teeracle-worker -T wss://integritee-teeracle-worker - -u ws://integritee-node -U ws://integritee-teeracle-worker -P 2011 -w 2101 -p 9912 -h 4645 - run --dev ${ADDITIONAL_RUNTIME_FLAGS} --teeracle-interval ${TEERACLE_INTERVAL_SECONDS}s" - restart: always - demo-teeracle: - image: bitacross-cli:${VERSION:-dev} - devices: - - "${SGX_PROVISION:-/dev/null}:/dev/sgx/provision" - - "${SGX_ENCLAVE:-/dev/null}:/dev/sgx/enclave" - volumes: - - "${AESMD:-/dev/null}:/var/run/aesmd" - - "${SGX_QCNL:-/dev/null}:/etc/sgx_default_qcnl.conf" - build: - context: ${PWD}/.. - dockerfile: build.Dockerfile - target: deployed-client - depends_on: - integritee-node-${VERSION}: - condition: service_healthy - integritee-teeracle-worker-${VERSION}: - condition: service_healthy - environment: - - RUST_LOG=warn,sp_io=warn,integritee_cli::exchange_oracle=debug - networks: - - integritee-test-network - entrypoint: - "/usr/local/worker-cli/demo_teeracle_whitelist.sh - -u ws://integritee-node -p 9912 - -V wss://integritee-teeracle-worker -P 2011 - -d 7 -i ${TEERACLE_INTERVAL_SECONDS} - -C /usr/local/bin/bitacross-cli 2>&1" - restart: "no" -networks: - integritee-test-network: - driver: bridge diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 8261ba97b2..5506e3e89e 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -845,7 +845,6 @@ dependencies = [ "frame-system", "hex", "ipfs-unixfs", - "ita-oracle", "ita-parentchain-interface", "ita-sgx-runtime", "ita-stf", @@ -1677,19 +1676,6 @@ dependencies = [ "sgx_tstd", ] -[[package]] -name = "http_req" -version = "0.8.1" -source = "git+https://github.com/integritee-network/http_req#3723e88235f2b29bc1a31835853b072ffd0455fd" -dependencies = [ - "log", - "rustls 0.19.0 (git+https://github.com/mesalock-linux/rustls?branch=mesalock_sgx)", - "sgx_tstd", - "unicase", - "webpki", - "webpki-roots 0.21.0 (git+https://github.com/mesalock-linux/webpki-roots?branch=mesalock_sgx)", -] - [[package]] name = "httparse" version = "1.4.1" @@ -1811,23 +1797,6 @@ dependencies = [ "sha2 0.9.9", ] -[[package]] -name = "ita-oracle" -version = "0.9.0" -dependencies = [ - "itc-rest-client", - "itp-enclave-metrics", - "itp-ocall-api", - "lazy_static", - "log", - "parity-scale-codec", - "serde 1.0.193", - "sgx_tstd", - "substrate-fixed", - "thiserror", - "url", -] - [[package]] name = "ita-parentchain-interface" version = "0.9.0" @@ -2083,21 +2052,6 @@ dependencies = [ "sgx_tstd", ] -[[package]] -name = "itc-rest-client" -version = "0.9.0" -dependencies = [ - "base64 0.13.1", - "http", - "http_req", - "log", - "serde 1.0.193", - "serde_json 1.0.107", - "sgx_tstd", - "thiserror", - "url", -] - [[package]] name = "itc-tls-websocket-server" version = "0.9.0" @@ -5092,15 +5046,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "unicase" -version = "2.6.0" -source = "git+https://github.com/mesalock-linux/unicase-sgx#0b0519348572927118af47af3da4da9ffdca8ec6" -dependencies = [ - "sgx_tstd", - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.4" diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 0c25d6ded4..5488ba3106 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -29,11 +29,6 @@ offchain-worker = [ "itp-settings/offchain-worker", "itp-top-pool-author/offchain-worker", ] -teeracle = [ - "ita-oracle", - "itp-settings/teeracle", - "itp-top-pool-author/teeracle", -] test = [ "ita-stf/test", "itc-parentchain/test", @@ -96,7 +91,6 @@ multibase = { default-features = false, git = "https://github.com/whalelephant/r teerex-primitives = { default-features = false, git = "https://github.com/integritee-network/pallets.git", branch = "sdk-v0.12.0-polkadot-v0.9.42" } # local deps -ita-oracle = { path = "../app-libs/oracle", default-features = false, optional = true, features = ["sgx"] } ita-parentchain-interface = { path = "../app-libs/parentchain-interface", default-features = false, features = ["sgx"] } ita-sgx-runtime = { path = "../app-libs/sgx-runtime", default-features = false } ita-stf = { path = "../app-libs/stf", default-features = false, features = ["sgx"] } diff --git a/bitacross-worker/enclave-runtime/Enclave.edl b/bitacross-worker/enclave-runtime/Enclave.edl index 04c02fea61..80d878511b 100644 --- a/bitacross-worker/enclave-runtime/Enclave.edl +++ b/bitacross-worker/enclave-runtime/Enclave.edl @@ -140,20 +140,6 @@ enclave { [out] uint32_t* unchecked_extrinsic_size ); - public sgx_status_t update_market_data_xt( - [in, size=crypto_currency_size] uint8_t* crypto_currency, uint32_t crypto_currency_size, - [in, size=fiat_currency_size] uint8_t* fiat_currency, uint32_t fiat_currency_size, - [out, size=unchecked_extrinsic_max_size] uint8_t* unchecked_extrinsic, uint32_t unchecked_extrinsic_max_size, - [out] uint32_t* unchecked_extrinsic_size - ); - - public sgx_status_t update_weather_data_xt( - [in, size=weather_info_logitude_size] uint8_t* weather_info_logitude, uint32_t weather_info_logitude_size, - [in, size=weather_info_latitude_size] uint8_t* weather_info_latitude, uint32_t weather_info_latitude_size, - [out, size=unchecked_extrinsic_max_size] uint8_t* unchecked_extrinsic, uint32_t unchecked_extrinsic_max_size, - [out] uint32_t* unchecked_extrinsic_size - ); - public sgx_status_t dump_ias_ra_cert_to_disk(); public sgx_status_t dump_dcap_ra_cert_to_disk([in] const sgx_target_info_t* quoting_enclave_target_info, uint32_t quote_size); diff --git a/bitacross-worker/enclave-runtime/src/empty_impls.rs b/bitacross-worker/enclave-runtime/src/empty_impls.rs index e401fa8d05..e011e4d19c 100644 --- a/bitacross-worker/enclave-runtime/src/empty_impls.rs +++ b/bitacross-worker/enclave-runtime/src/empty_impls.rs @@ -22,35 +22,3 @@ pub extern "C" fn test_main_entrance() -> sgx_types::size_t { unreachable!("Tests are not available when compiled in production mode.") } - -/// Empty Teeracle market data implementation. -#[cfg(not(feature = "teeracle"))] -#[no_mangle] -#[allow(clippy::unreachable)] -pub unsafe extern "C" fn update_market_data_xt( - _crypto_currency_ptr: *const u8, - _crypto_currency_size: u32, - _fiat_currency_ptr: *const u8, - _fiat_currency_size: u32, - _unchecked_extrinsic: *mut u8, - _unchecked_extrinsic_max_size: u32, - _unchecked_extrinsic_size: *mut u32, -) -> sgx_types::sgx_status_t { - unreachable!("Cannot update market data, teeracle feature is not enabled.") -} - -/// Empty Teeracle Weather data implementation. -#[cfg(not(feature = "teeracle"))] -#[no_mangle] -#[allow(clippy::unreachable)] -pub unsafe extern "C" fn update_weather_data_xt( - _weather_info_longitude: *const u8, - _weather_info_longitude_size: u32, - _weather_info_latitude: *const u8, - _weather_info_latitude_size: u32, - _unchecked_extrinsic: *mut u8, - _unchecked_extrinsic_max_size: u32, - _unchecked_extrinsic_size: *mut u32, -) -> sgx_types::sgx_status_t { - unreachable!("Cannot update weather data, teeracle feature is not enabled.") -} diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs index f297c4960e..aeeec12a5b 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs @@ -100,8 +100,6 @@ impl IntegriteeParachainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => - Arc::new(IntegriteeParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs index b5ae349479..207115d47f 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs @@ -99,8 +99,6 @@ impl IntegriteeSolochainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => - Arc::new(IntegriteeParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs index bf24f6fdd4..e4a08cce6d 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs @@ -104,8 +104,6 @@ impl TargetAParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetAParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs index f5cf2ae8ff..e26ce6833d 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs @@ -97,8 +97,6 @@ impl TargetASolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetAParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs index be44224c65..36d83a0e06 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs @@ -104,8 +104,6 @@ impl TargetBParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetBParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs index 842baa8129..015ff2cea6 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs @@ -97,8 +97,6 @@ impl TargetBSolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetBParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index 208dcb50f5..d5b0a39c93 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -71,7 +71,7 @@ use itp_component_container::ComponentGetter; use itp_import_queue::PushToQueue; use itp_node_api::metadata::NodeMetadata; use itp_nonce_cache::{MutateNonce, Nonce}; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; +use itp_settings::worker_mode::{ProvideWorkerMode, WorkerModeProvider}; use itp_sgx_crypto::key_repository::AccessPubkey; use itp_storage::{StorageProof, StorageProofChecker}; use itp_types::{ShardIdentifier, SignedBlock}; @@ -99,14 +99,10 @@ mod utils; pub mod error; pub mod rpc; mod sync; -mod tls_ra; -pub mod top_pool_execution; - -#[cfg(feature = "teeracle")] -pub mod teeracle; - #[cfg(feature = "test")] pub mod test; +mod tls_ra; +pub mod top_pool_execution; pub type Hash = sp_core::H256; pub type AuthorityPair = sp_core::ed25519::Pair; @@ -590,10 +586,6 @@ fn dispatch_parentchain_blocks_for_import id: &ParentchainId, is_syncing: bool, ) -> Result<()> { - if WorkerModeProvider::worker_mode() == WorkerMode::Teeracle { - trace!("Not importing any parentchain blocks"); - return Ok(()) - } trace!( "[{:?}] Dispatching Import of {} blocks and {} events", id, diff --git a/bitacross-worker/enclave-runtime/src/teeracle/mod.rs b/bitacross-worker/enclave-runtime/src/teeracle/mod.rs deleted file mode 100644 index c38dd27c2e..0000000000 --- a/bitacross-worker/enclave-runtime/src/teeracle/mod.rs +++ /dev/null @@ -1,279 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - error::{Error, Result}, - initialization::global_components::GLOBAL_OCALL_API_COMPONENT, - utils::{ - get_extrinsic_factory_from_integritee_solo_or_parachain, - get_node_metadata_repository_from_integritee_solo_or_parachain, - }, -}; -use codec::{Decode, Encode}; -use core::slice; -use ita_oracle::{ - create_coin_gecko_oracle, create_coin_market_cap_oracle, create_open_meteo_weather_oracle, - metrics_exporter::ExportMetrics, - oracles::{ - exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - weather_oracle::{GetLongitude, WeatherOracle}, - }, - traits::OracleSource, - types::{TradingInfo, TradingPair, WeatherInfo, WeatherQuery}, -}; -use itp_component_container::ComponentGetter; -use itp_extrinsics_factory::CreateExtrinsics; -use itp_node_api::metadata::{pallet_teeracle::TeeracleCallIndexes, provider::AccessNodeMetadata}; -use itp_types::OpaqueCall; -use itp_utils::write_slice_and_whitespace_pad; -use log::*; -use sgx_types::sgx_status_t; -use sp_runtime::OpaqueExtrinsic; -use std::{string::String, vec::Vec}; - -fn update_weather_data_internal(weather_info: WeatherInfo) -> Result> { - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - - let mut extrinsic_calls: Vec = Vec::new(); - - let open_meteo_weather_oracle = create_open_meteo_weather_oracle(ocall_api); - - match get_longitude(weather_info, open_meteo_weather_oracle) { - Ok(opaque_call) => extrinsic_calls.push(opaque_call), - Err(e) => { - error!("[-] Failed to get the newest longitude from OpenMeteo. {:?}", e); - }, - }; - let extrinsics = extrinsics_factory.create_extrinsics(extrinsic_calls.as_slice(), None)?; - Ok(extrinsics) -} - -fn get_longitude( - weather_info: WeatherInfo, - oracle: WeatherOracle, -) -> Result -where - OracleSourceType: OracleSource< - WeatherInfo, - OracleRequestResult = std::result::Result, - >, - MetricsExporter: ExportMetrics, -{ - let longitude = - oracle.get_longitude(weather_info.clone()).map_err(|e| Error::Other(e.into()))?; - - let base_url = oracle.get_base_url().map_err(|e| Error::Other(e.into()))?; - let source_base_url = base_url.as_str(); - - println!("Update the longitude: {}, for source {}", longitude, source_base_url); - - let node_metadata_repository = - get_node_metadata_repository_from_integritee_solo_or_parachain()?; - - let call_ids = node_metadata_repository - .get_from_metadata(|m| m.update_oracle_call_indexes()) - .map_err(Error::NodeMetadataProvider)? - .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - let call = OpaqueCall::from_tuple(&( - call_ids, - weather_info.weather_query.key().as_bytes().to_vec(), - source_base_url.as_bytes().to_vec(), - longitude.encode(), - )); - - Ok(call) -} - -#[no_mangle] -pub unsafe extern "C" fn update_weather_data_xt( - weather_info_longitude: *const u8, - weather_info_longitude_size: u32, - weather_info_latitude: *const u8, - weather_info_latitude_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, -) -> sgx_status_t { - let mut weather_info_longitude_slice = - slice::from_raw_parts(weather_info_longitude, weather_info_longitude_size as usize); - let longitude = match String::decode(&mut weather_info_longitude_slice) { - Ok(val) => val, - Err(e) => { - error!("Could not decode longitude: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let mut weather_info_latitude_slice = - slice::from_raw_parts(weather_info_latitude, weather_info_latitude_size as usize); - let latitude = match String::decode(&mut weather_info_latitude_slice) { - Ok(val) => val, - Err(e) => { - error!("Could not decode latitude: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let weather_query = WeatherQuery { longitude, latitude, hourly: " ".into() }; - let weather_info = WeatherInfo { weather_query }; - - let extrinsics = match update_weather_data_internal(weather_info) { - Ok(xts) => xts, - Err(e) => { - error!("Updating weather info failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let extrinsic_slice = - slice::from_raw_parts_mut(unchecked_extrinsic, unchecked_extrinsic_max_size as usize); - - // Save created extrinsic as slice in the return value unchecked_extrinsic. - *unchecked_extrinsic_size = - match write_slice_and_whitespace_pad(extrinsic_slice, extrinsics.encode()) { - Ok(l) => l as u32, - Err(e) => { - error!("Copying encoded extrinsics into return slice failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - sgx_status_t::SGX_SUCCESS -} - -/// For now get the crypto/fiat currency exchange rate from coingecko and CoinMarketCap. -#[no_mangle] -pub unsafe extern "C" fn update_market_data_xt( - crypto_currency_ptr: *const u8, - crypto_currency_size: u32, - fiat_currency_ptr: *const u8, - fiat_currency_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, -) -> sgx_status_t { - let mut crypto_currency_slice = - slice::from_raw_parts(crypto_currency_ptr, crypto_currency_size as usize); - #[allow(clippy::unwrap_used)] - let crypto_currency: String = Decode::decode(&mut crypto_currency_slice).unwrap(); - - let mut fiat_currency_slice = - slice::from_raw_parts(fiat_currency_ptr, fiat_currency_size as usize); - #[allow(clippy::unwrap_used)] - let fiat_currency: String = Decode::decode(&mut fiat_currency_slice).unwrap(); - - let extrinsics = match update_market_data_internal(crypto_currency, fiat_currency) { - Ok(xts) => xts, - Err(e) => { - error!("Update market data failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - if extrinsics.is_empty() { - error!("Updating market data yielded no extrinsics"); - return sgx_status_t::SGX_ERROR_UNEXPECTED - } - let extrinsic_slice = - slice::from_raw_parts_mut(unchecked_extrinsic, unchecked_extrinsic_max_size as usize); - - // Save created extrinsic as slice in the return value unchecked_extrinsic. - *unchecked_extrinsic_size = - match write_slice_and_whitespace_pad(extrinsic_slice, extrinsics.encode()) { - Ok(l) => l as u32, - Err(e) => { - error!("Copying encoded extrinsics into return slice failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - sgx_status_t::SGX_SUCCESS -} - -fn update_market_data_internal( - crypto_currency: String, - fiat_currency: String, -) -> Result> { - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - - let mut extrinsic_calls: Vec = Vec::new(); - - // Get the exchange rate - let trading_pair = TradingPair { crypto_currency, fiat_currency }; - - let coin_gecko_oracle = create_coin_gecko_oracle(ocall_api.clone()); - - match get_exchange_rate(trading_pair.clone(), coin_gecko_oracle) { - Ok(opaque_call) => extrinsic_calls.push(opaque_call), - Err(e) => { - error!("[-] Failed to get the newest exchange rate from CoinGecko. {:?}", e); - }, - }; - - let coin_market_cap_oracle = create_coin_market_cap_oracle(ocall_api); - match get_exchange_rate(trading_pair, coin_market_cap_oracle) { - Ok(oc) => extrinsic_calls.push(oc), - Err(e) => { - error!("[-] Failed to get the newest exchange rate from CoinMarketCap. {:?}", e); - }, - }; - - let extrinsics = extrinsics_factory.create_extrinsics(extrinsic_calls.as_slice(), None)?; - Ok(extrinsics) -} - -fn get_exchange_rate( - trading_pair: TradingPair, - oracle: ExchangeRateOracle, -) -> Result -where - OracleSourceType: OracleSource, - MetricsExporter: ExportMetrics, -{ - let (rate, base_url) = oracle - .get_exchange_rate(trading_pair.clone()) - .map_err(|e| Error::Other(e.into()))?; - - let source_base_url = base_url.as_str(); - - println!( - "Update the exchange rate: {} = {:?} for source {}", - trading_pair.clone().key(), - rate, - source_base_url, - ); - - let node_metadata_repository = - get_node_metadata_repository_from_integritee_solo_or_parachain()?; - - let call_ids = node_metadata_repository - .get_from_metadata(|m| m.update_exchange_rate_call_indexes()) - .map_err(Error::NodeMetadataProvider)? - .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - let call = OpaqueCall::from_tuple(&( - call_ids, - source_base_url.as_bytes().to_vec(), - trading_pair.key().as_bytes().to_vec(), - Some(rate), - )); - - Ok(call) -} diff --git a/bitacross-worker/enclave-runtime/src/test/mod.rs b/bitacross-worker/enclave-runtime/src/test/mod.rs index 6f3d7a252e..b3a25415a3 100644 --- a/bitacross-worker/enclave-runtime/src/test/mod.rs +++ b/bitacross-worker/enclave-runtime/src/test/mod.rs @@ -29,6 +29,3 @@ pub mod sidechain_event_tests; mod state_getter_tests; pub mod tests_main; pub mod top_pool_tests; - -#[cfg(feature = "teeracle")] -pub mod teeracle_tests; diff --git a/bitacross-worker/enclave-runtime/src/test/teeracle_tests.rs b/bitacross-worker/enclave-runtime/src/test/teeracle_tests.rs deleted file mode 100644 index bd9a4c8391..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/teeracle_tests.rs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use codec::alloc::string::ToString; -use ita_oracle::{ - create_coin_gecko_oracle, create_coin_market_cap_oracle, - oracles::exchange_rate_oracle::GetExchangeRate, types::TradingPair, -}; -use itp_test::mock::metrics_ocall_mock::MetricsOCallMock; -use std::sync::Arc; - -pub(super) fn test_verify_get_exchange_rate_from_coin_gecko_works() { - // Get the exchange rate - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - - let coin_gecko_oracle = create_coin_gecko_oracle(Arc::new(MetricsOCallMock::default())); - - let result = coin_gecko_oracle.get_exchange_rate(trading_pair.clone()); - assert!(result.is_ok()); -} - -/// Get exchange rate from coin market cap. Requires API key (therefore not suited for unit testing). -#[allow(unused)] -pub(super) fn test_verify_get_exchange_rate_from_coin_market_cap_works() { - // Get the exchange rate - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - - let coin_market_cap_oracle = - create_coin_market_cap_oracle(Arc::new(MetricsOCallMock::default())); - - let result = coin_market_cap_oracle.get_exchange_rate(trading_pair.clone()); - assert!(result.is_ok()); -} diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index 8632bfbeea..0a63f5454f 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -171,23 +171,9 @@ pub extern "C" fn test_main_entrance() -> size_t { // ipfs::test_verification_ok_for_correct_content, // ipfs::test_verification_fails_for_incorrect_content, // test_ocall_read_write_ipfs, - - // Teeracle tests - run_teeracle_tests, ) } -#[cfg(feature = "teeracle")] -fn run_teeracle_tests() { - use super::teeracle_tests::*; - test_verify_get_exchange_rate_from_coin_gecko_works(); - // Disabled - requires API key, cannot run locally - //test_verify_get_exchange_rate_from_coin_market_cap_works(); -} - -#[cfg(not(feature = "teeracle"))] -fn run_teeracle_tests() {} - #[cfg(feature = "evm")] fn run_evm_tests() { evm_pallet_tests::test_evm_call(); diff --git a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs index 33f72e9095..e5fbed0a09 100644 --- a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs +++ b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs @@ -55,8 +55,7 @@ enum ProvisioningPayload { impl From for ProvisioningPayload { fn from(m: WorkerMode) -> Self { match m { - WorkerMode::OffChainWorker | WorkerMode::Teeracle => - ProvisioningPayload::ShieldingKeyAndLightClient, + WorkerMode::OffChainWorker => ProvisioningPayload::ShieldingKeyAndLightClient, WorkerMode::Sidechain => ProvisioningPayload::Everything, } } diff --git a/bitacross-worker/samples/teeracle/README.md b/bitacross-worker/samples/teeracle/README.md deleted file mode 100644 index 05758f7d09..0000000000 --- a/bitacross-worker/samples/teeracle/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Teeracle install into Securitee's kubernetes cluster - -This example is about to install [Integritee's Teeracle](https://docs.integritee.network/3-our-technology/3.5-use-cases/3.5.3-teeracle-oracle-framework). - -*Prerequisites:* - -* Ensure you have access to a Kubernetes cluster with SGX-enabled nodes and kubectl installed and configured. The easiest way to get started is to order Kubernetes from Securitee [Securitee Kubernetes](https://securitee.tech/products/), which offers SGX-enabled nodes. -* You have [Helm](https://helm.sh/docs/intro/install/) installed - -## Kubernetes deployment walkthrough - -We are now installing Teeracle - -### Install steps - - -* Edit the configuration values in file [kubernetes/values.yaml](kubernetes/values.yaml) - ```yaml - app: - url: "wss://rococo.api.integritee.network" - interval: "2m" - ``` -* Install the Teeracle into the cluster - - ```bash - helm install -f ./kubernetes/values.yaml teeracle ./kubernetes --create-namespace -n teeracle - or run - ./install-teeracle.sh - ``` - - -## Misc. - -### SGX Plugin - -If you are running in simulation mode, or are using a different plugin please edit the [kubernetes/templates/teeracle.yaml](kubernetes/templates/teeracle.yaml) - ```yaml - limits: - sgx.intel.com/epc: "10Mi" - sgx.intel.com/enclave: 1 - sgx.intel.com/provision: 1 - ``` - -### PCCS server - -The DCAP attestation requires a running PCCS server - which is provided by Securitee by default that's why we need to mount the ```/etc/sgx_default_qcnl.conf``` config file -see [kubernetes/templates/teeracle.yaml](kubernetes/templates/teeracle.yaml) - ```yaml - volumeMounts: - - name: qcnl - mountPath: /etc/sgx_default_qcnl.conf - ... - volumes: - - name: qcnl - hostPath: - path: /etc/sgx_default_qcnl.conf - - ``` diff --git a/bitacross-worker/samples/teeracle/install-teeracle.sh b/bitacross-worker/samples/teeracle/install-teeracle.sh deleted file mode 100755 index dbc21bc2b4..0000000000 --- a/bitacross-worker/samples/teeracle/install-teeracle.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/env bash - -namespace=teeracle -helm uninstall -n $namespace teeracle - -helm install -f ./kubernetes/values.yaml teeracle ./kubernetes --create-namespace -n $namespace - diff --git a/bitacross-worker/samples/teeracle/kubernetes/Chart.yaml b/bitacross-worker/samples/teeracle/kubernetes/Chart.yaml deleted file mode 100644 index d1f3a9a7f8..0000000000 --- a/bitacross-worker/samples/teeracle/kubernetes/Chart.yaml +++ /dev/null @@ -1,7 +0,0 @@ -apiVersion: v2 -name: teeracle -description: teeracle dcap - -type: application -version: 0.1.0 -appVersion: 1.0.0 \ No newline at end of file diff --git a/bitacross-worker/samples/teeracle/kubernetes/templates/teeracle.yaml b/bitacross-worker/samples/teeracle/kubernetes/templates/teeracle.yaml deleted file mode 100644 index 130ad79cb4..0000000000 --- a/bitacross-worker/samples/teeracle/kubernetes/templates/teeracle.yaml +++ /dev/null @@ -1,73 +0,0 @@ -kind: ServiceAccount -apiVersion: v1 -metadata: - name: teeracle - namespace: {{ .Release.Namespace }} ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: teeracle-main - namespace: {{ .Release.Namespace }} - labels: - app: teeracle - role: main - tier: backend -spec: - replicas: 1 - selector: - matchLabels: - app: teeracle - role: main - tier: backend - template: - metadata: - labels: - app: teeracle - spec: - serviceAccountName: teeracle - containers: - - image: {{ .Values.image }} - imagePullPolicy: {{ .Values.imagePullPolicy }} - - args: [ - "-p", "443", - "-u", {{ .Values.app.url }}, - "--enable-metrics", - "--data-dir", "/opt/teeracle", - "run", - "--teeracle-interval", {{ .Values.app.interval }} - ] - name: teeracle - - resources: - # Resource request to use Intel SGX Device Plugin - # If you are running in simulation mode, or are using a different plugin, - # update these values accordingly - limits: - sgx.intel.com/epc: "10Mi" - sgx.intel.com/enclave: 1 - sgx.intel.com/provision: 1 - - volumeMounts: - - name: aesmd-socket - mountPath: /var/run/aesmd - - name: data-dir - mountPath: /opt/teeracle - - name: qcnl - mountPath: /etc/sgx_default_qcnl.conf - {{- with .Values.imagePullSecrets }} - imagePullSecrets: - {{- toYaml . | nindent 8 }} - {{- end }} - volumes: - - name: aesmd-socket - hostPath: - path: /var/run/aesmd - - name: data-dir - hostPath: - path: /opt/teeracle - - name: qcnl - hostPath: - path: /etc/sgx_default_qcnl.conf - diff --git a/bitacross-worker/samples/teeracle/kubernetes/values.yaml b/bitacross-worker/samples/teeracle/kubernetes/values.yaml deleted file mode 100644 index 8a423ee3ab..0000000000 --- a/bitacross-worker/samples/teeracle/kubernetes/values.yaml +++ /dev/null @@ -1,14 +0,0 @@ -imagePullSecrets: - - name: regcred - -imagePullPolicy: IfNotPresent - -image: integritee/teeracle:v0.12.2-dev - -# -# To get more insights run: -# docker run integritee/teeracle:v0.12.2-dev --help -# -app: - url: "wss://rococo.api.integritee.network" - interval: "2m" \ No newline at end of file diff --git a/bitacross-worker/scripts/changelog/templates/changes.md.tera b/bitacross-worker/scripts/changelog/templates/changes.md.tera index 571f2f4cab..1dcb6ea978 100644 --- a/bitacross-worker/scripts/changelog/templates/changes.md.tera +++ b/bitacross-worker/scripts/changelog/templates/changes.md.tera @@ -19,6 +19,5 @@ {% include "changes_sidechain.md.tera" %} -{% include "changes_teeracle.md.tera" %} {% include "changes_misc.md.tera" %} diff --git a/bitacross-worker/scripts/changelog/templates/changes_teeracle.md.tera b/bitacross-worker/scripts/changelog/templates/changes_teeracle.md.tera deleted file mode 100644 index 6e94e88b2c..0000000000 --- a/bitacross-worker/scripts/changelog/templates/changes_teeracle.md.tera +++ /dev/null @@ -1,17 +0,0 @@ -{% import "change.md.tera" as m_c -%} -### Teeracle - -{#- The changes are sorted by merge date #} -{%- for pr in changes | sort(attribute="merged_at") %} - -{%- if pr.meta.B %} - {%- if pr.meta.B.value == 0 %} - {#- We skip silent ones -#} - {%- else -%} - - {%- if pr.meta.A.value == 5 %} -- {{ m_c::change(c=pr) }} - {%- endif -%} - {% endif -%} - {% endif -%} -{% endfor %} diff --git a/bitacross-worker/scripts/teeracle.sh b/bitacross-worker/scripts/teeracle.sh deleted file mode 100644 index 829c67b2a3..0000000000 --- a/bitacross-worker/scripts/teeracle.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/bin/bash -set -euo pipefail - -# Runs Teeracle1 demo: Either set `CLIENT_DIR` env var directly or run script with: -# -# source ./init_env.sh && ./teeracle.sh - -echo "$CLIENT_DIR" - -cd "$CLIENT_DIR" || exit - -LOG_1="${LOG_1:-$LOG_DIR/teeracle1_demo_whitelist.log}" - -echo "[teeracle.sh] printing to logs:" -echo " $LOG_1" - -touch "$LOG_1" - -./demo_teeracle_whitelist.sh -p 9944 -P 2000 -d 120 -i 24 2>&1 | tee "$LOG_1" diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index 49e1f4e61d..e56b810cb3 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -89,7 +89,6 @@ production = [ "litentry-macros/production", "litentry-primitives/production", ] -teeracle = ["itp-settings/teeracle"] dcap = [] attesteer = ["dcap"] # Must be enabled to build a binary and link it with the enclave successfully. diff --git a/bitacross-worker/service/src/cli.yml b/bitacross-worker/service/src/cli.yml index e517e6b1bb..959798e17e 100644 --- a/bitacross-worker/service/src/cli.yml +++ b/bitacross-worker/service/src/cli.yml @@ -145,17 +145,6 @@ subcommands: long: request-state short: r help: Run the worker and request key and state provisioning from another worker. - - teeracle-interval: - required: false - long: teeracle-interval - short: i - help: Set the teeracle exchange rate update interval. Example of accepted syntax <5 seconds 15 minutes 2 hours 1 days> or short <5s15m2h1d> - takes_value: true - - reregister-teeracle-interval: - required: false - long: reregister - help: Set the teeracle reregistration interval. Example of accepted syntax <5 seconds 15 minutes 2 hours 1 days> or short <5s15m2h1d> - takes_value: true - request-state: about: join a shard by requesting key provisioning from another worker args: diff --git a/bitacross-worker/service/src/config.rs b/bitacross-worker/service/src/config.rs index bc9b8b7cdb..fd33d09957 100644 --- a/bitacross-worker/service/src/config.rs +++ b/bitacross-worker/service/src/config.rs @@ -17,7 +17,6 @@ use clap::ArgMatches; use itc_rest_client::rest_client::Url; -use itp_settings::teeracle::{DEFAULT_MARKET_DATA_UPDATE_INTERVAL, ONE_DAY, THIRTY_MINUTES}; use parse_duration::parse; use serde::{Deserialize, Serialize}; use std::{ @@ -295,10 +294,6 @@ pub struct RunConfig { request_state: bool, /// Shard identifier base58 encoded. Defines the shard that this worker operates on. Default is mrenclave. shard: Option, - /// Optional teeracle update interval - teeracle_update_interval: Option, - /// Optional teeracle reregistration interval - reregister_teeracle_interval: Option, /// Marblerun's Prometheus endpoint base URL marblerun_base_url: Option, } @@ -320,19 +315,6 @@ impl RunConfig { self.shard.as_deref() } - pub fn teeracle_update_interval(&self) -> Duration { - self.teeracle_update_interval.unwrap_or(DEFAULT_MARKET_DATA_UPDATE_INTERVAL) - } - - /// The periodic registration period of the teeracle. - /// - /// Defaults to 23h30m, as this is slightly below the currently configured automatic - /// deregistration period on the Integritee chains. - pub fn reregister_teeracle_interval(&self) -> Duration { - // Todo: Derive this from chain https://github.com/integritee-network/worker/issues/1351 - self.reregister_teeracle_interval.unwrap_or(ONE_DAY - THIRTY_MINUTES) - } - pub fn marblerun_base_url(&self) -> &str { // This conflicts with the default port of a substrate node, but it is indeed the // default port of marblerun too: @@ -347,12 +329,6 @@ impl From<&ArgMatches<'_>> for RunConfig { let dev = m.is_present("dev"); let request_state = m.is_present("request-state"); let shard = m.value_of("shard").map(|s| s.to_string()); - let teeracle_update_interval = m.value_of("teeracle-interval").map(|i| { - parse(i).unwrap_or_else(|e| panic!("teeracle-interval parsing error {:?}", e)) - }); - let reregister_teeracle_interval = m.value_of("reregister-teeracle-interval").map(|i| { - parse(i).unwrap_or_else(|e| panic!("teeracle-interval parsing error {:?}", e)) - }); let marblerun_base_url = m.value_of("marblerun-url").map(|i| { Url::parse(i) @@ -360,15 +336,7 @@ impl From<&ArgMatches<'_>> for RunConfig { .to_string() }); - Self { - skip_ra, - dev, - request_state, - shard, - teeracle_update_interval, - reregister_teeracle_interval, - marblerun_base_url, - } + Self { skip_ra, dev, request_state, shard, marblerun_base_url } } } @@ -504,7 +472,6 @@ mod test { assert_eq!(run_config.dev, false); assert_eq!(run_config.skip_ra, false); assert!(run_config.shard.is_none()); - assert!(run_config.teeracle_update_interval.is_none()); } #[test] @@ -517,11 +484,9 @@ mod test { ("dev", Default::default()), ("skip-ra", Default::default()), ("shard", Default::default()), - ("teeracle-interval", Default::default()), ]); // Workaround because MatchedArg is private. args.args.get_mut("shard").unwrap().vals = vec![shard_identifier.into()]; - args.args.get_mut("teeracle-interval").unwrap().vals = vec!["42s".into()]; let run_config = RunConfig::from(&args); @@ -529,7 +494,6 @@ mod test { assert_eq!(run_config.dev, true); assert_eq!(run_config.skip_ra, true); assert_eq!(run_config.shard.unwrap(), shard_identifier.to_string()); - assert_eq!(run_config.teeracle_update_interval.unwrap(), Duration::from_secs(42)); } #[test] @@ -563,17 +527,6 @@ mod test { assert_eq!(config.mu_ra_url_external(), format!("{}:{}", expected_worker_ip, mu_ra_port)); } - #[test] - fn teeracle_interval_parsing_panics_if_format_is_invalid() { - let teeracle_interval = "24s_invalid-format"; - let mut args = ArgMatches::default(); - args.args = HashMap::from([("teeracle-interval", Default::default())]); - args.args.get_mut("teeracle-interval").unwrap().vals = vec![teeracle_interval.into()]; - - let result = std::panic::catch_unwind(|| RunConfig::from(&args)); - assert!(result.is_err()); - } - #[test] fn external_addresses_are_returned_correctly_if_set() { let trusted_ext_addr = "wss://1.1.1.2:700"; diff --git a/bitacross-worker/service/src/main.rs b/bitacross-worker/service/src/main.rs index 6c5a888eee..feab3398ea 100644 --- a/bitacross-worker/service/src/main.rs +++ b/bitacross-worker/service/src/main.rs @@ -31,8 +31,6 @@ mod setup; mod sidechain_setup; mod sync_block_broadcaster; mod sync_state; -#[cfg(feature = "teeracle")] -mod teeracle; mod tests; mod utils; mod worker; diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 77b1c6a984..764eb8aabf 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "teeracle")] -use crate::teeracle::{schedule_periodic_reregistration_thread, start_periodic_market_update}; - #[cfg(not(feature = "dcap"))] use crate::utils::check_files; use crate::{ @@ -36,7 +33,6 @@ use itp_enclave_api::{ enclave_base::EnclaveBase, remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, sidechain::Sidechain, - teeracle_api::TeeracleApi, }; use itp_node_api::{ api_client::{AccountApi, PalletTeerexApi, ParentchainApi}, @@ -319,13 +315,7 @@ fn start_worker( quote_size: Option, ) where T: GetTokioHandle, - E: EnclaveBase - + DirectRequest - + Sidechain - + RemoteAttestation - + TlsRemoteAttestation - + TeeracleApi - + Clone, + E: EnclaveBase + DirectRequest + Sidechain + RemoteAttestation + TlsRemoteAttestation + Clone, D: BlockPruner + FetchBlocks + Sync + Send + 'static, InitializationHandler: TrackInitialization + IsInitialized + Sync + Send + 'static, WorkerModeProvider: ProvideWorkerMode, @@ -333,13 +323,11 @@ fn start_worker( let run_config = config.run_config().clone().expect("Run config missing"); let skip_ra = run_config.skip_ra(); - #[cfg(feature = "teeracle")] - let flavor_str = "teeracle"; #[cfg(feature = "sidechain")] let flavor_str = "sidechain"; #[cfg(feature = "offchain-worker")] let flavor_str = "offchain-worker"; - #[cfg(not(any(feature = "offchain-worker", feature = "sidechain", feature = "teeracle")))] + #[cfg(not(any(feature = "offchain-worker", feature = "sidechain")))] let flavor_str = "offchain-worker"; println!("Litentry Worker for {} v{}", flavor_str, VERSION); @@ -589,23 +577,6 @@ fn start_worker( initialization_handler.registered_on_parentchain(); match WorkerModeProvider::worker_mode() { - WorkerMode::Teeracle => { - // ------------------------------------------------------------------------ - // initialize teeracle interval - #[cfg(feature = "teeracle")] - schedule_periodic_reregistration_thread( - send_register_xt, - run_config.reregister_teeracle_interval(), - ); - - #[cfg(feature = "teeracle")] - start_periodic_market_update( - &litentry_rpc_api, - run_config.teeracle_update_interval(), - enclave.as_ref(), - &tokio_handle, - ); - }, WorkerMode::OffChainWorker => { println!("*** [+] Finished initializing light client, syncing parentchain..."); @@ -735,18 +706,17 @@ fn init_target_parentchain( let (parentchain_handler, last_synched_header) = init_parentchain(enclave, &node_api, tee_account_id, parentchain_id); - if WorkerModeProvider::worker_mode() != WorkerMode::Teeracle { - println!( - "*** [+] [{:?}] Finished initializing light client, syncing parentchain...", - parentchain_id - ); + println!( + "*** [+] [{:?}] Finished initializing light client, syncing parentchain...", + parentchain_id + ); - // Syncing all parentchain blocks, this might take a while.. - let last_synched_header = - parentchain_handler.sync_parentchain(last_synched_header, 0, true).unwrap(); + // Syncing all parentchain blocks, this might take a while.. + let last_synched_header = + parentchain_handler.sync_parentchain(last_synched_header, 0, true).unwrap(); + + start_parentchain_header_subscription_thread(parentchain_handler, last_synched_header); - start_parentchain_header_subscription_thread(parentchain_handler, last_synched_header) - } println!("[{:?}] initializing proxied shard vault account now", parentchain_id); enclave.init_proxied_shard_vault(shard, &parentchain_id).unwrap(); diff --git a/bitacross-worker/service/src/prometheus_metrics.rs b/bitacross-worker/service/src/prometheus_metrics.rs index 64a3615135..55f0edbfc8 100644 --- a/bitacross-worker/service/src/prometheus_metrics.rs +++ b/bitacross-worker/service/src/prometheus_metrics.rs @@ -17,9 +17,6 @@ //! Service for prometheus metrics, hosted on a http server. -#[cfg(feature = "teeracle")] -use crate::teeracle::teeracle_metrics::update_teeracle_metrics; - use crate::{ account_funding::EnclaveAccountInfo, error::{Error, ServiceResult}, @@ -225,12 +222,6 @@ impl ReceiveEnclaveMetrics for EnclaveMetricsReceiver { ENCLAVE_SIDECHAIN_SLOT_BLOCK_COMPOSITION_TIME.observe(time.as_secs_f64()), EnclaveMetric::SidechainBlockBroadcastingTime(time) => ENCLAVE_SIDECHAIN_BLOCK_BROADCASTING_TIME.observe(time.as_secs_f64()), - #[cfg(feature = "teeracle")] - EnclaveMetric::ExchangeRateOracle(m) => update_teeracle_metrics(m)?, - #[cfg(not(feature = "teeracle"))] - EnclaveMetric::ExchangeRateOracle(_) => { - error!("Received Teeracle metric, but Teeracle feature is not enabled, ignoring metric item.") - }, } Ok(()) } diff --git a/bitacross-worker/service/src/teeracle/schedule_periodic.rs b/bitacross-worker/service/src/teeracle/schedule_periodic.rs deleted file mode 100644 index cde09af452..0000000000 --- a/bitacross-worker/service/src/teeracle/schedule_periodic.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use std::{ - thread, - time::{Duration, Instant}, -}; - -/// Schedules a periodic task in the current thread. -/// -/// In case the task takes longer than is scheduled by the interval duration, -/// the interval timing will drift. The task is responsible for -/// ensuring it does not use up more time than is scheduled. -pub(super) fn schedule_periodic(task: T, period: Duration) -where - T: Fn(), -{ - let mut interval_start = Instant::now(); - loop { - let elapsed = interval_start.elapsed(); - - if elapsed >= period { - // update interval time - interval_start = Instant::now(); - task(); - } else { - // sleep for the rest of the interval - let sleep_time = period - elapsed; - thread::sleep(sleep_time); - } - } -} diff --git a/bitacross-worker/service/src/teeracle/teeracle_metrics.rs b/bitacross-worker/service/src/teeracle/teeracle_metrics.rs deleted file mode 100644 index 8fe62c2092..0000000000 --- a/bitacross-worker/service/src/teeracle/teeracle_metrics.rs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, ServiceResult}; -use itp_enclave_metrics::ExchangeRateOracleMetric; -use lazy_static::lazy_static; -use prometheus::{ - register_gauge_vec, register_int_counter, register_int_counter_vec, register_int_gauge, - register_int_gauge_vec, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, -}; - -lazy_static! { - /// Register Teeracle specific metrics - - static ref EXCHANGE_RATE: GaugeVec = - register_gauge_vec!("integritee_teeracle_exchange_rate", "Exchange rates partitioned into source and trading pair", &["source", "trading_pair"]) - .unwrap(); - static ref RESPONSE_TIME: IntGaugeVec = - register_int_gauge_vec!("integritee_teeracle_response_times", "Response times in ms for requests that the oracle makes", &["source"]) - .unwrap(); - static ref NUMBER_OF_REQUESTS: IntCounterVec = - register_int_counter_vec!("integritee_teeracle_number_of_requests", "Number of requests made per source", &["source"]) - .unwrap(); - - static ref NUMBER_OF_REQUEST_FAILURES: IntCounter = - register_int_counter!("integritee_teeracle_request_failures", "Number of requests that failed") - .unwrap(); - - static ref EXTRINSIC_INCLUSION_SUCCESS: IntGauge = - register_int_gauge!("integritee_teeracle_extrinsic_inclusion_success", "1 if extrinsics was successfully finalized, 0 if not") - .unwrap(); -} - -pub(super) fn increment_number_of_request_failures() { - NUMBER_OF_REQUEST_FAILURES.inc(); -} - -pub(super) fn set_extrinsics_inclusion_success(is_successful: bool) { - let success_values = i64::from(is_successful); - EXTRINSIC_INCLUSION_SUCCESS.set(success_values); -} - -pub fn update_teeracle_metrics(metric: ExchangeRateOracleMetric) -> ServiceResult<()> { - match metric { - ExchangeRateOracleMetric::ExchangeRate(source, trading_pair, exchange_rate) => - EXCHANGE_RATE - .get_metric_with_label_values(&[source.as_str(), trading_pair.as_str()]) - .map(|m| m.set(exchange_rate.to_num())) - .map_err(|e| Error::Custom(e.into()))?, - - ExchangeRateOracleMetric::ResponseTime(source, t) => RESPONSE_TIME - .get_metric_with_label_values(&[source.as_str()]) - .map(|m| m.set(t as i64)) - .map_err(|e| Error::Custom(e.into()))?, - - ExchangeRateOracleMetric::NumberRequestsIncrement(source) => NUMBER_OF_REQUESTS - .get_metric_with_label_values(&[source.as_str()]) - .map(|m| m.inc()) - .map_err(|e| Error::Custom(e.into()))?, - }; - Ok(()) -} diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index beec0ab4a9..b3f96a690c 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -12,6 +12,13 @@ function worker_clippy() { cargo clippy --release --features offchain-worker -- -D warnings } +function bitacross_clippy() { + cargo clippy --release -- -D warnings + cargo clippy --release --features evm -- -D warnings + cargo clippy --release --features sidechain -- -D warnings + cargo clippy --release --features offchain-worker -- -D warnings +} + function parachain_check() { make clippy cargo test --locked --release -p pallet-* --lib @@ -57,15 +64,15 @@ RUST_LOG=info SKIP_WASM_BUILD=1 cargo test --release -- --show-output echo "[Step 5], tee-worker service test" clean_up cd "$root_dir/tee-worker" -SGX_MODE=SW SKIP_WASM_BUILD=1 make +#SGX_MODE=SW SKIP_WASM_BUILD=1 make cd "$root_dir/tee-worker/bin" ./litentry-worker test --all echo "[Step 6], bitacross-worker clippy" -cd "$root_dir/bitacross-worker" && worker_clippy +cd "$root_dir/bitacross-worker" && bitacross_clippy echo "[Step 7], bitacross-worker enclave clippy" -cd "$root_dir/bitacross-worker/enclave-runtime" && worker_clippy +cd "$root_dir/bitacross-worker/enclave-runtime" && bitacross_clippy echo "[Step 8], bitacross-worker cargo test" cd "$root_dir/bitacross-worker" From 581321fde998f725b44c877e29fa10b4380a5b68 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Wed, 31 Jan 2024 17:21:57 +0100 Subject: [PATCH 04/64] remove workers .github folders (#2457) --- .../.github/workflows/build_and_test.yml | 562 ---------------- .../.github/workflows/check_labels.yml | 24 - .../.github/workflows/delete-release.yml | 70 -- .../.github/workflows/label-checker.yml | 21 - .../workflows/publish-docker-release.yml | 69 -- .../workflows/publish-docker-sidechain.yml | 43 -- .../workflows/publish-draft-release.yml | 69 -- .../.github/workflows/build_and_test.yml | 599 ------------------ tee-worker/.github/workflows/check_labels.yml | 24 - .../.github/workflows/delete-release.yml | 70 -- .../.github/workflows/label-checker.yml | 21 - .../workflows/publish-docker-release.yml | 69 -- .../workflows/publish-docker-sidechain.yml | 43 -- .../workflows/publish-docker-teeracle.yml | 43 -- .../workflows/publish-draft-release.yml | 69 -- 15 files changed, 1796 deletions(-) delete mode 100644 bitacross-worker/.github/workflows/build_and_test.yml delete mode 100644 bitacross-worker/.github/workflows/check_labels.yml delete mode 100644 bitacross-worker/.github/workflows/delete-release.yml delete mode 100644 bitacross-worker/.github/workflows/label-checker.yml delete mode 100644 bitacross-worker/.github/workflows/publish-docker-release.yml delete mode 100644 bitacross-worker/.github/workflows/publish-docker-sidechain.yml delete mode 100644 bitacross-worker/.github/workflows/publish-draft-release.yml delete mode 100644 tee-worker/.github/workflows/build_and_test.yml delete mode 100644 tee-worker/.github/workflows/check_labels.yml delete mode 100644 tee-worker/.github/workflows/delete-release.yml delete mode 100644 tee-worker/.github/workflows/label-checker.yml delete mode 100644 tee-worker/.github/workflows/publish-docker-release.yml delete mode 100644 tee-worker/.github/workflows/publish-docker-sidechain.yml delete mode 100644 tee-worker/.github/workflows/publish-docker-teeracle.yml delete mode 100644 tee-worker/.github/workflows/publish-draft-release.yml diff --git a/bitacross-worker/.github/workflows/build_and_test.yml b/bitacross-worker/.github/workflows/build_and_test.yml deleted file mode 100644 index b9f6467b86..0000000000 --- a/bitacross-worker/.github/workflows/build_and_test.yml +++ /dev/null @@ -1,562 +0,0 @@ -name: Build, Test, Clippy - -on: - workflow_dispatch: - push: - branches: - - master - - 'sdk-v[0-9]+.[0-9]+.[0-9]+-*' - tags: - - 'v[0-9]+.[0-9]+.[0-9]+*' - pull_request: - branches: - - master - - 'sdk-v[0-9]+.[0-9]+.[0-9]+-*' - -env: - CARGO_TERM_COLOR: always - LOG_DIR: logs - BUILD_CONTAINER_NAME: integritee_worker_enclave_test - -jobs: - cancel_previous_runs: - name: Cancel Previous Runs - runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} - - build-test: - runs-on: ${{ matrix.host }} - strategy: - fail-fast: false - matrix: - include: - - flavor_id: sidechain - mode: sidechain - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: offchain-worker - mode: offchain-worker - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: sidechain-evm - mode: sidechain - additional_features: evm,dcap - host: integritee-builder-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Build Worker - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee-worker-${{ env.IMAGE_SUFFIX }} - --target deployed-worker - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg FINGERPRINT=${FINGERPRINT} --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} --build-arg SGX_MODE=${{ matrix.sgx_mode }} - -f build.Dockerfile . - - - run: docker images --all - - - name: Test Enclave # cargo test is not supported in the enclave, see: https://github.com/apache/incubator-teaclave-sgx-sdk/issues/232 - run: docker run --rm ${{ env.DOCKER_DEVICES }} ${{ env.DOCKER_VOLUMES }} integritee-worker-${{ env.IMAGE_SUFFIX }} test --all - - - name: Export worker image - run: | - docker image save integritee-worker-${{ env.IMAGE_SUFFIX }} | gzip > integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Upload worker image - uses: actions/upload-artifact@v3 - with: - name: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - path: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Create Enclave Digest File - run: | - mrenclave_hex=$(docker run integritee-worker-${{ env.IMAGE_SUFFIX }} mrenclave | grep -oP ':\s*\K[a-fA-F0-9]+') - echo "$mrenclave_hex" > mrenclave-${{ env.IMAGE_SUFFIX }}.hex - - - name: Upload Enclave Digest File - uses: actions/upload-artifact@v3 - with: - name: mrenclave-${{ env.IMAGE_SUFFIX }}.hex - path: mrenclave-${{ env.IMAGE_SUFFIX }}.hex - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-worker-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-worker-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - docker images --all - - build-client: - runs-on: ${{ matrix.host }} - strategy: - fail-fast: false - matrix: - include: - - flavor_id: sidechain - mode: sidechain - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: offchain-worker - mode: offchain-worker - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: sidechain-evm - mode: sidechain - additional_features: evm,dcap - host: integritee-builder-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Build CLI client - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee-cli-client-${{ env.IMAGE_SUFFIX }} - --target deployed-client - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} - -f build.Dockerfile . - - - run: docker images --all - - - name: Export client image - run: | - docker image save integritee-cli-client-${{ env.IMAGE_SUFFIX }} | gzip > integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Upload CLI client image - uses: actions/upload-artifact@v3 - with: - name: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - path: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - docker images --all - - code-quality: - runs-on: ubuntu-latest - container: "integritee/integritee-dev:0.2.2" - strategy: - fail-fast: false - matrix: - check: [ - # Workspace - cargo test --release, - # Worker - # Use release mode as the CI runs out of disk space otherwise. - cargo clippy --release -- -D warnings, - cargo clippy --release --features evm -- -D warnings, - cargo clippy --release --features sidechain -- -D warnings, - cargo clippy --release --features offchain-worker -- -D warnings, - - # Enclave - cd enclave-runtime && cargo clippy -- -D warnings, - cd enclave-runtime && cargo clippy --features evm -- -D warnings, - cd enclave-runtime && cargo clippy --features sidechain -- -D warnings, - cd enclave-runtime && cargo clippy --features offchain-worker -- -D warnings, - - # Fmt - cargo fmt --all -- --check, - cd enclave-runtime && cargo fmt --all -- --check, - ] - steps: - - uses: actions/checkout@v3 - - name: init-rust-target - # Enclave is not in the same workspace - run: rustup show && cd enclave-runtime && rustup show - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ matrix.check }} - - - name: ${{ matrix.check }} - run: ${{ matrix.check }} - - toml-fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: init rust - run: rustup show - - - name: Install taplo - run: cargo install taplo-cli --locked - - name: Cargo.toml fmt - run: taplo fmt --check - - - name: Fail-fast; cancel other jobs - if: failure() - uses: andymckay/cancel-action@0.3 - - integration-tests: - runs-on: ${{ matrix.host }} - if: ${{ always() }} - needs: [build-test, build-client] - env: - WORKER_IMAGE_TAG: integritee-worker:dev - CLIENT_IMAGE_TAG: integritee-cli:dev - - strategy: - fail-fast: false - matrix: - include: - - test: M6 - flavor_id: sidechain - demo_name: demo-shielding-unshielding-multiworker - host: test-runner-sgx - sgx_mode: HW - - test: M8 - flavor_id: sidechain - demo_name: demo-direct-call - host: test-runner-sgx - sgx_mode: HW - - test: Sidechain - flavor_id: sidechain - demo_name: demo-sidechain - host: test-runner-sgx - sgx_mode: HW - - test: M6 - flavor_id: offchain-worker - demo_name: demo-shielding-unshielding-multiworker - host: test-runner-sgx - sgx_mode: HW - - test: Benchmark - flavor_id: sidechain - demo_name: sidechain-benchmark - host: test-runner-sgx - sgx_mode: HW - - test: EVM - flavor_id: sidechain-evm - demo_name: demo-smart-contract - host: test-runner-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - version=$RANDOM - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - echo "FLAVOR_ID=${{ matrix.flavor_id }}" >> $GITHUB_ENV - echo "PROJECT=${{ matrix.flavor_id }}-${{ matrix.demo_name }}" >> $GITHUB_ENV - echo "VERSION=dev.$version" >> $GITHUB_ENV - echo "WORKER_IMAGE_TAG=integritee-worker:dev.$version" >> $GITHUB_ENV - echo "INTEGRITEE_NODE=integritee-node:1.1.3.$version" >> $GITHUB_ENV - echo "CLIENT_IMAGE_TAG=integritee-cli:dev.$version" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "SGX_PROVISION=/dev/sgx/provision" >> $GITHUB_ENV - echo "SGX_ENCLAVE=/dev/sgx/enclave" >> $GITHUB_ENV - echo "AESMD=/var/run/aesmd" >> $GITHUB_ENV - echo "SGX_QCNL=/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - fi - - echo "LOG_DIR=./logs-$version" >> $GITHUB_ENV - - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - path: . - - - name: Download CLI client Image - uses: actions/download-artifact@v3 - with: - name: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - path: . - - - name: Load Worker & Client Images - env: - DOCKER_BUILDKIT: 1 - run: | - docker image load --input integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - docker image load --input integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - docker images --all - - ## - # Before tagging, delete the old "stuck" ones to be sure that the newly created ones are the latest - # Without if the docker image rmi throws an error if the image doesn't exist. - ## - - name: Re-name Image Tags - run: | - if [[ "$(docker images -q ${{ env.WORKER_IMAGE_TAG }} 2> /dev/null)" == "" ]]; then - docker image rmi --force ${{ env.WORKER_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.CLIENT_IMAGE_TAG }} 2> /dev/null)" == "" ]]; then - docker image rmi --force ${{ env.CLIENT_IMAGE_TAG }} 2>/dev/null - fi - docker tag integritee-worker-${{ env.IMAGE_SUFFIX }} ${{ env.WORKER_IMAGE_TAG }} - docker tag integritee-cli-client-${{ env.IMAGE_SUFFIX }} ${{ env.CLIENT_IMAGE_TAG }} - docker pull integritee/integritee-node:1.1.3 - docker tag integritee/integritee-node:1.1.3 ${{ env.INTEGRITEE_NODE }} - docker images --all - - ## - # Stop any stucked/running compose projects - ## - - name: Stop docker containers - if: always() - continue-on-error: true - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} stop - - - name: Integration Test ${{ matrix.test }}-${{ matrix.flavor_id }} - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} up ${{ matrix.demo_name }} --no-build --exit-code-from ${{ matrix.demo_name }} --remove-orphans - - - - name: Collect Docker Logs - continue-on-error: true - if: always() - uses: jwalton/gh-docker-logs@v2 - with: - images: '${{ env.WORKER_IMAGE_TAG }},${{ env.CLIENT_IMAGE_TAG }},${{ env.INTEGRITEE_NODE }}' - tail: all - dest: ${{ env.LOG_DIR }} - - - name: Upload logs - if: always() - uses: actions/upload-artifact@v3 - with: - name: logs-${{ matrix.test }}-${{ matrix.flavor_id }} - path: ${{ env.LOG_DIR }} - - - name: Stop docker containers - if: always() - continue-on-error: true - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} stop - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-worker-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-worker-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - if [[ "$(docker images -q integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.WORKER_IMAGE_TAG }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.WORKER_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.CLIENT_IMAGE_TAG }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.CLIENT_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.INTEGRITEE_NODE }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.INTEGRITEE_NODE }} 2>/dev/null - fi - docker images --all - - release-build: - runs-on: integritee-builder-sgx - name: Release Build of teeracle - if: startsWith(github.ref, 'refs/tags/') - needs: [ build-test, integration-tests ] - - strategy: - fail-fast: false - matrix: - include: - - flavor_id: sidechain - mode: sidechain - sgx_mode: HW - additional_features: dcap - - steps: - - uses: actions/checkout@v3 - - - name: Add masks - run: | - echo "::add-mask::$VAULT_TOKEN" - echo "::add-mask::$PRIVKEY_B64" - echo "::add-mask::$PRIVKEY_PASS" - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - echo "VAULT_TOKEN=$VAULT_TOKEN" >> "$GITHUB_ENV" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Import secrets - uses: hashicorp/vault-action@v2 - id: import-secrets - with: - url: ${{ secrets.VAULT_URL }} - tlsSkipVerify: false - token: ${{ env.VAULT_TOKEN }} - exportEnv: false - secrets: | - ${{ secrets.VAULT_PATH }} intel_sgx_pem_base64 | PRIVKEY_B64 ; - ${{ secrets.VAULT_PATH }} password | PRIVKEY_PASS - - - name: Get secrets - env: - PRIVKEY_B64: ${{ steps.import-secrets.outputs.PRIVKEY_B64 }} - PRIVKEY_PASS: ${{ steps.import-secrets.outputs.PRIVKEY_PASS }} - run: | - echo $PRIVKEY_B64 | base64 --ignore-garbage --decode > enclave-runtime/intel_sgx.pem - echo $PRIVKEY_PASS > enclave-runtime/passfile.txt - - - name: Build Worker & Run Cargo Test - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} - --target deployed-worker - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg SGX_COMMERCIAL_KEY=enclave-runtime/intel_sgx.pem --build-arg SGX_PASSFILE=enclave-runtime/passfile.txt --build-arg SGX_PRODUCTION=1 --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} --build-arg SGX_MODE=${{ matrix.sgx_mode }} - -f build.Dockerfile . - - - name: Save released teeracle - run: | - docker image save integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} | gzip > integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - docker images --all - - - name: Upload teeracle image - uses: actions/upload-artifact@v3 - with: - name: integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - path: integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - - - name: Delete images - run: | - if [[ "$(docker images -q integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} 2>/dev/null - fi - docker images --all - - release: - runs-on: ubuntu-latest - name: Draft Release - if: startsWith(github.ref, 'refs/tags/') - needs: [ build-test, integration-tests, release-build ] - outputs: - release_url: ${{ steps.create-release.outputs.html_url }} - asset_upload_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - uses: actions/checkout@v3 - - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-sidechain-${{ github.ref_name }}.tar.gz - path: . - - # - # Temporary comment out until we decide what to release - # - # - name: Download Integritee Client - # uses: actions/download-artifact@v3 - # with: - # name: integritee-client-sidechain-${{ github.sha }} - # path: integritee-client-tmp - - # - name: Download Enclave Signed - # uses: actions/download-artifact@v3 - # with: - # name: enclave-signed-sidechain-${{ github.sha }} - # path: enclave-signed-tmp - - # - name: Move service binaries - # run: mv integritee-worker-tmp/integritee-service ./integritee-demo-validateer - - # - name: Move service client binaries - # run: mv integritee-client-tmp/integritee-cli ./integritee-client - - # - name: Move service client binaries - # run: mv enclave-signed-tmp/enclave.signed.so ./enclave.signed.so - - - name: Changelog - uses: scottbrenner/generate-changelog-action@master - id: Changelog - - - name: Display structure of downloaded files - run: ls -R - working-directory: . - - - name: Release - id: create-release - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body: | - ${{ steps.Changelog.outputs.changelog }} - draft: true - name: Docker ${{ github.ref_name }} - files: | - integritee-worker-sidechain-${{ github.ref_name }}.tar.gz - integritee-client - integritee-demo-validateer - enclave.signed.so diff --git a/bitacross-worker/.github/workflows/check_labels.yml b/bitacross-worker/.github/workflows/check_labels.yml deleted file mode 100644 index fdab3e9736..0000000000 --- a/bitacross-worker/.github/workflows/check_labels.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Labels Check -on: - pull_request: - types: [opened, labeled, unlabeled, synchronize, ready_for_review] -jobs: - A-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "A0-core,A1-cli,A2-applibs,A3-sidechain,A4-offchain,A6-evm,A7-somethingelse" - - B-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "B0-silent,B1-releasenotes" - - C-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "C1-low 📌,C3-medium 📣,C7-high ❗️,C9-critical ‼️" - - E-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "E0-breaksnothing,E3-hardmerge,E5-publicapi,E6-parentchain,E8-breakseverything" diff --git a/bitacross-worker/.github/workflows/delete-release.yml b/bitacross-worker/.github/workflows/delete-release.yml deleted file mode 100644 index 7029a5eb1f..0000000000 --- a/bitacross-worker/.github/workflows/delete-release.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Delete-Release - -on: - release: - types: [deleted] # should be deleted - -jobs: - purge-image: - name: Delete image from ghcr.io - runs-on: ubuntu-latest - strategy: - matrix: - #binary: ["integritee-client", "integritee-demo-validateer"] - binary: [] - steps: - - uses: actions/checkout@v2 - - - name: Set output - id: vars - run: echo "{tag}={$GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - - - name: Get Tag - id: get_tag - run: echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} - - - name: Check output - env: - RELEASE_VERSION: ${{ steps.get_tag.outputs.TAG }} - run: | - echo $RELEASE_VERSION - echo ${{ steps.vars.outputs.tag }} - echo ${{github.event.pull_request.number}} - - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - # Unfortunately accessing the repo with personal access token is not possible - # Workaround: disable 2FA and user password instead of TOKEN - - name: Delete docker tag - run: | - ORGANIZATION="integritee" - IMAGE="${{ matrix.binary }}" - TAG="${{ steps.get_tag.outputs.TAG }}" - - login_data() { - cat < /dev/null)" != "" ]]; then - docker image rmi --force integritee/sidechain:${{ github.event.release.tag_name }} 2>/dev/null - fi - docker images --all diff --git a/bitacross-worker/.github/workflows/publish-draft-release.yml b/bitacross-worker/.github/workflows/publish-draft-release.yml deleted file mode 100644 index 0e8c72dd6c..0000000000 --- a/bitacross-worker/.github/workflows/publish-draft-release.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Release - Publish draft - -on: - push: - tags: - # Catches only v1.2.3 (-dev,-rc1 etc won't be released as SDK) - - v[0-9]+.[0-9]+.[0-9]+ - -jobs: - publish-draft-release: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: worker - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.0.0 - - - name: Download srtool json output - uses: actions/download-artifact@v3 - - - name: Prepare tooling - run: | - cd worker/scripts/changelog - gem install bundler changelogerator:0.9.1 - bundle install - changelogerator --help - URL=https://github.com/chevdor/tera-cli/releases/download/v0.2.1/tera-cli_linux_amd64.deb - wget $URL -O tera.deb - sudo dpkg -i tera.deb - tera --version - - - name: Generate release notes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEBUG: 1 - PRE_RELEASE: ${{ github.event.inputs.pre_release }} - run: | - find ${{env.GITHUB_WORKSPACE}} -type f -name "*_srtool_output.json" - - cd worker/scripts/changelog - - ./bin/changelog ${GITHUB_REF} - ls -al release-notes.md - ls -al context.json - - - name: Archive artifact context.json - uses: actions/upload-artifact@v3 - with: - name: release-notes-context - path: | - worker/scripts/changelog/context.json - **/*_srtool_output.json - - - name: Create draft release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: SDK ${{ github.ref }} - body_path: ./worker/scripts/changelog/release-notes.md - draft: true diff --git a/tee-worker/.github/workflows/build_and_test.yml b/tee-worker/.github/workflows/build_and_test.yml deleted file mode 100644 index d007fe1eb4..0000000000 --- a/tee-worker/.github/workflows/build_and_test.yml +++ /dev/null @@ -1,599 +0,0 @@ -name: Build, Test, Clippy - -on: - workflow_dispatch: - push: - branches: - - master - - 'sdk-v[0-9]+.[0-9]+.[0-9]+-*' - tags: - - 'v[0-9]+.[0-9]+.[0-9]+*' - pull_request: - branches: - - master - - 'sdk-v[0-9]+.[0-9]+.[0-9]+-*' - -env: - CARGO_TERM_COLOR: always - LOG_DIR: logs - BUILD_CONTAINER_NAME: integritee_worker_enclave_test - -jobs: - cancel_previous_runs: - name: Cancel Previous Runs - runs-on: ubuntu-latest - steps: - - uses: styfle/cancel-workflow-action@0.11.0 - with: - access_token: ${{ secrets.GITHUB_TOKEN }} - - build-test: - runs-on: ${{ matrix.host }} - strategy: - fail-fast: false - matrix: - include: - - flavor_id: sidechain - mode: sidechain - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: offchain-worker - mode: offchain-worker - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: teeracle - mode: teeracle - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: sidechain-evm - mode: sidechain - additional_features: evm,dcap - host: integritee-builder-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Build Worker - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee-worker-${{ env.IMAGE_SUFFIX }} - --target deployed-worker - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg FINGERPRINT=${FINGERPRINT} --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} --build-arg SGX_MODE=${{ matrix.sgx_mode }} - -f build.Dockerfile . - - - run: docker images --all - - - name: Test Enclave # cargo test is not supported in the enclave, see: https://github.com/apache/incubator-teaclave-sgx-sdk/issues/232 - run: docker run --rm ${{ env.DOCKER_DEVICES }} ${{ env.DOCKER_VOLUMES }} integritee-worker-${{ env.IMAGE_SUFFIX }} test --all - - - name: Export worker image - run: | - docker image save integritee-worker-${{ env.IMAGE_SUFFIX }} | gzip > integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Upload worker image - uses: actions/upload-artifact@v3 - with: - name: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - path: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Create Enclave Digest File - run: | - mrenclave_hex=$(docker run integritee-worker-${{ env.IMAGE_SUFFIX }} mrenclave | grep -oP ':\s*\K[a-fA-F0-9]+') - echo "$mrenclave_hex" > mrenclave-${{ env.IMAGE_SUFFIX }}.hex - - - name: Upload Enclave Digest File - uses: actions/upload-artifact@v3 - with: - name: mrenclave-${{ env.IMAGE_SUFFIX }}.hex - path: mrenclave-${{ env.IMAGE_SUFFIX }}.hex - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-worker-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-worker-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - docker images --all - - build-client: - runs-on: ${{ matrix.host }} - strategy: - fail-fast: false - matrix: - include: - - flavor_id: sidechain - mode: sidechain - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: offchain-worker - mode: offchain-worker - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: teeracle - mode: teeracle - host: integritee-builder-sgx - sgx_mode: HW - additional_features: dcap - - flavor_id: sidechain-evm - mode: sidechain - additional_features: evm,dcap - host: integritee-builder-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Build CLI client - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee-cli-client-${{ env.IMAGE_SUFFIX }} - --target deployed-client - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} - -f build.Dockerfile . - - - run: docker images --all - - - name: Export client image - run: | - docker image save integritee-cli-client-${{ env.IMAGE_SUFFIX }} | gzip > integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Upload CLI client image - uses: actions/upload-artifact@v3 - with: - name: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - path: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - docker images --all - - code-quality: - runs-on: ubuntu-latest - container: "integritee/integritee-dev:0.2.2" - strategy: - fail-fast: false - matrix: - check: [ - # Workspace - cargo test --release, - # Worker - # Use release mode as the CI runs out of disk space otherwise. - cargo clippy --release -- -D warnings, - cargo clippy --release --features evm -- -D warnings, - cargo clippy --release --features sidechain -- -D warnings, - cargo clippy --release --features teeracle -- -D warnings, - cargo clippy --release --features offchain-worker -- -D warnings, - - # Enclave - cd enclave-runtime && cargo clippy -- -D warnings, - cd enclave-runtime && cargo clippy --features evm -- -D warnings, - cd enclave-runtime && cargo clippy --features sidechain -- -D warnings, - cd enclave-runtime && cargo clippy --features teeracle -- -D warnings, - cd enclave-runtime && cargo clippy --features offchain-worker -- -D warnings, - - # Fmt - cargo fmt --all -- --check, - cd enclave-runtime && cargo fmt --all -- --check, - ] - steps: - - uses: actions/checkout@v3 - - name: init-rust-target - # Enclave is not in the same workspace - run: rustup show && cd enclave-runtime && rustup show - - - uses: Swatinem/rust-cache@v2 - with: - key: ${{ matrix.check }} - - - name: ${{ matrix.check }} - run: ${{ matrix.check }} - - toml-fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - name: init rust - run: rustup show - - - name: Install taplo - run: cargo install taplo-cli --locked - - name: Cargo.toml fmt - run: taplo fmt --check - - - name: Fail-fast; cancel other jobs - if: failure() - uses: andymckay/cancel-action@0.3 - - integration-tests: - runs-on: ${{ matrix.host }} - if: ${{ always() }} - needs: [build-test, build-client] - env: - WORKER_IMAGE_TAG: integritee-worker:dev - CLIENT_IMAGE_TAG: integritee-cli:dev - COINMARKETCAP_KEY: ${{ secrets.COINMARKETCAP_KEY }} - # IAS_EPID_SPID: ${{ secrets.IAS_SPID }} - # IAS_EPID_KEY: ${{ secrets.IAS_PRIMARY_KEY }} - TEERACLE_INTERVAL_SECONDS: 10 - - strategy: - fail-fast: false - matrix: - include: - - test: M6 - flavor_id: sidechain - demo_name: demo-shielding-unshielding-multiworker - host: test-runner-sgx - sgx_mode: HW - - test: M8 - flavor_id: sidechain - demo_name: demo-direct-call - host: test-runner-sgx - sgx_mode: HW - - test: Sidechain - flavor_id: sidechain - demo_name: demo-sidechain - host: test-runner-sgx - sgx_mode: HW - - test: M6 - flavor_id: offchain-worker - demo_name: demo-shielding-unshielding-multiworker - host: test-runner-sgx - sgx_mode: HW - - test: Teeracle - flavor_id: teeracle - demo_name: demo-teeracle - host: test-runner-sgx - sgx_mode: HW - - test: Teeracle - flavor_id: teeracle - demo_name: demo-teeracle-generic - host: test-runner-sgx - sgx_mode: HW - - test: Benchmark - flavor_id: sidechain - demo_name: sidechain-benchmark - host: test-runner-sgx - sgx_mode: HW - - test: EVM - flavor_id: sidechain-evm - demo_name: demo-smart-contract - host: test-runner-sgx - sgx_mode: HW - - steps: - - uses: actions/checkout@v3 - - - name: Set env - run: | - version=$RANDOM - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - echo "FLAVOR_ID=${{ matrix.flavor_id }}" >> $GITHUB_ENV - echo "PROJECT=${{ matrix.flavor_id }}-${{ matrix.demo_name }}" >> $GITHUB_ENV - echo "VERSION=dev.$version" >> $GITHUB_ENV - echo "WORKER_IMAGE_TAG=integritee-worker:dev.$version" >> $GITHUB_ENV - echo "INTEGRITEE_NODE=integritee-node:1.1.3.$version" >> $GITHUB_ENV - echo "CLIENT_IMAGE_TAG=integritee-cli:dev.$version" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "SGX_PROVISION=/dev/sgx/provision" >> $GITHUB_ENV - echo "SGX_ENCLAVE=/dev/sgx/enclave" >> $GITHUB_ENV - echo "AESMD=/var/run/aesmd" >> $GITHUB_ENV - echo "SGX_QCNL=/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - fi - - echo "LOG_DIR=./logs-$version" >> $GITHUB_ENV - - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - path: . - - - name: Download CLI client Image - uses: actions/download-artifact@v3 - with: - name: integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - path: . - - - name: Load Worker & Client Images - env: - DOCKER_BUILDKIT: 1 - run: | - docker image load --input integritee-worker-${{ env.IMAGE_SUFFIX }}.tar.gz - docker image load --input integritee-cli-client-${{ env.IMAGE_SUFFIX }}.tar.gz - docker images --all - - ## - # Before tagging, delete the old "stuck" ones to be sure that the newly created ones are the latest - # Without if the docker image rmi throws an error if the image doesn't exist. - ## - - name: Re-name Image Tags - run: | - if [[ "$(docker images -q ${{ env.WORKER_IMAGE_TAG }} 2> /dev/null)" == "" ]]; then - docker image rmi --force ${{ env.WORKER_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.CLIENT_IMAGE_TAG }} 2> /dev/null)" == "" ]]; then - docker image rmi --force ${{ env.CLIENT_IMAGE_TAG }} 2>/dev/null - fi - docker tag integritee-worker-${{ env.IMAGE_SUFFIX }} ${{ env.WORKER_IMAGE_TAG }} - docker tag integritee-cli-client-${{ env.IMAGE_SUFFIX }} ${{ env.CLIENT_IMAGE_TAG }} - docker pull integritee/integritee-node:1.1.3 - docker tag integritee/integritee-node:1.1.3 ${{ env.INTEGRITEE_NODE }} - docker images --all - - ## - # Stop any stucked/running compose projects - ## - - name: Stop docker containers - if: always() - continue-on-error: true - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} stop - - - name: Integration Test ${{ matrix.test }}-${{ matrix.flavor_id }} - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} up ${{ matrix.demo_name }} --no-build --exit-code-from ${{ matrix.demo_name }} --remove-orphans - - - - name: Collect Docker Logs - continue-on-error: true - if: always() - uses: jwalton/gh-docker-logs@v2 - with: - images: '${{ env.WORKER_IMAGE_TAG }},${{ env.CLIENT_IMAGE_TAG }},${{ env.INTEGRITEE_NODE }}' - tail: all - dest: ${{ env.LOG_DIR }} - - - name: Upload logs - if: always() - uses: actions/upload-artifact@v3 - with: - name: logs-${{ matrix.test }}-${{ matrix.flavor_id }} - path: ${{ env.LOG_DIR }} - - - name: Stop docker containers - if: always() - continue-on-error: true - run: | - cd docker - docker compose -f <(envsubst < docker-compose.yml) -f <(envsubst < ${{ matrix.demo_name }}.yml) -p ${PROJECT} stop - - - name: Delete images - run: | - if [[ "$(docker images -q integritee-worker-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-worker-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - if [[ "$(docker images -q integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee-cli-client-${{ env.IMAGE_SUFFIX }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.WORKER_IMAGE_TAG }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.WORKER_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.CLIENT_IMAGE_TAG }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.CLIENT_IMAGE_TAG }} 2>/dev/null - fi - if [[ "$(docker images -q ${{ env.INTEGRITEE_NODE }} 2> /dev/null)" != "" ]]; then - docker image rmi --force ${{ env.INTEGRITEE_NODE }} 2>/dev/null - fi - docker images --all - - release-build: - runs-on: integritee-builder-sgx - name: Release Build of teeracle - if: startsWith(github.ref, 'refs/tags/') - needs: [ build-test, integration-tests ] - - strategy: - fail-fast: false - matrix: - include: - - flavor_id: teeracle - mode: teeracle - sgx_mode: HW - additional_features: dcap - - flavor_id: sidechain - mode: sidechain - sgx_mode: HW - additional_features: dcap - - steps: - - uses: actions/checkout@v3 - - - name: Add masks - run: | - echo "::add-mask::$VAULT_TOKEN" - echo "::add-mask::$PRIVKEY_B64" - echo "::add-mask::$PRIVKEY_PASS" - - - name: Set env - run: | - fingerprint=$RANDOM - echo "FINGERPRINT=$fingerprint" >> $GITHUB_ENV - SGX_MODE_LOWERCASE=$(echo "${${{ matrix.sgx_mode }},,}") - echo "IMAGE_SUFFIX=$SGX_MODE_LOWERCASE-${{ matrix.flavor_id }}-${{ github.sha }}" >> $GITHUB_ENV - if [[ ${{ matrix.sgx_mode }} == 'HW' ]]; then - echo "DOCKER_DEVICES=--device=/dev/sgx/enclave --device=/dev/sgx/provision" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=--volume /var/run/aesmd:/var/run/aesmd --volume /etc/sgx_default_qcnl.conf:/etc/sgx_default_qcnl.conf" >> $GITHUB_ENV - else - echo "DOCKER_DEVICES=" >> $GITHUB_ENV - echo "DOCKER_VOLUMES=" >> $GITHUB_ENV - fi - echo "VAULT_TOKEN=$VAULT_TOKEN" >> "$GITHUB_ENV" - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - with: - buildkitd-flags: --debug - driver: docker-container - - - name: Import secrets - uses: hashicorp/vault-action@v2 - id: import-secrets - with: - url: ${{ secrets.VAULT_URL }} - tlsSkipVerify: false - token: ${{ env.VAULT_TOKEN }} - exportEnv: false - secrets: | - ${{ secrets.VAULT_PATH }} intel_sgx_pem_base64 | PRIVKEY_B64 ; - ${{ secrets.VAULT_PATH }} password | PRIVKEY_PASS - - - name: Get secrets - env: - PRIVKEY_B64: ${{ steps.import-secrets.outputs.PRIVKEY_B64 }} - PRIVKEY_PASS: ${{ steps.import-secrets.outputs.PRIVKEY_PASS }} - run: | - echo $PRIVKEY_B64 | base64 --ignore-garbage --decode > enclave-runtime/intel_sgx.pem - echo $PRIVKEY_PASS > enclave-runtime/passfile.txt - - - name: Build Worker & Run Cargo Test - env: - DOCKER_BUILDKIT: 1 - run: > - docker build -t integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} - --target deployed-worker - --build-arg WORKER_MODE_ARG=${{ matrix.mode }} --build-arg SGX_COMMERCIAL_KEY=enclave-runtime/intel_sgx.pem --build-arg SGX_PASSFILE=enclave-runtime/passfile.txt --build-arg SGX_PRODUCTION=1 --build-arg ADDITIONAL_FEATURES_ARG=${{ matrix.additional_features }} --build-arg SGX_MODE=${{ matrix.sgx_mode }} - -f build.Dockerfile . - - - name: Save released teeracle - run: | - docker image save integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} | gzip > integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - docker images --all - - - name: Upload teeracle image - uses: actions/upload-artifact@v3 - with: - name: integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - path: integritee-worker-${{ matrix.flavor_id }}-${{ github.ref_name }}.tar.gz - - - name: Delete images - run: | - if [[ "$(docker images -q integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee/${{ matrix.flavor_id }}:${{ github.ref_name }} 2>/dev/null - fi - docker images --all - - release: - runs-on: ubuntu-latest - name: Draft Release - if: startsWith(github.ref, 'refs/tags/') - needs: [ build-test, integration-tests, release-build ] - outputs: - release_url: ${{ steps.create-release.outputs.html_url }} - asset_upload_url: ${{ steps.create-release.outputs.upload_url }} - steps: - - uses: actions/checkout@v3 - - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-teeracle-${{ github.ref_name }}.tar.gz - path: . - - - name: Download Worker Image - uses: actions/download-artifact@v3 - with: - name: integritee-worker-sidechain-${{ github.ref_name }}.tar.gz - path: . - - # - # Temporary comment out until we decide what to release - # - # - name: Download Integritee Client - # uses: actions/download-artifact@v3 - # with: - # name: integritee-client-sidechain-${{ github.sha }} - # path: integritee-client-tmp - - # - name: Download Enclave Signed - # uses: actions/download-artifact@v3 - # with: - # name: enclave-signed-sidechain-${{ github.sha }} - # path: enclave-signed-tmp - - # - name: Move service binaries - # run: mv integritee-worker-tmp/integritee-service ./integritee-demo-validateer - - # - name: Move service client binaries - # run: mv integritee-client-tmp/integritee-cli ./integritee-client - - # - name: Move service client binaries - # run: mv enclave-signed-tmp/enclave.signed.so ./enclave.signed.so - - - name: Changelog - uses: scottbrenner/generate-changelog-action@master - id: Changelog - - - name: Display structure of downloaded files - run: ls -R - working-directory: . - - - name: Release - id: create-release - uses: softprops/action-gh-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - body: | - ${{ steps.Changelog.outputs.changelog }} - draft: true - name: Docker ${{ github.ref_name }} - files: | - integritee-worker-teeracle-${{ github.ref_name }}.tar.gz - integritee-worker-sidechain-${{ github.ref_name }}.tar.gz - integritee-client - integritee-demo-validateer - enclave.signed.so diff --git a/tee-worker/.github/workflows/check_labels.yml b/tee-worker/.github/workflows/check_labels.yml deleted file mode 100644 index 9511ed0b93..0000000000 --- a/tee-worker/.github/workflows/check_labels.yml +++ /dev/null @@ -1,24 +0,0 @@ -name: Labels Check -on: - pull_request: - types: [opened, labeled, unlabeled, synchronize, ready_for_review] -jobs: - A-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "A0-core,A1-cli,A2-applibs,A3-sidechain,A4-offchain,A5-teeracle,A6-evm,A7-somethingelse" - - B-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "B0-silent,B1-releasenotes" - - C-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "C1-low 📌,C3-medium 📣,C7-high ❗️,C9-critical ‼️" - - E-label-check: - uses: ./.github/workflows/label-checker.yml - with: - predefined_labels: "E0-breaksnothing,E3-hardmerge,E5-publicapi,E6-parentchain,E8-breakseverything" diff --git a/tee-worker/.github/workflows/delete-release.yml b/tee-worker/.github/workflows/delete-release.yml deleted file mode 100644 index 53fbdbb0f3..0000000000 --- a/tee-worker/.github/workflows/delete-release.yml +++ /dev/null @@ -1,70 +0,0 @@ -name: Delete-Release - -on: - release: - types: [deleted] # should be deleted - -jobs: - purge-image: - name: Delete image from ghcr.io - runs-on: ubuntu-latest - strategy: - matrix: - #binary: ["integritee-client", "integritee-demo-validateer"] - binary: ["teeracle"] - steps: - - uses: actions/checkout@v2 - - - name: Set output - id: vars - run: echo "{tag}={$GITHUB_REF#refs/*/}" >> $GITHUB_OUTPUT - - - name: Get Tag - id: get_tag - run: echo ::set-output name=TAG::${GITHUB_REF/refs\/tags\//} - - - name: Check output - env: - RELEASE_VERSION: ${{ steps.get_tag.outputs.TAG }} - run: | - echo $RELEASE_VERSION - echo ${{ steps.vars.outputs.tag }} - echo ${{github.event.pull_request.number}} - - - name: Login to DockerHub - if: github.event_name != 'pull_request' - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - # Unfortunately accessing the repo with personal access token is not possible - # Workaround: disable 2FA and user password instead of TOKEN - - name: Delete docker tag - run: | - ORGANIZATION="integritee" - IMAGE="${{ matrix.binary }}" - TAG="${{ steps.get_tag.outputs.TAG }}" - - login_data() { - cat < /dev/null)" != "" ]]; then - docker image rmi --force integritee/sidechain:${{ github.event.release.tag_name }} 2>/dev/null - fi - docker images --all diff --git a/tee-worker/.github/workflows/publish-docker-teeracle.yml b/tee-worker/.github/workflows/publish-docker-teeracle.yml deleted file mode 100644 index 01a9a6f8b0..0000000000 --- a/tee-worker/.github/workflows/publish-docker-teeracle.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish Docker image for new teeracle release - -on: - release: - types: - - published - -jobs: - main: - name: Push Integritee Teeracle to Dockerhub - runs-on: [ self-hosted ] - steps: - - uses: actions/checkout@v3 - - - name: Download teeracle from release - uses: dsaltares/fetch-gh-release-asset@master - with: - version: "tags/${{ github.event.release.tag_name }}" - file: integritee-worker-teeracle-${{ github.event.release.tag_name }}.tar.gz - target: "integritee-worker-teeracle.tar.gz" - token: ${{ secrets.GITHUB_TOKEN }} - - - - name: Login to Dockerhub - uses: docker/login-action@v1 - with: - username: ${{ secrets.DOCKER_HUB_USERNAME }} - password: ${{ secrets.DOCKER_HUB_ACCESS_TOKEN }} - - - name: Load Worker & Push - env: - DOCKER_BUILDKIT: 1 - run: | - docker image load --input integritee-worker-teeracle.tar.gz - docker images --all - docker push integritee/teeracle:${{ github.event.release.tag_name }} - - - name: Delete images - run: | - if [[ "$(docker images -q integritee/teeracle:${{ github.event.release.tag_name }} 2> /dev/null)" != "" ]]; then - docker image rmi --force integritee/teeracle:${{ github.event.release.tag_name }} 2>/dev/null - fi - docker images --all diff --git a/tee-worker/.github/workflows/publish-draft-release.yml b/tee-worker/.github/workflows/publish-draft-release.yml deleted file mode 100644 index 0e8c72dd6c..0000000000 --- a/tee-worker/.github/workflows/publish-draft-release.yml +++ /dev/null @@ -1,69 +0,0 @@ -name: Release - Publish draft - -on: - push: - tags: - # Catches only v1.2.3 (-dev,-rc1 etc won't be released as SDK) - - v[0-9]+.[0-9]+.[0-9]+ - -jobs: - publish-draft-release: - runs-on: ubuntu-latest - steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - fetch-depth: 0 - path: worker - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: 3.0.0 - - - name: Download srtool json output - uses: actions/download-artifact@v3 - - - name: Prepare tooling - run: | - cd worker/scripts/changelog - gem install bundler changelogerator:0.9.1 - bundle install - changelogerator --help - URL=https://github.com/chevdor/tera-cli/releases/download/v0.2.1/tera-cli_linux_amd64.deb - wget $URL -O tera.deb - sudo dpkg -i tera.deb - tera --version - - - name: Generate release notes - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - DEBUG: 1 - PRE_RELEASE: ${{ github.event.inputs.pre_release }} - run: | - find ${{env.GITHUB_WORKSPACE}} -type f -name "*_srtool_output.json" - - cd worker/scripts/changelog - - ./bin/changelog ${GITHUB_REF} - ls -al release-notes.md - ls -al context.json - - - name: Archive artifact context.json - uses: actions/upload-artifact@v3 - with: - name: release-notes-context - path: | - worker/scripts/changelog/context.json - **/*_srtool_output.json - - - name: Create draft release - id: create-release - uses: actions/create-release@v1 - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - tag_name: ${{ github.ref }} - release_name: SDK ${{ github.ref }} - body_path: ./worker/scripts/changelog/release-notes.md - draft: true From 4276678c0fb3d1d3d6f985135f65039ce2f91e63 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Thu, 1 Feb 2024 14:07:30 +0100 Subject: [PATCH 05/64] fix on taplo 0.8.1 release (#2458) --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7785e11e78..ed0f342620 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,7 +153,7 @@ jobs: - name: Install pre-built taplo run: | mkdir -p $HOME/.local/bin - wget -q https://github.com/tamasfe/taplo/releases/latest/download/taplo-linux-x86_64.gz + wget -q https://github.com/tamasfe/taplo/releases/download/0.8.1/taplo-linux-x86_64.gz gzip -d taplo-linux-x86_64.gz cp taplo-linux-x86_64 $HOME/.local/bin/taplo chmod a+x $HOME/.local/bin/taplo From 81e8402cce68dc9d214e831d3d9e3c91153925d4 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Thu, 1 Feb 2024 14:53:58 +0100 Subject: [PATCH 06/64] Ethereum and Bitcoin key repository (#2454) * init eth and btc wallet * fmt --------- Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- bitacross-worker/Cargo.lock | 1 + .../core-primitives/sgx/crypto/Cargo.toml | 1 + .../core-primitives/sgx/crypto/src/lib.rs | 1 + .../sgx/crypto/src/secp256k1.rs | 168 ++++++++++++++++++ bitacross-worker/enclave-runtime/Cargo.lock | 1 + bitacross-worker/enclave-runtime/Makefile | 4 +- .../src/initialization/global_components.rs | 18 +- .../enclave-runtime/src/initialization/mod.rs | 16 +- 8 files changed, 206 insertions(+), 4 deletions(-) create mode 100644 bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index a0631f3ce6..6d07382944 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -5253,6 +5253,7 @@ dependencies = [ "derive_more", "itp-sgx-io", "itp-sgx-temp-dir", + "libsecp256k1", "log 0.4.20", "ofb", "parity-scale-codec", diff --git a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml index fd8a971e49..c7f4f35360 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml +++ b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml @@ -9,6 +9,7 @@ edition = "2021" aes = { version = "0.6.0" } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } derive_more = { version = "0.99.5" } +libsecp256k1 = { version = "0.7.1", default-features = false } log = { version = "0.4", default-features = false } ofb = { version = "0.4.0" } serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs index 832239c027..b530e16788 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs @@ -38,6 +38,7 @@ pub mod ed25519_derivation; pub mod error; pub mod key_repository; pub mod rsa3072; +pub mod secp256k1; pub mod traits; pub use self::{aes::*, ed25519::*, rsa3072::*}; diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs b/bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs new file mode 100644 index 0000000000..e31d6f0c26 --- /dev/null +++ b/bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs @@ -0,0 +1,168 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . +#[cfg(feature = "sgx")] +pub use sgx::*; + +pub use libsecp256k1; + +use libsecp256k1::{PublicKey, SecretKey}; + +/// File name of the sealed seed file. +pub const SEALED_SIGNER_SEED_FILE: &str = "secp256k1_key_sealed.bin"; + +#[derive(Clone, PartialEq)] +pub struct Pair { + pub public: PublicKey, + pub secret: SecretKey, +} + +#[cfg(feature = "sgx")] +pub mod sgx { + use super::SEALED_SIGNER_SEED_FILE; + use crate::{ + error::{Error, Result}, + key_repository::KeyRepository, + secp256k1::Pair, + std::string::ToString, + }; + use itp_sgx_io::{seal, unseal, SealedIO}; + use libsecp256k1::{PublicKey, SecretKey}; + use log::*; + use sgx_rand::{Rng, StdRng}; + use std::{path::PathBuf, string::String}; + + /// Creates a repository for an secp256k1 keypair and initializes + /// a fresh private key if it doesn't exist at `path`. + pub fn create_secp256k1_repository( + path: PathBuf, + key_file_prefix: &str, + ) -> Result> { + let seal = Seal::new(path, key_file_prefix.to_string()); + Ok(KeyRepository::new(seal.init()?, seal.into())) + } + + #[derive(Clone, Debug)] + pub struct Seal { + base_path: PathBuf, + key_file_prefix: String, + } + + impl Seal { + pub fn new(base_path: PathBuf, key_file_prefix: String) -> Self { + Self { base_path, key_file_prefix } + } + + pub fn path(&self) -> PathBuf { + self.base_path + .join(self.key_file_prefix.clone() + "_" + SEALED_SIGNER_SEED_FILE) + } + } + + impl Seal { + fn unseal_pair(&self) -> Result { + self.unseal() + } + + fn exists(&self) -> bool { + self.path().exists() + } + + fn init(&self) -> Result { + if !self.exists() { + info!("Keyfile not found, creating new! {}", self.path().display()); + let mut seed = [0u8; 32]; + let mut rand = StdRng::new()?; + rand.fill_bytes(&mut seed); + seal(&seed, self.path())?; + } + self.unseal_pair() + } + } + + impl SealedIO for Seal { + type Error = Error; + type Unsealed = Pair; + + fn unseal(&self) -> Result { + let raw = unseal(self.path())?; + + let secret = SecretKey::parse_slice(&raw) + .map_err(|e| Error::Other(format!("{:?}", e).into()))?; + let public = PublicKey::from_secret_key(&secret); + + Ok(Pair { public, secret }) + } + + fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { + let raw = unsealed.secret.serialize(); + seal(&raw, self.path()).map_err(|e| e.into()) + } + } +} + +#[cfg(feature = "test")] +pub mod sgx_tests { + use super::sgx::*; + use crate::{key_repository::AccessKey, secp256k1::Pair, std::string::ToString, ToPubkey}; + use itp_sgx_temp_dir::TempDir; + use std::path::{Path, PathBuf}; + + #[test] + pub fn creating_repository_with_same_path_and_prefix_results_in_same_key() { + let key_file_prefix = "test"; + fn get_key_from_repo(path: PathBuf, prefix: &str) -> Pair { + create_secp256k1_repository(path, prefix).unwrap().retrieve_key().unwrap() + } + let temp_dir = TempDir::with_prefix( + "creating_repository_with_same_path_and_prefix_results_in_same_key", + ) + .unwrap(); + let temp_path = temp_dir.path().to_path_buf(); + assert_eq!( + get_key_from_repo(temp_path.clone(), key_file_prefix), + get_key_from_repo(temp_path.clone(), key_file_prefix) + ); + } + + #[test] + pub fn seal_init_should_create_new_key_if_not_present() { + //given + let temp_dir = + TempDir::with_prefix("seal_init_should_create_new_key_if_not_present").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + assert!(!seal.exists()); + + //when + seal.init().unwrap(); + + //then + assert!(seal.exists()); + } + + #[test] + pub fn seal_init_should_not_change_key_if_exists() { + //given + let temp_dir = TempDir::with_prefix("seal_init_should_not_change_key_if_exists").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + let pair = seal.init().unwrap(); + + //when + let new_pair = seal.init().unwrap(); + + //then + assert!(pair, new_pair); + } +} diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 5506e3e89e..ae5936ad0b 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -2279,6 +2279,7 @@ dependencies = [ "derive_more", "itp-sgx-io", "itp-sgx-temp-dir", + "libsecp256k1", "log", "ofb", "parity-scale-codec", diff --git a/bitacross-worker/enclave-runtime/Makefile b/bitacross-worker/enclave-runtime/Makefile index b4dc322eed..bf3aa55dee 100644 --- a/bitacross-worker/enclave-runtime/Makefile +++ b/bitacross-worker/enclave-runtime/Makefile @@ -27,8 +27,8 @@ # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ######## Worker Feature Settings ######## -# Set sidechain as default feature mode -WORKER_MODE ?= sidechain +# Set offchain-worker as default feature mode +WORKER_MODE ?= offchain-worker Rust_Enclave_Name := libenclave.a Rust_Enclave_Files := $(wildcard src/*.rs) $(wildcard ../stf/src/*.rs) diff --git a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs index 8f45ddcc7f..d0aa22e265 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs @@ -63,7 +63,11 @@ use itp_node_api::{ metadata::{provider::NodeMetadataRepository, NodeMetadata}, }; use itp_nonce_cache::NonceCache; -use itp_sgx_crypto::{key_repository::KeyRepository, Aes, AesSeal, Ed25519Seal, Rsa3072Seal}; +use itp_sgx_crypto::{ + key_repository::KeyRepository, + secp256k1::{Pair as Secp256k1Pair, Seal as Secp256k1Seal}, + Aes, AesSeal, Ed25519Seal, Rsa3072Seal, +}; use itp_stf_executor::{ enclave_signer::StfEnclaveSigner, executor::StfExecutor, getter_executor::GetterExecutor, state_getter::StfStateGetter, @@ -106,6 +110,8 @@ pub type EnclaveStf = Stf; pub type EnclaveShieldingKeyRepository = KeyRepository; pub type EnclaveSigningKeyRepository = KeyRepository; +pub type EnclaveBitcoinKeyRepository = KeyRepository; +pub type EnclaveEthereumKeyRepository = KeyRepository; pub type EnclaveStateFileIo = SgxStateFileIo; pub type EnclaveStateSnapshotRepository = StateSnapshotRepository; pub type EnclaveStateObserver = StateObserver; @@ -381,6 +387,16 @@ pub static GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT: ComponentContainer< EnclaveSigningKeyRepository, > = ComponentContainer::new("Signing key repository"); +/// Bitcoin key repository +pub static GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT: ComponentContainer< + EnclaveBitcoinKeyRepository, +> = ComponentContainer::new("Bitcoin key repository"); + +/// Ethereum key repository +pub static GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT: ComponentContainer< + EnclaveEthereumKeyRepository, +> = ComponentContainer::new("Ethereum key repository"); + /// Light client db seal for the Integritee parentchain pub static GLOBAL_INTEGRITEE_PARENTCHAIN_LIGHT_CLIENT_SEAL: ComponentContainer< EnclaveLightClientSeal, diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index 1510341a61..f97b9e3679 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -27,7 +27,8 @@ use crate::{ EnclaveStateHandler, EnclaveStateInitializer, EnclaveStateObserver, EnclaveStateSnapshotRepository, EnclaveStfEnclaveSigner, EnclaveTopPool, EnclaveTopPoolAuthor, DIRECT_RPC_REQUEST_SINK_COMPONENT, - GLOBAL_ATTESTATION_HANDLER_COMPONENT, GLOBAL_DIRECT_RPC_BROADCASTER_COMPONENT, + GLOBAL_ATTESTATION_HANDLER_COMPONENT, GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT, + GLOBAL_DIRECT_RPC_BROADCASTER_COMPONENT, GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT, GLOBAL_INTEGRITEE_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_OCALL_API_COMPONENT, GLOBAL_RPC_WS_HANDLER_COMPONENT, GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT, GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT, GLOBAL_SIDECHAIN_BLOCK_SYNCER_COMPONENT, @@ -70,6 +71,7 @@ use itp_settings::files::{ }; use itp_sgx_crypto::{ get_aes_repository, get_ed25519_repository, get_rsa3072_repository, key_repository::AccessKey, + secp256k1::create_secp256k1_repository, }; use itp_stf_state_handler::{ file_io::StateDir, handle_state::HandleState, query_shard_state::QueryShardState, @@ -99,6 +101,18 @@ pub(crate) fn init_enclave( let signer = signing_key_repository.retrieve_key()?; info!("[Enclave initialized] Ed25519 prim raw : {:?}", signer.public().0); + let bitcoin_key_repository = + Arc::new(create_secp256k1_repository(base_dir.clone(), "bitcoin")?); + GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT.initialize(bitcoin_key_repository.clone()); + let bitcoin_key = bitcoin_key_repository.retrieve_key()?; + info!("[Enclave initialized] Bitcoin public key raw : {:?}", bitcoin_key.public.serialize()); + + let ethereum_key_repository = + Arc::new(create_secp256k1_repository(base_dir.clone(), "ethereum")?); + GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.initialize(ethereum_key_repository.clone()); + let ethereum_key = ethereum_key_repository.retrieve_key()?; + info!("[Enclave initialized] Ethereum public key raw : {:?}", ethereum_key.public.serialize()); + let shielding_key_repository = Arc::new(get_rsa3072_repository(base_dir.clone())?); GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT.initialize(shielding_key_repository.clone()); From a0a007f34c962727bd754033614d58327c340464 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Fri, 2 Feb 2024 15:27:45 +0100 Subject: [PATCH 07/64] add litentry archive service mock (#2462) --- tee-worker/docker/docker-compose.yml | 1 + .../docker/multiworker-docker-compose.yml | 3 ++ .../litentry/core/assertion-build/src/a20.rs | 16 ++++---- .../litentry/core/data-providers/src/lib.rs | 9 +++++ .../litentry/core/mock-server/src/lib.rs | 2 + .../core/mock-server/src/litentry_archive.rs | 37 +++++++++++++++++++ .../receiver/src/handler/assertion.rs | 3 +- .../lc-vc-task-receiver/src/vc_handling.rs | 3 +- tee-worker/local-setup/.env.dev | 3 +- 9 files changed, 66 insertions(+), 11 deletions(-) create mode 100644 tee-worker/litentry/core/mock-server/src/litentry_archive.rs diff --git a/tee-worker/docker/docker-compose.yml b/tee-worker/docker/docker-compose.yml index 707588493d..44d8743e33 100644 --- a/tee-worker/docker/docker-compose.yml +++ b/tee-worker/docker/docker-compose.yml @@ -135,6 +135,7 @@ services: - CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID + - LITENTRY_ARCHIVE_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: diff --git a/tee-worker/docker/multiworker-docker-compose.yml b/tee-worker/docker/multiworker-docker-compose.yml index 70225a3d47..5e92664999 100644 --- a/tee-worker/docker/multiworker-docker-compose.yml +++ b/tee-worker/docker/multiworker-docker-compose.yml @@ -136,6 +136,7 @@ services: - CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID + - LITENTRY_ARCHIVE_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: @@ -186,6 +187,7 @@ services: - CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID + - LITENTRY_ARCHIVE_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: @@ -236,6 +238,7 @@ services: - CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID + - LITENTRY_ARCHIVE_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: diff --git a/tee-worker/litentry/core/assertion-build/src/a20.rs b/tee-worker/litentry/core/assertion-build/src/a20.rs index 40fa7e72b5..859f4358d9 100644 --- a/tee-worker/litentry/core/assertion-build/src/a20.rs +++ b/tee-worker/litentry/core/assertion-build/src/a20.rs @@ -23,7 +23,7 @@ extern crate sgx_tstd as std; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{error::Error as RestClientError, RestGet, RestPath}; -use lc_data_providers::build_client; +use lc_data_providers::{build_client, DataProviderConfig}; use serde::{Deserialize, Serialize}; #[cfg(all(not(feature = "std"), feature = "sgx"))] @@ -42,12 +42,15 @@ pub struct EarlyBirdResponse { has_joined: bool, } impl RestPath for EarlyBirdResponse { - fn get_path(path: String) -> core::result::Result { - Ok(path) + fn get_path(_path: String) -> core::result::Result { + Ok("events/does-user-joined-evm-campaign".to_string()) } } -pub fn build(req: &AssertionBuildRequest) -> Result { +pub fn build( + req: &AssertionBuildRequest, + data_provider_config: &DataProviderConfig, +) -> Result { // Note: it's not perfectly implemented here // it only attests if the main address meets the criteria, but we should have implemented // the supported web3networks and attested the linked identities. @@ -62,10 +65,7 @@ pub fn build(req: &AssertionBuildRequest) -> Result { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let mut client = build_client( - "https://archive-test.litentry.io/events/does-user-joined-evm-campaign", - headers, - ); + let mut client = build_client(&data_provider_config.litentry_archive_url, headers); let query = vec![("account", who.as_str())]; let value = client .get_with::("".to_string(), query.as_slice()) diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index 973da9c4ff..c0fac547ce 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -189,6 +189,7 @@ pub struct DataProviderConfig { pub vip3_url: String, pub geniidata_url: String, pub geniidata_api_key: String, + pub litentry_archive_url: String, } impl Default for DataProviderConfig { @@ -229,6 +230,7 @@ impl DataProviderConfig { vip3_url: "https://dappapi.vip3.io/".to_string(), geniidata_url: "https://api.geniidata.com/api/1/brc20/balance?".to_string(), geniidata_api_key: "".to_string(), + litentry_archive_url: "https://archive-test.litentry.io".to_string(), }; // we allow to override following config properties for non prod dev @@ -287,6 +289,9 @@ impl DataProviderConfig { if let Ok(v) = env::var("GENIIDATA_URL") { config.set_geniidata_url(v); } + if let Ok(v) = env::var("LITENTRY_ARCHIVE_URL") { + config.set_litentry_archive_url(v); + } }); // set secrets from env variables if let Ok(v) = env::var("TWITTER_AUTH_TOKEN_V2") { @@ -405,6 +410,10 @@ impl DataProviderConfig { debug!("set_geniidata_api_key: {:?}", v); self.geniidata_api_key = v; } + pub fn set_litentry_archive_url(&mut self, v: String) { + debug!("set_litentry_archive_url: {:?}", v); + self.litentry_archive_url = v; + } } #[derive(Debug, thiserror::Error, Clone)] diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index ebe5f3245b..f60f4a303d 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -24,6 +24,7 @@ use warp::Filter; pub mod achainable; pub mod discord_litentry; pub mod discord_official; +pub mod litentry_archive; pub mod nodereal_jsonrpc; pub mod twitter_litentry; pub mod twitter_official; @@ -63,6 +64,7 @@ pub fn run(port: u16) -> Result { .or(discord_litentry::has_role()) .or(nodereal_jsonrpc::query()) .or(achainable::query()) + .or(litentry_archive::query_user_joined_evm_campaign()) .boxed(), ) .bind_with_graceful_shutdown(([127, 0, 0, 1], port), shutdown_signal()); diff --git a/tee-worker/litentry/core/mock-server/src/litentry_archive.rs b/tee-worker/litentry/core/mock-server/src/litentry_archive.rs new file mode 100644 index 0000000000..f256fc7d41 --- /dev/null +++ b/tee-worker/litentry/core/mock-server/src/litentry_archive.rs @@ -0,0 +1,37 @@ +// Copyright 2020-2023 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . +#![allow(opaque_hidden_inferred_bound)] + +use std::collections::HashMap; +use warp::{http::Response, Filter}; + +pub(crate) fn query_user_joined_evm_campaign( +) -> impl Filter + Clone { + warp::get() + .and(warp::path!("events" / "does-user-joined-evm-campaign")) + .and(warp::query::>()) + .map(move |p: HashMap| { + let default = String::default(); + let account = p.get("account").unwrap_or(&default); + if account == "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" { + let body = r#"{ "hasJoined": true }"#; + Response::builder().body(body.to_string()) + } else { + let body = r#"{ "hasJoined": false }"#; + Response::builder().body(body.to_string()) + } + }) +} diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index dc9c355e85..9ea4dd0743 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -134,7 +134,8 @@ where &self.context.data_provider_config, ), - Assertion::A20 => lc_assertion_build::a20::build(&self.req), + Assertion::A20 => + lc_assertion_build::a20::build(&self.req, &self.context.data_provider_config), Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( &self.req, diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs index d21759f4a9..e3d05c2b22 100644 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs +++ b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs @@ -110,7 +110,8 @@ where &self.context.data_provider_config, ), - Assertion::A20 => lc_assertion_build::a20::build(&self.req), + Assertion::A20 => + lc_assertion_build::a20::build(&self.req, &self.context.data_provider_config), Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( &self.req, diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index 7c9cf8f7bb..bc89ff9b61 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -49,4 +49,5 @@ CONTEST_LEGEND_DISCORD_ROLE_ID=1172576273063739462 CONTEST_POPULARITY_DISCORD_ROLE_ID=1172576681119195208 CONTEST_PARTICIPANT_DISCORD_ROLE_ID=1172576734135210104 VIP3_URL=https://dappapi.vip3.io/ -GENIIDATA_URL=https://api.geniidata.com/api/1/brc20/balance? \ No newline at end of file +GENIIDATA_URL=https://api.geniidata.com/api/1/brc20/balance? +LITENTRY_ARCHIVE_URL=https://archive-test.litentry.io \ No newline at end of file From 7fe49591fce64e9fea20aa413fd94da434941e0f Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Fri, 2 Feb 2024 15:37:00 +0100 Subject: [PATCH 08/64] local dev env config (#2463) --- tee-worker/local-setup/.env.dev | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index bc89ff9b61..668e449b65 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -34,20 +34,20 @@ GENIIDATA_API_KEY=142cf1b0-1ca7-11ee-bb5e-9d74c2e854ac # The followings are default value. # Can be skipped; or overwrite within non-production mode. -TWITTER_OFFICIAL_URL=https://api.twitter.com +TWITTER_OFFICIAL_URL=http://localhost:19527 TWITTER_LITENTRY_URL=http://localhost:19527 -DISCORD_OFFICIAL_URL=https://discordapp.com +DISCORD_OFFICIAL_URL=http://localhost:19527 DISCORD_LITENTRY_URL=http://localhost:19527 -ACHAINABLE_URL=https://label-production.graph.tdf-labs.io/ -CREDENTIAL_ENDPOINT=wss://rpc.rococo-parachain.litentry.io -ONEBLOCK_NOTION_URL=https://api.notion.com/v1/blocks/e4068e6a326243468f35dcdc0c43f686/children -SORA_QUIZ_MASTER_ID=1164463721989554218 -SORA_QUIZ_ATTENDEE_ID=1166941149219532800 -NODEREAL_API_URL=https://open-platform.nodereal.io/ +ACHAINABLE_URL=http://localhost:19527 +CREDENTIAL_ENDPOINT=http://localhost:9933 +ONEBLOCK_NOTION_URL=https://abc.com +SORA_QUIZ_MASTER_ID=SORA_QUIZ_MASTER_ID +SORA_QUIZ_ATTENDEE_ID=SORA_QUIZ_ATTENDEE_ID +NODEREAL_API_URL=http://localhost:19527 NODEREAL_API_CHAIN_NETWORK_URL=https://{chain}-mainnet.nodereal.io/ -CONTEST_LEGEND_DISCORD_ROLE_ID=1172576273063739462 -CONTEST_POPULARITY_DISCORD_ROLE_ID=1172576681119195208 -CONTEST_PARTICIPANT_DISCORD_ROLE_ID=1172576734135210104 +CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID +CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID +CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID VIP3_URL=https://dappapi.vip3.io/ GENIIDATA_URL=https://api.geniidata.com/api/1/brc20/balance? -LITENTRY_ARCHIVE_URL=https://archive-test.litentry.io \ No newline at end of file +LITENTRY_ARCHIVE_URL=http://localhost:19527 \ No newline at end of file From 76bdf1fc50ce18f6e96844166aeab737f1f208d5 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Fri, 2 Feb 2024 16:27:02 +0100 Subject: [PATCH 09/64] Support payload digest in trusted verification (#2460) * Use blake2_256 hash for tcs sig * update trusted getter * small update --- tee-worker/app-libs/stf/src/getter.rs | 17 +++++++++-------- tee-worker/app-libs/stf/src/trusted_call.rs | 7 +++++-- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tee-worker/app-libs/stf/src/getter.rs b/tee-worker/app-libs/stf/src/getter.rs index c80cc31fbf..c3405ba208 100644 --- a/tee-worker/app-libs/stf/src/getter.rs +++ b/tee-worker/app-libs/stf/src/getter.rs @@ -23,6 +23,7 @@ use itp_utils::stringify::account_id_to_string; use litentry_macros::if_production_or; use litentry_primitives::{Identity, LitentryMultiSignature}; use log::*; +use sp_core::blake2_256; use sp_std::vec; use std::prelude::v1::*; @@ -145,7 +146,7 @@ impl TrustedGetter { } pub fn sign(&self, pair: &KeyPair) -> TrustedGetterSigned { - let signature = pair.sign(self.encode().as_slice()); + let signature = pair.sign(&blake2_256(self.encode().as_slice())); TrustedGetterSigned { getter: self.clone(), signature } } } @@ -162,18 +163,18 @@ impl TrustedGetterSigned { } pub fn verify_signature(&self) -> bool { + let payload = self.getter.encode(); // in non-prod, we accept signature from Alice too if_production_or!( { - self.signature - .verify(self.getter.encode().as_slice(), self.getter.sender_identity()) + self.signature.verify(&payload, self.getter.sender_identity()) + || self.signature.verify(&blake2_256(&payload), self.getter.sender_identity()) }, { - self.signature - .verify(self.getter.encode().as_slice(), self.getter.sender_identity()) - || self - .signature - .verify(self.getter.encode().as_slice(), &ALICE_ACCOUNTID32.into()) + self.signature.verify(&payload, self.getter.sender_identity()) + || self.signature.verify(&blake2_256(&payload), self.getter.sender_identity()) + || self.signature.verify(&payload, &ALICE_ACCOUNTID32.into()) + || self.signature.verify(&blake2_256(&payload), &ALICE_ACCOUNTID32.into()) } ) } diff --git a/tee-worker/app-libs/stf/src/trusted_call.rs b/tee-worker/app-libs/stf/src/trusted_call.rs index 045eaa5c6a..d18f18dd86 100644 --- a/tee-worker/app-libs/stf/src/trusted_call.rs +++ b/tee-worker/app-libs/stf/src/trusted_call.rs @@ -264,7 +264,8 @@ impl TrustedCallSigning for TrustedCall { payload.append(&mut mrenclave.encode()); payload.append(&mut shard.encode()); - TrustedCallSigned { call: self.clone(), nonce, signature: pair.sign(payload.as_slice()) } + // use blake2_256 hash to shorten the payload - see `verify_signature` below + TrustedCallSigned { call: self.clone(), nonce, signature: pair.sign(&blake2_256(&payload)) } } } @@ -317,7 +318,9 @@ impl TrustedCallVerification for TrustedCallSigned { payload.append(&mut mrenclave.encode()); payload.append(&mut shard.encode()); - self.signature.verify(payload.as_slice(), self.call.sender_identity()) + // make it backwards compatible for now - will deprecate the old way later + self.signature.verify(&blake2_256(&payload), self.call.sender_identity()) + || self.signature.verify(&payload, self.call.sender_identity()) } fn metric_name(&self) -> &'static str { From 1d99778700b987f70ac7f343a3da36eabf28f34b Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Fri, 2 Feb 2024 21:01:55 +0530 Subject: [PATCH 10/64] fix: add generating config.json (#2461) --- bitacross-worker/local-setup/launch.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/bitacross-worker/local-setup/launch.py b/bitacross-worker/local-setup/launch.py index ae4a6e5b24..3adfc131b5 100755 --- a/bitacross-worker/local-setup/launch.py +++ b/bitacross-worker/local-setup/launch.py @@ -102,6 +102,26 @@ def check_all_ports_and_reallocate(): reallocate_ports(x, os.environ.get(x)) print("All preliminary port checks completed") + +# Generate `config.local.json` used by parachain ts utils +def generate_config_local_json(parachain_dir): + data = { + "eth_endpoint": "http://127.0.0.1:8545", + "eth_address": "[0x4d88dc5d528a33e4b8be579e9476715f60060582]", + "private_key": "0xe82c0c4259710bb0d6cf9f9e8d0ad73419c1278a14d375e5ca691e7618103011", + "ocw_account": "5FEYX9NES9mAJt1Xg4WebmHWywxyeGQK8G3oEBXtyfZrRePX", + "genesis_state_path": parachain_dir+"/genesis-state", + "genesis_wasm_path": parachain_dir+"/genesis-wasm", + "parachain_ws": "ws://localhost:" + os.environ.get("CollatorWSPort", "9944"), + "relaychain_ws": "ws://localhost:" + os.environ.get("AliceWSPort", "9946"), + "bridge_path": "/tmp/parachain_dev/chainbridge", + } + config_file = "../ts-tests/config.local.json" + + with open(config_file, "w") as f: + json.dump(data, f, indent=4) + + print("Successfully written ", config_file) def run_node(config, i: int): @@ -122,6 +142,7 @@ def setup_environment(offset, config, parachain_dir): load_dotenv(".env.dev") offset_port(offset) check_all_ports_and_reallocate() + generate_config_local_json(parachain_dir) # TODO: only works for single worker for now for p in [ From 04016d2e2fe0b43671259f59cbba7b74faf8eff1 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Fri, 2 Feb 2024 18:18:33 +0100 Subject: [PATCH 11/64] Refactor teerex/sidechain pallet into teebag (#2459) * init * init refactoring * rococo runtime compiles * add tests * add sidchain methods * small update * fix tests * Remove ScheduledEnclaveNotApplicable * remove comment --- Cargo.lock | 386 +- Cargo.toml | 1 + .../enclave-runtime/src/attestation.rs | 2 - node/src/chain_specs/rococo.rs | 11 +- pallets/teebag/Cargo.toml | 78 + pallets/teebag/README.md | 11 + pallets/teebag/src/lib.rs | 828 ++++ pallets/teebag/src/mock.rs | 185 + pallets/teebag/src/quoting_enclave.rs | 78 + .../AttestationReportSigningCACert.pem | 31 + pallets/teebag/src/sgx_verify/collateral.rs | 277 ++ .../teebag/src/sgx_verify/ephemeral_key.rs | 31 + pallets/teebag/src/sgx_verify/fuzz/Cargo.lock | 3589 +++++++++++++++++ pallets/teebag/src/sgx_verify/fuzz/Cargo.toml | 58 + .../fuzz/fuzz_targets/decode_quote.rs | 15 + .../fuzz/fuzz_targets/deserialize_json.rs | 11 + .../fuzz/fuzz_targets/extract_tcb_info.rs | 8 + .../fuzz/fuzz_targets/signature_check.rs | 25 + .../fuzz/fuzz_targets/verify_ias_report.rs | 9 + pallets/teebag/src/sgx_verify/mod.rs | 882 ++++ .../teebag/src/sgx_verify/netscape_comment.rs | 48 + .../sgx_verify/test/dcap/dcap_quote_cert.der | 25 + .../src/sgx_verify/test/dcap/pck_crl.der | 1 + .../test/dcap/pck_crl_issuer_chain.pem | 32 + .../src/sgx_verify/test/dcap/qe_identity.json | 1 + .../sgx_verify/test/dcap/qe_identity_cert.pem | 14 + .../test/dcap/qe_identity_issuer_chain.pem | 32 + .../src/sgx_verify/test/dcap/root_ca_crl.der | 1 + .../src/sgx_verify/test/dcap/tcb_info.json | 1 + .../test/dcap/tcb_info_issuer_chain.pem | 32 + .../test/enclave-signing-pubkey-TEST4.bin | 2 + .../test/enclave-signing-pubkey-TEST5.bin | 2 + .../test/enclave-signing-pubkey-TEST6.bin | 1 + .../test/enclave-signing-pubkey-TEST7.bin | 1 + ...nclave-signing-pubkey-TEST8-PRODUCTION.bin | Bin 0 -> 32 bytes .../sgx_verify/test/ra_dump_cert_TEST4.der | Bin 0 -> 3234 bytes .../sgx_verify/test/ra_dump_cert_TEST5.der | Bin 0 -> 3234 bytes .../sgx_verify/test/ra_dump_cert_TEST6.der | Bin 0 -> 3233 bytes .../sgx_verify/test/ra_dump_cert_TEST7.der | Bin 0 -> 3233 bytes .../test/ra_dump_cert_TEST8_PRODUCTION.der | Bin 0 -> 3385 bytes .../test_ra_cert_MRSIGNER1_MRENCLAVE1.der | Bin 0 -> 3232 bytes .../test_ra_cert_MRSIGNER2_MRENCLAVE2.der | Bin 0 -> 3232 bytes .../test_ra_cert_MRSIGNER3_MRENCLAVE2.der | Bin 0 -> 3231 bytes ...st_ra_signer_attn_MRSIGNER1_MRENCLAVE1.bin | Bin 0 -> 64 bytes ...st_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin | 1 + ...st_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin | 2 + ..._ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin | 1 + ..._ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin | 1 + ..._ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin | 1 + pallets/teebag/src/sgx_verify/tests.rs | 298 ++ pallets/teebag/src/sgx_verify/utils.rs | 38 + pallets/teebag/src/tcb.rs | 103 + pallets/teebag/src/tests.rs | 414 ++ pallets/teebag/src/types.rs | 179 + runtime/common/Cargo.toml | 3 + runtime/common/src/lib.rs | 15 +- runtime/litmus/src/lib.rs | 2 +- runtime/rococo/Cargo.toml | 4 + runtime/rococo/src/lib.rs | 9 + tee-worker/Cargo.lock | 28 + tee-worker/enclave-runtime/src/attestation.rs | 2 - 61 files changed, 7619 insertions(+), 191 deletions(-) create mode 100644 pallets/teebag/Cargo.toml create mode 100644 pallets/teebag/README.md create mode 100644 pallets/teebag/src/lib.rs create mode 100644 pallets/teebag/src/mock.rs create mode 100644 pallets/teebag/src/quoting_enclave.rs create mode 100644 pallets/teebag/src/sgx_verify/AttestationReportSigningCACert.pem create mode 100644 pallets/teebag/src/sgx_verify/collateral.rs create mode 100644 pallets/teebag/src/sgx_verify/ephemeral_key.rs create mode 100644 pallets/teebag/src/sgx_verify/fuzz/Cargo.lock create mode 100644 pallets/teebag/src/sgx_verify/fuzz/Cargo.toml create mode 100644 pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/decode_quote.rs create mode 100644 pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/deserialize_json.rs create mode 100644 pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/extract_tcb_info.rs create mode 100644 pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/signature_check.rs create mode 100644 pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/verify_ias_report.rs create mode 100644 pallets/teebag/src/sgx_verify/mod.rs create mode 100644 pallets/teebag/src/sgx_verify/netscape_comment.rs create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/dcap_quote_cert.der create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/pck_crl.der create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/pck_crl_issuer_chain.pem create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/qe_identity.json create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/qe_identity_cert.pem create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/qe_identity_issuer_chain.pem create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/root_ca_crl.der create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/tcb_info.json create mode 100644 pallets/teebag/src/sgx_verify/test/dcap/tcb_info_issuer_chain.pem create mode 100644 pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST4.bin create mode 100644 pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST5.bin create mode 100644 pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST6.bin create mode 100644 pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST7.bin create mode 100644 pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST8-PRODUCTION.bin create mode 100644 pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST4.der create mode 100644 pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST5.der create mode 100644 pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST6.der create mode 100644 pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST7.der create mode 100644 pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST8_PRODUCTION.der create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER1_MRENCLAVE1.der create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER2_MRENCLAVE2.der create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER3_MRENCLAVE2.der create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER1_MRENCLAVE1.bin create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin create mode 100644 pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin create mode 100644 pallets/teebag/src/sgx_verify/tests.rs create mode 100644 pallets/teebag/src/sgx_verify/utils.rs create mode 100644 pallets/teebag/src/tcb.rs create mode 100644 pallets/teebag/src/tests.rs create mode 100644 pallets/teebag/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index 4e9e5bcfdd..f94641285d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,7 +570,7 @@ dependencies = [ [[package]] name = "binary-merkle-tree" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -3191,7 +3191,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fork-tree" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", ] @@ -3312,7 +3312,7 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "frame-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-support-procedural", @@ -3337,7 +3337,7 @@ dependencies = [ [[package]] name = "frame-benchmarking-cli" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "array-bytes 4.2.0", @@ -3384,7 +3384,7 @@ dependencies = [ [[package]] name = "frame-election-provider-solution-type" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3395,7 +3395,7 @@ dependencies = [ [[package]] name = "frame-election-provider-support" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-election-provider-solution-type", "frame-support", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "frame-executive" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -3441,7 +3441,7 @@ dependencies = [ [[package]] name = "frame-remote-externalities" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-recursion", "futures 0.3.30", @@ -3461,7 +3461,7 @@ dependencies = [ [[package]] name = "frame-support" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bitflags 1.3.2", "environmental", @@ -3494,7 +3494,7 @@ dependencies = [ [[package]] name = "frame-support-procedural" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "cfg-expr", @@ -3510,7 +3510,7 @@ dependencies = [ [[package]] name = "frame-support-procedural-tools" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support-procedural-tools-derive", "proc-macro-crate", @@ -3522,7 +3522,7 @@ dependencies = [ [[package]] name = "frame-support-procedural-tools-derive" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -3532,7 +3532,7 @@ dependencies = [ [[package]] name = "frame-system" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "log", @@ -3550,7 +3550,7 @@ dependencies = [ [[package]] name = "frame-system-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -3565,7 +3565,7 @@ dependencies = [ [[package]] name = "frame-system-rpc-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -3574,7 +3574,7 @@ dependencies = [ [[package]] name = "frame-try-runtime" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "parity-scale-codec", @@ -5854,7 +5854,7 @@ dependencies = [ [[package]] name = "mmr-gadget" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "log", @@ -5873,7 +5873,7 @@ dependencies = [ [[package]] name = "mmr-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "jsonrpsee", @@ -6791,7 +6791,7 @@ dependencies = [ [[package]] name = "pallet-aura" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6807,7 +6807,7 @@ dependencies = [ [[package]] name = "pallet-authority-discovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6823,7 +6823,7 @@ dependencies = [ [[package]] name = "pallet-authorship" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6837,7 +6837,7 @@ dependencies = [ [[package]] name = "pallet-babe" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -6861,7 +6861,7 @@ dependencies = [ [[package]] name = "pallet-bags-list" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -6881,7 +6881,7 @@ dependencies = [ [[package]] name = "pallet-balances" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -6896,7 +6896,7 @@ dependencies = [ [[package]] name = "pallet-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6915,7 +6915,7 @@ dependencies = [ [[package]] name = "pallet-beefy-mmr" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "binary-merkle-tree", @@ -6939,7 +6939,7 @@ dependencies = [ [[package]] name = "pallet-bounties" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -6995,7 +6995,7 @@ dependencies = [ [[package]] name = "pallet-child-bounties" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7033,7 +7033,7 @@ dependencies = [ [[package]] name = "pallet-collective" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7050,7 +7050,7 @@ dependencies = [ [[package]] name = "pallet-conviction-voting" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "assert_matches", "frame-benchmarking", @@ -7067,7 +7067,7 @@ dependencies = [ [[package]] name = "pallet-democracy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7101,7 +7101,7 @@ dependencies = [ [[package]] name = "pallet-election-provider-multi-phase" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7124,7 +7124,7 @@ dependencies = [ [[package]] name = "pallet-election-provider-support-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7137,7 +7137,7 @@ dependencies = [ [[package]] name = "pallet-elections-phragmen" version = "5.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7359,7 +7359,7 @@ dependencies = [ [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7377,7 +7377,7 @@ dependencies = [ [[package]] name = "pallet-grandpa" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7416,7 +7416,7 @@ dependencies = [ [[package]] name = "pallet-identity" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "enumflags2", "frame-benchmarking", @@ -7454,7 +7454,7 @@ dependencies = [ [[package]] name = "pallet-im-online" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7474,7 +7474,7 @@ dependencies = [ [[package]] name = "pallet-indices" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7491,7 +7491,7 @@ dependencies = [ [[package]] name = "pallet-membership" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7508,7 +7508,7 @@ dependencies = [ [[package]] name = "pallet-mmr" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7525,7 +7525,7 @@ dependencies = [ [[package]] name = "pallet-multisig" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7541,7 +7541,7 @@ dependencies = [ [[package]] name = "pallet-nis" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7557,7 +7557,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools" version = "1.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7574,7 +7574,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools-benchmarking" version = "1.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7594,7 +7594,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools-runtime-api" version = "1.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "pallet-nomination-pools", "parity-scale-codec", @@ -7605,7 +7605,7 @@ dependencies = [ [[package]] name = "pallet-offences" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7622,7 +7622,7 @@ dependencies = [ [[package]] name = "pallet-offences-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7686,7 +7686,7 @@ dependencies = [ [[package]] name = "pallet-preimage" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7703,7 +7703,7 @@ dependencies = [ [[package]] name = "pallet-proxy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7718,7 +7718,7 @@ dependencies = [ [[package]] name = "pallet-ranked-collective" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7736,7 +7736,7 @@ dependencies = [ [[package]] name = "pallet-recovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7751,7 +7751,7 @@ dependencies = [ [[package]] name = "pallet-referenda" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "assert_matches", "frame-benchmarking", @@ -7770,7 +7770,7 @@ dependencies = [ [[package]] name = "pallet-scheduler" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7787,7 +7787,7 @@ dependencies = [ [[package]] name = "pallet-session" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7808,7 +7808,7 @@ dependencies = [ [[package]] name = "pallet-session-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7850,7 +7850,7 @@ dependencies = [ [[package]] name = "pallet-society" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7864,7 +7864,7 @@ dependencies = [ [[package]] name = "pallet-staking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7887,7 +7887,7 @@ dependencies = [ [[package]] name = "pallet-staking-reward-curve" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -7898,7 +7898,7 @@ dependencies = [ [[package]] name = "pallet-staking-reward-fn" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sp-arithmetic", @@ -7907,7 +7907,7 @@ dependencies = [ [[package]] name = "pallet-staking-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -7916,7 +7916,7 @@ dependencies = [ [[package]] name = "pallet-state-trie-migration" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7933,7 +7933,7 @@ dependencies = [ [[package]] name = "pallet-sudo" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7944,6 +7944,38 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-teebag" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "chrono", + "der 0.6.1", + "env_logger 0.9.3", + "frame-benchmarking", + "frame-support", + "frame-system", + "hex", + "hex-literal 0.4.1", + "log", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "ring 0.16.20", + "rustls-webpki", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-externalities", + "sp-io", + "sp-keyring", + "sp-runtime", + "sp-std", + "test-utils", + "x509-cert", +] + [[package]] name = "pallet-teeracle" version = "0.1.0" @@ -7998,7 +8030,7 @@ dependencies = [ [[package]] name = "pallet-timestamp" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8016,7 +8048,7 @@ dependencies = [ [[package]] name = "pallet-tips" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8035,7 +8067,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -8051,7 +8083,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "pallet-transaction-payment-rpc-runtime-api", @@ -8067,7 +8099,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment-rpc-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "pallet-transaction-payment", "parity-scale-codec", @@ -8079,7 +8111,7 @@ dependencies = [ [[package]] name = "pallet-treasury" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8096,7 +8128,7 @@ dependencies = [ [[package]] name = "pallet-utility" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8134,7 +8166,7 @@ dependencies = [ [[package]] name = "pallet-vesting" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8149,7 +8181,7 @@ dependencies = [ [[package]] name = "pallet-whitelist" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -10550,6 +10582,7 @@ dependencies = [ "pallet-session", "pallet-sidechain", "pallet-sudo", + "pallet-teebag", "pallet-teeracle", "pallet-teerex", "pallet-timestamp", @@ -10771,6 +10804,7 @@ dependencies = [ "pallet-group", "pallet-membership", "pallet-multisig", + "pallet-teebag", "pallet-teerex", "pallet-transaction-payment", "pallet-treasury", @@ -10978,7 +11012,7 @@ dependencies = [ [[package]] name = "sc-allocator" version = "4.1.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sp-core", @@ -10989,7 +11023,7 @@ dependencies = [ [[package]] name = "sc-authority-discovery" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11017,7 +11051,7 @@ dependencies = [ [[package]] name = "sc-basic-authorship" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "futures-timer", @@ -11040,7 +11074,7 @@ dependencies = [ [[package]] name = "sc-block-builder" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sc-client-api", @@ -11055,7 +11089,7 @@ dependencies = [ [[package]] name = "sc-chain-spec" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "memmap2", "sc-chain-spec-derive", @@ -11074,7 +11108,7 @@ dependencies = [ [[package]] name = "sc-chain-spec-derive" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -11085,7 +11119,7 @@ dependencies = [ [[package]] name = "sc-cli" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "chrono", @@ -11125,7 +11159,7 @@ dependencies = [ [[package]] name = "sc-client-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "fnv", "futures 0.3.30", @@ -11151,7 +11185,7 @@ dependencies = [ [[package]] name = "sc-client-db" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "kvdb", @@ -11177,7 +11211,7 @@ dependencies = [ [[package]] name = "sc-consensus" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11202,7 +11236,7 @@ dependencies = [ [[package]] name = "sc-consensus-aura" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11231,7 +11265,7 @@ dependencies = [ [[package]] name = "sc-consensus-babe" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "fork-tree", @@ -11267,7 +11301,7 @@ dependencies = [ [[package]] name = "sc-consensus-babe-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11289,7 +11323,7 @@ dependencies = [ [[package]] name = "sc-consensus-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11324,7 +11358,7 @@ dependencies = [ [[package]] name = "sc-consensus-beefy-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11343,7 +11377,7 @@ dependencies = [ [[package]] name = "sc-consensus-epochs" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "fork-tree", "parity-scale-codec", @@ -11356,7 +11390,7 @@ dependencies = [ [[package]] name = "sc-consensus-grandpa" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "array-bytes 4.2.0", @@ -11396,7 +11430,7 @@ dependencies = [ [[package]] name = "sc-consensus-grandpa-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "finality-grandpa", "futures 0.3.30", @@ -11416,7 +11450,7 @@ dependencies = [ [[package]] name = "sc-consensus-slots" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11439,7 +11473,7 @@ dependencies = [ [[package]] name = "sc-executor" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lru 0.8.1", "parity-scale-codec", @@ -11463,7 +11497,7 @@ dependencies = [ [[package]] name = "sc-executor-common" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sc-allocator", "sp-maybe-compressed-blob", @@ -11476,7 +11510,7 @@ dependencies = [ [[package]] name = "sc-executor-wasmi" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sc-allocator", @@ -11489,7 +11523,7 @@ dependencies = [ [[package]] name = "sc-executor-wasmtime" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "cfg-if", @@ -11507,7 +11541,7 @@ dependencies = [ [[package]] name = "sc-informant" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "futures 0.3.30", @@ -11523,7 +11557,7 @@ dependencies = [ [[package]] name = "sc-keystore" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11538,7 +11572,7 @@ dependencies = [ [[package]] name = "sc-network" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-channel", @@ -11583,7 +11617,7 @@ dependencies = [ [[package]] name = "sc-network-bitswap" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "cid", "futures 0.3.30", @@ -11603,7 +11637,7 @@ dependencies = [ [[package]] name = "sc-network-common" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11631,7 +11665,7 @@ dependencies = [ [[package]] name = "sc-network-gossip" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "futures 0.3.30", @@ -11650,7 +11684,7 @@ dependencies = [ [[package]] name = "sc-network-light" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11672,7 +11706,7 @@ dependencies = [ [[package]] name = "sc-network-sync" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11706,7 +11740,7 @@ dependencies = [ [[package]] name = "sc-network-transactions" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11726,7 +11760,7 @@ dependencies = [ [[package]] name = "sc-offchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "bytes", @@ -11757,7 +11791,7 @@ dependencies = [ [[package]] name = "sc-peerset" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "libp2p", @@ -11770,7 +11804,7 @@ dependencies = [ [[package]] name = "sc-proposer-metrics" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "substrate-prometheus-endpoint", @@ -11779,7 +11813,7 @@ dependencies = [ [[package]] name = "sc-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11809,7 +11843,7 @@ dependencies = [ [[package]] name = "sc-rpc-api" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "parity-scale-codec", @@ -11828,7 +11862,7 @@ dependencies = [ [[package]] name = "sc-rpc-server" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "http", "jsonrpsee", @@ -11843,7 +11877,7 @@ dependencies = [ [[package]] name = "sc-rpc-spec-v2" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11869,7 +11903,7 @@ dependencies = [ [[package]] name = "sc-service" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "directories", @@ -11935,7 +11969,7 @@ dependencies = [ [[package]] name = "sc-state-db" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "parity-scale-codec", @@ -11946,7 +11980,7 @@ dependencies = [ [[package]] name = "sc-storage-monitor" version = "0.1.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "clap", "fs4", @@ -11962,7 +11996,7 @@ dependencies = [ [[package]] name = "sc-sync-state-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "parity-scale-codec", @@ -11981,7 +12015,7 @@ dependencies = [ [[package]] name = "sc-sysinfo" version = "6.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "libc", @@ -12000,7 +12034,7 @@ dependencies = [ [[package]] name = "sc-telemetry" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "chrono", "futures 0.3.30", @@ -12019,7 +12053,7 @@ dependencies = [ [[package]] name = "sc-tracing" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "atty", @@ -12050,7 +12084,7 @@ dependencies = [ [[package]] name = "sc-tracing-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -12061,7 +12095,7 @@ dependencies = [ [[package]] name = "sc-transaction-pool" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12088,7 +12122,7 @@ dependencies = [ [[package]] name = "sc-transaction-pool-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12102,7 +12136,7 @@ dependencies = [ [[package]] name = "sc-utils" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-channel", "futures 0.3.30", @@ -12654,7 +12688,7 @@ dependencies = [ [[package]] name = "sp-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -12674,7 +12708,7 @@ dependencies = [ [[package]] name = "sp-api-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "blake2", @@ -12688,7 +12722,7 @@ dependencies = [ [[package]] name = "sp-application-crypto" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12701,7 +12735,7 @@ dependencies = [ [[package]] name = "sp-arithmetic" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "integer-sqrt", "num-traits", @@ -12715,7 +12749,7 @@ dependencies = [ [[package]] name = "sp-authority-discovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12728,7 +12762,7 @@ dependencies = [ [[package]] name = "sp-block-builder" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -12740,7 +12774,7 @@ dependencies = [ [[package]] name = "sp-blockchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "log", @@ -12758,7 +12792,7 @@ dependencies = [ [[package]] name = "sp-consensus" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12773,7 +12807,7 @@ dependencies = [ [[package]] name = "sp-consensus-aura" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "parity-scale-codec", @@ -12791,7 +12825,7 @@ dependencies = [ [[package]] name = "sp-consensus-babe" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "parity-scale-codec", @@ -12812,7 +12846,7 @@ dependencies = [ [[package]] name = "sp-consensus-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lazy_static", "parity-scale-codec", @@ -12831,7 +12865,7 @@ dependencies = [ [[package]] name = "sp-consensus-grandpa" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "finality-grandpa", "log", @@ -12849,7 +12883,7 @@ dependencies = [ [[package]] name = "sp-consensus-slots" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12861,7 +12895,7 @@ dependencies = [ [[package]] name = "sp-core" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "bitflags 1.3.2", @@ -12905,7 +12939,7 @@ dependencies = [ [[package]] name = "sp-core-hashing" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "blake2b_simd", "byteorder", @@ -12919,7 +12953,7 @@ dependencies = [ [[package]] name = "sp-core-hashing-proc-macro" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -12930,7 +12964,7 @@ dependencies = [ [[package]] name = "sp-database" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "kvdb", "parking_lot 0.12.1", @@ -12939,7 +12973,7 @@ dependencies = [ [[package]] name = "sp-debug-derive" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -12949,7 +12983,7 @@ dependencies = [ [[package]] name = "sp-externalities" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "environmental", "parity-scale-codec", @@ -12960,7 +12994,7 @@ dependencies = [ [[package]] name = "sp-inherents" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "impl-trait-for-tuples", @@ -12975,7 +13009,7 @@ dependencies = [ [[package]] name = "sp-io" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bytes", "ed25519", @@ -13001,7 +13035,7 @@ dependencies = [ [[package]] name = "sp-keyring" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lazy_static", "sp-core", @@ -13012,7 +13046,7 @@ dependencies = [ [[package]] name = "sp-keystore" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "parity-scale-codec", @@ -13026,7 +13060,7 @@ dependencies = [ [[package]] name = "sp-maybe-compressed-blob" version = "4.1.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "thiserror", "zstd 0.12.4", @@ -13035,7 +13069,7 @@ dependencies = [ [[package]] name = "sp-metadata-ir" version = "0.1.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -13046,7 +13080,7 @@ dependencies = [ [[package]] name = "sp-mmr-primitives" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ckb-merkle-mountain-range", "log", @@ -13064,7 +13098,7 @@ dependencies = [ [[package]] name = "sp-npos-elections" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13078,7 +13112,7 @@ dependencies = [ [[package]] name = "sp-offchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sp-api", "sp-core", @@ -13088,7 +13122,7 @@ dependencies = [ [[package]] name = "sp-panic-handler" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "backtrace", "lazy_static", @@ -13098,7 +13132,7 @@ dependencies = [ [[package]] name = "sp-rpc" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "rustc-hash", "serde", @@ -13108,7 +13142,7 @@ dependencies = [ [[package]] name = "sp-runtime" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "either", "hash256-std-hasher", @@ -13130,7 +13164,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bytes", "impl-trait-for-tuples", @@ -13148,7 +13182,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface-proc-macro" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "proc-macro-crate", @@ -13160,7 +13194,7 @@ dependencies = [ [[package]] name = "sp-session" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13174,7 +13208,7 @@ dependencies = [ [[package]] name = "sp-staking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13187,7 +13221,7 @@ dependencies = [ [[package]] name = "sp-state-machine" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -13207,12 +13241,12 @@ dependencies = [ [[package]] name = "sp-std" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" [[package]] name = "sp-storage" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "impl-serde", "parity-scale-codec", @@ -13225,7 +13259,7 @@ dependencies = [ [[package]] name = "sp-timestamp" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures-timer", @@ -13240,7 +13274,7 @@ dependencies = [ [[package]] name = "sp-tracing" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-std", @@ -13252,7 +13286,7 @@ dependencies = [ [[package]] name = "sp-transaction-pool" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sp-api", "sp-runtime", @@ -13261,7 +13295,7 @@ dependencies = [ [[package]] name = "sp-transaction-storage-proof" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "log", @@ -13277,7 +13311,7 @@ dependencies = [ [[package]] name = "sp-trie" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "hash-db 0.16.0", @@ -13300,7 +13334,7 @@ dependencies = [ [[package]] name = "sp-version" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "impl-serde", "parity-scale-codec", @@ -13317,7 +13351,7 @@ dependencies = [ [[package]] name = "sp-version-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "proc-macro2", @@ -13328,7 +13362,7 @@ dependencies = [ [[package]] name = "sp-wasm-interface" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "impl-trait-for-tuples", @@ -13342,7 +13376,7 @@ dependencies = [ [[package]] name = "sp-weights" version = "4.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13572,7 +13606,7 @@ dependencies = [ [[package]] name = "substrate-build-script-utils" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "platforms 2.0.0", ] @@ -13601,7 +13635,7 @@ dependencies = [ [[package]] name = "substrate-frame-rpc-system" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-system-rpc-runtime-api", "futures 0.3.30", @@ -13620,7 +13654,7 @@ dependencies = [ [[package]] name = "substrate-prometheus-endpoint" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hyper", "log", @@ -13632,7 +13666,7 @@ dependencies = [ [[package]] name = "substrate-rpc-client" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "jsonrpsee", @@ -13645,7 +13679,7 @@ dependencies = [ [[package]] name = "substrate-state-trie-migration-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "log", @@ -13664,7 +13698,7 @@ dependencies = [ [[package]] name = "substrate-wasm-builder" version = "5.0.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "build-helper", @@ -14349,7 +14383,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "try-runtime-cli" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "clap", diff --git a/Cargo.toml b/Cargo.toml index ba77c155ad..dc4ee6ba27 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,6 +10,7 @@ members = [ 'pallets/identity-management', 'pallets/vc-management', 'pallets/sidechain', + 'pallets/teebag', 'pallets/teeracle', 'pallets/teerex', 'pallets/teerex/sgx-verify', diff --git a/bitacross-worker/enclave-runtime/src/attestation.rs b/bitacross-worker/enclave-runtime/src/attestation.rs index 5b7f7ded3a..7702cd8f0f 100644 --- a/bitacross-worker/enclave-runtime/src/attestation.rs +++ b/bitacross-worker/enclave-runtime/src/attestation.rs @@ -533,8 +533,6 @@ fn get_shielding_pubkey() -> EnclaveResult>> { }) .ok(); - debug!("[Enclave] shielding_pubkey size: {:?}", shielding_pubkey.clone().map(|key| key.len())); - Ok(shielding_pubkey) } diff --git a/node/src/chain_specs/rococo.rs b/node/src/chain_specs/rococo.rs index 0e6e80facf..0e6742ce9d 100644 --- a/node/src/chain_specs/rococo.rs +++ b/node/src/chain_specs/rococo.rs @@ -19,8 +19,8 @@ use cumulus_primitives_core::ParaId; use rococo_parachain_runtime::{ AccountId, AuraId, Balance, BalancesConfig, CouncilMembershipConfig, GenesisConfig, ParachainInfoConfig, ParachainStakingConfig, PolkadotXcmConfig, SessionConfig, SudoConfig, - SystemConfig, TechnicalCommitteeMembershipConfig, TeerexConfig, VCManagementConfig, UNIT, - WASM_BINARY, + SystemConfig, TechnicalCommitteeMembershipConfig, TeebagConfig, TeerexConfig, + VCManagementConfig, UNIT, WASM_BINARY, }; use sc_service::ChainType; use sc_telemetry::TelemetryEndpoints; @@ -245,10 +245,15 @@ fn generate_genesis( admin: Some(root_key.clone()), skip_scheduled_enclave_check, }, - vc_management: VCManagementConfig { admin: Some(root_key) }, + vc_management: VCManagementConfig { admin: Some(root_key.clone()) }, transaction_payment: Default::default(), tokens: Default::default(), ethereum: Default::default(), evm: Default::default(), + teebag: TeebagConfig { + allow_sgx_debug_mode: true, + admin: Some(root_key), + mode: Default::default(), + }, } } diff --git a/pallets/teebag/Cargo.toml b/pallets/teebag/Cargo.toml new file mode 100644 index 0000000000..ec93cb6335 --- /dev/null +++ b/pallets/teebag/Cargo.toml @@ -0,0 +1,78 @@ +[package] +authors = ['Trust Computing GmbH ', 'Integritee AG '] +edition = '2021' +homepage = 'https://litentry.com' +name = 'pallet-teebag' +description = 'Pallet for tee-worker registration and management' +repository = 'https://github.com/litentry/litentry-parachain' +license = 'GPL-3.0' +version = '0.1.0' + +[dependencies] +base64 = { version = "0.13", default-features = false, features = ["alloc"] } +chrono = { version = "0.4", default-features = false, features = ["serde"] } +codec = { version = "3.0.0", default-features = false, features = ["derive"], package = "parity-scale-codec" } +der = { default-features = false, version = "0.6.0" } +hex = { default-features = false, version = "0.4.3", features = ["alloc"] } +log = { version = "0.4", default-features = false } +ring = { version = "0.16.20", default-features = false, features = ["alloc"] } +scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } +serde = { default-features = false, version = "1.0", features = ["derive"] } +serde_json = { version = "1.0", default-features = false, features = ["alloc"] } +webpki = { git = "https://github.com/rustls/webpki", version = "=0.102.0-alpha.3", rev = "da923ed", package = "rustls-webpki", default-features = false, features = ["alloc", "ring"] } +x509-cert = { default-features = false, version = "0.1.0", features = ["alloc"] } + +frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +frame-system = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +pallet-timestamp = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +sp-io = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } + +frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +hex-literal = { version = "0.4.1", optional = true } +pallet-balances = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +test-utils = { path = "../test-utils", default-features = false, optional = true } + +[dev-dependencies] +env_logger = "0.9.0" +externalities = { package = "sp-externalities", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +frame-benchmarking = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +hex-literal = "0.4.1" +pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +test-utils = { path = "../test-utils" } + +[features] +default = ["std"] +std = [ + "base64/std", + "chrono/std", + "der/std", + "hex/std", + "serde/std", + "serde_json/std", + "ring/std", + "x509-cert/std", + "codec/std", + "log/std", + "scale-info/std", + "webpki/std", + "frame-support/std", + "frame-system/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "pallet-timestamp/std", + "pallet-balances?/std", +] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "hex-literal", + "pallet-balances?/runtime-benchmarks", + "test-utils", + "pallet-timestamp/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/teebag/README.md b/pallets/teebag/README.md new file mode 100644 index 0000000000..644ddcf42f --- /dev/null +++ b/pallets/teebag/README.md @@ -0,0 +1,11 @@ +Pallet for Litentry's tee-worker registration and management. + +Currently it expects the following worker type: +- identity worker +- bitacross worker + +It serves as the registry for public information such as MRENCLAVE, worker-endpoint, vc-pubkey etc. + +TBA: MAA / RA information + +This crate is partially based on `teerex` and `sidechain` crate on https://github.com/integritee-network/pallets/commit/e124aebb2d3d05a9a65f209f8e6304c6790f15d5 - for the code part / file that is (mostly) copied from intergritee, the original licence is kept. \ No newline at end of file diff --git a/pallets/teebag/src/lib.rs b/pallets/teebag/src/lib.rs new file mode 100644 index 0000000000..577ee3f9a0 --- /dev/null +++ b/pallets/teebag/src/lib.rs @@ -0,0 +1,828 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(clippy::too_many_arguments)] + +use codec::Decode; +use frame_support::{ + dispatch::{DispatchErrorWithPostInfo, DispatchResult, DispatchResultWithPostInfo}, + ensure, + pallet_prelude::*, + traits::Get, +}; +use frame_system::pallet_prelude::*; +use sp_core::H256; +use sp_runtime::traits::{CheckedSub, SaturatedConversion}; +use sp_std::{prelude::*, str}; + +mod sgx_verify; +use sgx_verify::{ + deserialize_enclave_identity, deserialize_tcb_info, extract_certs, verify_certificate_chain, + verify_dcap_quote, verify_ias_report, SgxReport, +}; + +pub use pallet::*; + +mod types; +pub use types::*; + +mod quoting_enclave; +pub use quoting_enclave::*; + +mod tcb; +pub use tcb::*; + +const MAX_RA_REPORT_LEN: usize = 5244; +const MAX_DCAP_QUOTE_LEN: usize = 5000; +const MAX_URL_LEN: usize = 256; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_timestamp::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + type MomentsPerDay: Get; + /// The origin who can set the admin account + type SetAdminOrigin: EnsureOrigin; + } + + // TODO: maybe add more sidechain lifecycle events + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + ModeSet { + new_mode: OperationalMode, + }, + AdminSet { + new_admin: Option, + }, + MaxEnclaveCountSet { + worker_type: WorkerType, + new_count: u64, + }, + EnclaveAdded { + who: T::AccountId, + worker_type: WorkerType, + url: Vec, + }, + EnclaveRemoved { + who: T::AccountId, + }, + OpaqueTaskPosted { + shard: ShardIdentifier, + }, + ParentchainBlockProcessed { + who: T::AccountId, + block_number: T::BlockNumber, + block_hash: H256, + task_merkle_root: H256, + }, + SidechainBlockFinalized { + who: T::AccountId, + sidechain_block_number: SidechainBlockNumber, + }, + ScheduledEnclaveSet { + worker_type: WorkerType, + sidechain_block_number: SidechainBlockNumber, + mrenclave: MrEnclave, + }, + ScheduledEnclaveRemoved { + worker_type: WorkerType, + sidechain_block_number: SidechainBlockNumber, + }, + } + + #[pallet::error] + pub enum Error { + /// This operation needs the admin permission + RequireAdminOrRoot, + /// Failed to decode enclave signer. + EnclaveSignerDecodeError, + /// Sender does not match attested enclave in report. + SenderIsNotAttestedEnclave, + /// Verifying RA report failed. + RemoteAttestationVerificationFailed, + /// RA report is too old. + RemoteAttestationTooOld, + /// Invalid attestion type, e.g., an `Ignore` type under non-dev mode + InvalidAttestationType, + /// The enclave cannot attest, because its building mode is not allowed. + InvalidSgxMode, + /// The enclave doesn't exist. + EnclaveNotExist, + /// The shard doesn't match the enclave. + WrongMrenclaveForShard, + /// The worker url is too long. + EnclaveUrlTooLong, + /// The raw attestation data is too long. + AttestationTooLong, + /// The worker type is unexpected, because e.g. a non-sidechain worker calls sidechain + /// specific extrinsic + UnexpectedWorkerType, + /// Can not found the desired scheduled enclave. + ScheduledEnclaveNotExist, + /// Enclave not in the scheduled list, therefore unexpected. + EnclaveNotInSchedule, + /// The provided collateral data is invalid + CollateralInvalid, + /// The number of `extra_topics` passed to `publish_hash` exceeds the limit. + TooManyTopics, + /// The length of the `data` passed to `publish_hash` exceeds the limit. + DataTooLong, + /// max_enclave_count overflows + MaxEnclaveCountOverflow, + /// max_enclave_count underflows (than 0 or currently registered enclave count) + MaxEnclaveCountUnderflow, + /// A proposed block is unexpected. + ReceivedUnexpectedSidechainBlock, + /// The value for the next finalization candidate is invalid. + InvalidNextFinalizationCandidateBlockNumber, + } + + #[pallet::storage] + #[pallet::getter(fn admin)] + pub type Admin = StorageValue<_, T::AccountId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn mode)] + pub type Mode = StorageValue<_, OperationalMode, ValueQuery>; + + #[pallet::type_value] + pub fn DefaultMaxEnclaveCount() -> u64 { + 3 + } + + #[pallet::storage] + #[pallet::getter(fn max_enclave_count)] + pub type MaxEnclaveCount = + StorageMap<_, Blake2_128Concat, WorkerType, u64, ValueQuery, DefaultMaxEnclaveCount>; + + #[pallet::storage] + #[pallet::getter(fn enclave_count)] + pub type EnclaveCount = StorageMap<_, Blake2_128Concat, WorkerType, u64, ValueQuery>; + + // registry that holds all registered enclaves, using T::AccountId as the key + // having `worker_type` and `mrenclave` in each `Enclave` instance might seem a bit redundant, + // but it increases flexibility where we **could** allow the same type of worker to have + // different mrenclaves - e.g. when more than one version of an enclave is permitted in TEE-node + // cluster. + // + // It simplifies the lookup a bit too, otherwise we might need several storages. + #[pallet::storage] + #[pallet::getter(fn enclave_registry)] + pub type EnclaveRegistry = + StorageMap<_, Blake2_128Concat, T::AccountId, Enclave, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn allow_sgx_debug_mode)] + pub type AllowSGXDebugMode = StorageValue<_, bool, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn quoting_enclave_registry)] + pub type QuotingEnclaveRegistry = StorageValue<_, QuotingEnclave, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn tcb_info)] + pub type TcbInfo = + StorageMap<_, Blake2_128Concat, Fmspc, TcbInfoOnChain, ValueQuery>; + + // keep track of a list of scheduled/allowed enchalves, mainly used for enclave updates, + // can only be modified by AdminOrigin + // (worker_type, sidechain_block_number) -> expected MrEnclave + // + // about the first time enclave registration: + // prior to `register_enclave` this map needs to be populated with ((worker_type, 0), + // expected-mrenclave), otherwise the registration will fail + // + // For NON-sidechain worker_type, we still use this storage to whitelist mrenclave, in this case + // the `SidechainBlockNumber` is ignored - you could always set it to 0. + // + // Theorectically we could always push the enclave in `register_enclave`, but we want to + // limit the mrenclave that can be registered, as the parachain is supposed to process enclaves + // with specific mrenclaves. + #[pallet::storage] + #[pallet::getter(fn scheduled_enclave)] + pub type ScheduledEnclave = + StorageMap<_, Blake2_128Concat, (WorkerType, SidechainBlockNumber), MrEnclave>; + + #[pallet::storage] + #[pallet::getter(fn worker_for_shard)] + pub type WorkerForShard = + StorageMap<_, Blake2_128Concat, ShardIdentifier, T::AccountId, OptionQuery>; + + #[pallet::storage] + #[pallet::getter(fn latest_sidechain_block_confirmation)] + pub type LatestSidechainBlockConfirmation = + StorageMap<_, Blake2_128Concat, ShardIdentifier, SidechainBlockConfirmation, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn sidechain_block_finalization_candidate)] + pub type SidechainBlockFinalizationCandidate = + StorageMap<_, Blake2_128Concat, ShardIdentifier, SidechainBlockNumber, ValueQuery>; + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub allow_sgx_debug_mode: bool, + pub admin: Option, + pub mode: OperationalMode, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { allow_sgx_debug_mode: false, admin: None, mode: OperationalMode::Production } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + AllowSGXDebugMode::::put(self.allow_sgx_debug_mode); + Mode::::put(self.mode); + if let Some(ref admin) = self.admin { + Admin::::put(admin); + } + } + } + + #[pallet::call] + impl Pallet + where + // Needed for the conversion of `mrenclave` to a `Hash`. + // The condition holds for all known chains. + ::Hash: From<[u8; 32]>, + { + /// Set the admin account + /// + /// Weights should be 2 DB writes: 1 for mode and 1 for event + #[pallet::call_index(0)] + #[pallet::weight((2 * T::DbWeight::get().write, DispatchClass::Normal, Pays::No))] + pub fn set_admin( + origin: OriginFor, + new_admin: T::AccountId, + ) -> DispatchResultWithPostInfo { + T::SetAdminOrigin::ensure_origin(origin)?; + Admin::::put(new_admin.clone()); + Self::deposit_event(Event::AdminSet { new_admin: Some(new_admin) }); + Ok(Pays::No.into()) + } + + /// Set the mode + /// + /// Weights should be 2 DB writes: 1 for mode and 1 for event + #[pallet::call_index(1)] + #[pallet::weight((2 * T::DbWeight::get().write, DispatchClass::Normal, Pays::Yes))] + pub fn set_mode( + origin: OriginFor, + new_mode: OperationalMode, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + Mode::::put(new_mode); + Self::deposit_event(Event::ModeSet { new_mode }); + Ok(().into()) + } + + #[pallet::call_index(2)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn set_max_enclave_count( + origin: OriginFor, + worker_type: WorkerType, + new_count: u64, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + ensure!( + new_count > Self::max_enclave_count(worker_type), + Error::::MaxEnclaveCountUnderflow + ); + + MaxEnclaveCount::::insert(worker_type, new_count); + Self::deposit_event(Event::MaxEnclaveCountSet { worker_type, new_count }); + Ok(Pays::No.into()) + } + + #[pallet::call_index(3)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn force_add_enclave( + origin: OriginFor, + who: T::AccountId, + enclave: Enclave, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + Self::add_enclave(&who, &enclave)?; + Ok(Pays::No.into()) + } + + #[pallet::call_index(4)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn force_remove_enclave( + origin: OriginFor, + who: T::AccountId, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + Self::remove_enclave(&who)?; + Ok(Pays::No.into()) + } + + #[pallet::call_index(5)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn force_remove_enclave_by_mrenclave( + origin: OriginFor, + mrenclave: MrEnclave, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + let accounts = EnclaveRegistry::::iter() + .filter_map( + |(who, enclave)| { + if enclave.mrenclave == mrenclave { + Some(who) + } else { + None + } + }, + ) + .collect::>(); + + for who in accounts.into_iter() { + Self::remove_enclave(&who)?; + } + Ok(Pays::No.into()) + } + + #[pallet::call_index(6)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn force_remove_enclave_by_worker_type( + origin: OriginFor, + worker_type: WorkerType, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + let accounts = EnclaveRegistry::::iter() + .filter_map( + |(who, enclave)| { + if enclave.worker_type == worker_type { + Some(who) + } else { + None + } + }, + ) + .collect::>(); + + for who in accounts.into_iter() { + Self::remove_enclave(&who)?; + } + + Ok(Pays::No.into()) + } + + #[pallet::call_index(7)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn set_scheduled_enclave( + origin: OriginFor, + worker_type: WorkerType, + sidechain_block_number: SidechainBlockNumber, + mrenclave: MrEnclave, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + ScheduledEnclave::::insert((worker_type, sidechain_block_number), mrenclave); + Self::deposit_event(Event::ScheduledEnclaveSet { + worker_type, + sidechain_block_number, + mrenclave, + }); + Ok(().into()) + } + + #[pallet::call_index(8)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn remove_scheduled_enclave( + origin: OriginFor, + worker_type: WorkerType, + sidechain_block_number: SidechainBlockNumber, + ) -> DispatchResultWithPostInfo { + Self::ensure_admin_or_root(origin)?; + ensure!( + ScheduledEnclave::::contains_key((worker_type, sidechain_block_number)), + Error::::ScheduledEnclaveNotExist + ); + ScheduledEnclave::::remove((worker_type, sidechain_block_number)); + Self::deposit_event(Event::ScheduledEnclaveRemoved { + worker_type, + sidechain_block_number, + }); + Ok(().into()) + } + + #[pallet::call_index(9)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::Yes))] + pub fn register_enclave( + origin: OriginFor, + worker_type: WorkerType, + attestation: Vec, + worker_url: Vec, + shielding_pubkey: Option>, + vc_pubkey: Option>, + attestation_type: AttestationType, + ) -> DispatchResultWithPostInfo { + let sender = ensure_signed(origin)?; + + ensure!(worker_url.len() <= MAX_URL_LEN, Error::::EnclaveUrlTooLong); + + let mut enclave = Enclave::new( + worker_type, + worker_url, + shielding_pubkey, + vc_pubkey, + attestation_type, + ); + match attestation_type { + AttestationType::Ignore => { + ensure!( + Self::mode() == OperationalMode::Development, + Error::::InvalidAttestationType + ); + enclave.mrenclave = + ::decode(&mut attestation.as_slice()).unwrap_or_default(); + enclave.last_seen_timestamp = Self::now().saturated_into(); + enclave.sgx_build_mode = SgxBuildMode::default(); + }, + AttestationType::Ias => { + let report = Self::verify_ias(&sender, attestation)?; + enclave.mrenclave = report.mr_enclave; + enclave.last_seen_timestamp = report.timestamp; + enclave.sgx_build_mode = report.build_mode; + }, + AttestationType::Dcap(_) => { + let report = Self::verify_dcap(&sender, attestation)?; + enclave.mrenclave = report.mr_enclave; + enclave.last_seen_timestamp = report.timestamp; + enclave.sgx_build_mode = report.build_mode; + }, + }; + + match Self::mode() { + OperationalMode::Production | OperationalMode::Maintenance => { + if !Self::allow_sgx_debug_mode() && + enclave.sgx_build_mode == SgxBuildMode::Debug + { + return Err(Error::::InvalidSgxMode.into()) + } + ensure!( + ScheduledEnclave::::iter_values().any(|m| m == enclave.mrenclave), + Error::::EnclaveNotInSchedule + ); + }, + OperationalMode::Development => (), + }; + enclave.register_timestamp = Self::now().saturated_into(); + Self::add_enclave(&sender, &enclave)?; + Ok(().into()) + } + + #[pallet::call_index(10)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::Yes))] + pub fn unregister_enclave(origin: OriginFor) -> DispatchResultWithPostInfo { + let sender = ensure_signed(origin)?; + Self::remove_enclave(&sender)?; + Ok(().into()) + } + + #[pallet::call_index(11)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn register_quoting_enclave( + origin: OriginFor, + enclave_identity: Vec, + signature: Vec, + certificate_chain: Vec, + ) -> DispatchResultWithPostInfo { + // Quoting enclaves are registered globally and not for a specific sender + let _ = ensure_signed(origin)?; + let quoting_enclave = + Self::verify_quoting_enclave(enclave_identity, signature, certificate_chain)?; + >::put(quoting_enclave); + Ok(().into()) + } + + #[pallet::call_index(12)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn register_tcb_info( + origin: OriginFor, + tcb_info: Vec, + signature: Vec, + certificate_chain: Vec, + ) -> DispatchResultWithPostInfo { + // TCB info is registered globally and not for a specific sender + let _ = ensure_signed(origin)?; + let (fmspc, on_chain_info) = + Self::verify_tcb_info(tcb_info, signature, certificate_chain)?; + TcbInfo::::insert(fmspc, on_chain_info); + Ok(().into()) + } + + // =============================================================================== + // Following extrinsics are for runtime communication between parachain and worker + // =============================================================================== + + #[pallet::call_index(20)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::Yes))] + pub fn post_opaque_task(origin: OriginFor, request: RsaRequest) -> DispatchResult { + let _ = ensure_signed(origin)?; + Self::deposit_event(Event::OpaqueTaskPosted { shard: request.shard }); + Ok(()) + } + + #[pallet::call_index(21)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn parentchain_block_processed( + origin: OriginFor, + block_hash: H256, + block_number: T::BlockNumber, + task_merkle_root: H256, + ) -> DispatchResultWithPostInfo { + let sender = ensure_signed(origin)?; + let mut enclave = + EnclaveRegistry::::get(&sender).ok_or(Error::::EnclaveNotExist)?; + enclave.last_seen_timestamp = Self::now().saturated_into(); + Self::deposit_event(Event::ParentchainBlockProcessed { + who: sender, + block_number, + block_hash, + task_merkle_root, + }); + Ok(().into()) + } + + #[pallet::call_index(22)] + #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] + pub fn sidechain_block_imported( + origin: OriginFor, + shard: ShardIdentifier, + block_number: u64, + next_finalization_candidate_block_number: u64, + block_header_hash: H256, + ) -> DispatchResultWithPostInfo { + let confirmation = SidechainBlockConfirmation { block_number, block_header_hash }; + + let sender = ensure_signed(origin)?; + let mut sender_enclave = + EnclaveRegistry::::get(&sender).ok_or(Error::::EnclaveNotExist)?; + + ensure!( + sender_enclave.mrenclave.as_ref() == shard.as_ref(), + Error::::WrongMrenclaveForShard + ); + + ensure!(sender_enclave.worker_type.is_sidechain(), Error::::UnexpectedWorkerType,); + + sender_enclave.last_seen_timestamp = Self::now().saturated_into(); + + // Simple logic for now: only accept blocks from first registered enclave. + let primary_enclave = Self::primary_enclave(sender_enclave.worker_type) + .ok_or(Error::::EnclaveNotExist)?; + + if sender_enclave.register_timestamp > primary_enclave.register_timestamp { + log::debug!( + "Ignore block confirmation from registered enclave with timestamp {}", + sender_enclave.register_timestamp + ); + return Ok(().into()) + } + + let block_number = confirmation.block_number; + let finalization_candidate_block_number = + SidechainBlockFinalizationCandidate::::try_get(shard).unwrap_or(1); + + ensure!( + block_number == finalization_candidate_block_number, + Error::::ReceivedUnexpectedSidechainBlock + ); + ensure!( + next_finalization_candidate_block_number > finalization_candidate_block_number, + Error::::InvalidNextFinalizationCandidateBlockNumber + ); + + SidechainBlockFinalizationCandidate::::insert( + shard, + next_finalization_candidate_block_number, + ); + + Self::finalize_block(sender, shard, confirmation); + Ok(().into()) + } + } +} + +impl Pallet { + fn ensure_admin_or_root(origin: OriginFor) -> DispatchResultWithPostInfo { + ensure!( + ensure_root(origin.clone()).is_ok() || Some(ensure_signed(origin)?) == Self::admin(), + Error::::RequireAdminOrRoot + ); + Ok(().into()) + } + + fn increment_count(worker_type: WorkerType) -> Result<(), DispatchErrorWithPostInfo> { + let count = Self::enclave_count(worker_type); + ensure!(count < Self::max_enclave_count(worker_type), Error::::MaxEnclaveCountOverflow); + + EnclaveCount::::insert( + worker_type, + count.checked_add(1u64).ok_or(Error::::MaxEnclaveCountOverflow)?, + ); + + Ok(()) + } + + fn decrement_count(worker_type: WorkerType) -> Result<(), DispatchErrorWithPostInfo> { + let count = Self::enclave_count(worker_type); + EnclaveCount::::insert( + worker_type, + count.checked_sub(1u64).ok_or(Error::::MaxEnclaveCountUnderflow)?, + ); + + Ok(()) + } + + pub fn add_enclave(sender: &T::AccountId, enclave: &Enclave) -> DispatchResultWithPostInfo { + match EnclaveRegistry::::get(sender) { + Some(old_enclave) => { + if old_enclave.worker_type != enclave.worker_type { + // a tricky situation - we are re-registering the enclave with a different + // worker type + Self::decrement_count(old_enclave.worker_type)?; + Self::increment_count(enclave.worker_type)?; + } + // else - do nothing + }, + None => Self::increment_count(enclave.worker_type)?, + }; + EnclaveRegistry::::insert(sender, enclave); + Self::deposit_event(Event::::EnclaveAdded { + who: sender.clone(), + worker_type: enclave.worker_type, + url: enclave.url.clone(), + }); + Ok(().into()) + } + + fn remove_enclave(sender: &T::AccountId) -> DispatchResultWithPostInfo { + let enclave = EnclaveRegistry::::get(sender).ok_or(Error::::EnclaveNotExist)?; + let count = EnclaveCount::::get(enclave.worker_type); + + EnclaveCount::::insert( + enclave.worker_type, + count.checked_sub(1u64).ok_or(Error::::MaxEnclaveCountOverflow)?, + ); + + EnclaveRegistry::::remove(sender); + Self::deposit_event(Event::::EnclaveRemoved { who: sender.clone() }); + Ok(().into()) + } + + pub fn primary_enclave(worker_type: WorkerType) -> Option { + let mut enclaves = EnclaveRegistry::::iter_values() + .filter(|e| e.worker_type == worker_type) + .collect::>(); + enclaves.sort_by(|a, b| Ord::cmp(&a.register_timestamp, &b.register_timestamp)); + enclaves.get(0).cloned() + } + + fn verify_ias( + sender: &T::AccountId, + ra_report: Vec, + ) -> Result { + ensure!(ra_report.len() <= MAX_RA_REPORT_LEN, Error::::AttestationTooLong); + let report = verify_ias_report(&ra_report) + .map_err(|_| Error::::RemoteAttestationVerificationFailed)?; + + let enclave_signer = T::AccountId::decode(&mut &report.pubkey[..]) + .map_err(|_| Error::::EnclaveSignerDecodeError)?; + + ensure!(sender == &enclave_signer, Error::::SenderIsNotAttestedEnclave); + + Self::ensure_timestamp_within_24_hours(report.timestamp)?; + Ok(report) + } + + fn verify_dcap( + sender: &T::AccountId, + dcap_quote: Vec, + ) -> Result { + ensure!(dcap_quote.len() <= MAX_DCAP_QUOTE_LEN, Error::::AttestationTooLong); + let timestamp = Self::now(); + let qe = >::get(); + let (fmspc, tcb_info, report) = + verify_dcap_quote(&dcap_quote, timestamp.saturated_into(), &qe).map_err(|e| { + log::warn!("verify_dcap_quote failed: {:?}", e); + Error::::RemoteAttestationVerificationFailed + })?; + + let tcb_info_on_chain = >::get(fmspc); + ensure!(tcb_info_on_chain.verify_examinee(&tcb_info), "tcb_info is outdated"); + + let enclave_signer = T::AccountId::decode(&mut &report.pubkey[..]) + .map_err(|_| Error::::EnclaveSignerDecodeError)?; + ensure!(sender == &enclave_signer, Error::::SenderIsNotAttestedEnclave); + + Ok(report) + } + + fn verify_quoting_enclave( + enclave_identity: Vec, + signature: Vec, + certificate_chain: Vec, + ) -> Result { + let verification_time: u64 = Self::now().saturated_into(); + let certs = extract_certs(&certificate_chain); + ensure!(certs.len() >= 2, "Certificate chain must have at least two certificates"); + let intermediate_slices: Vec = + certs[1..].iter().map(|c| c.as_slice().into()).collect(); + let leaf_cert_der = webpki::types::CertificateDer::from(certs[0].as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der) + .map_err(|_| "Failed to parse leaf certificate")?; + verify_certificate_chain(&leaf_cert, &intermediate_slices, verification_time)?; + let enclave_identity = + deserialize_enclave_identity(&enclave_identity, &signature, &leaf_cert)?; + + if enclave_identity.is_valid(verification_time.try_into().unwrap()) { + Ok(enclave_identity.to_quoting_enclave()) + } else { + Err(Error::::CollateralInvalid.into()) + } + } + + pub fn verify_tcb_info( + tcb_info: Vec, + signature: Vec, + certificate_chain: Vec, + ) -> Result<(Fmspc, TcbInfoOnChain), DispatchErrorWithPostInfo> { + let verification_time: u64 = Self::now().saturated_into(); + let certs = extract_certs(&certificate_chain); + ensure!(certs.len() >= 2, "Certificate chain must have at least two certificates"); + let intermediate_slices: Vec = + certs[1..].iter().map(|c| c.as_slice().into()).collect(); + let leaf_cert_der = webpki::types::CertificateDer::from(certs[0].as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der) + .map_err(|_| "Failed to parse leaf certificate")?; + verify_certificate_chain(&leaf_cert, &intermediate_slices, verification_time)?; + let tcb_info = deserialize_tcb_info(&tcb_info, &signature, &leaf_cert)?; + if tcb_info.is_valid(verification_time.try_into().unwrap()) { + Ok(tcb_info.to_chain_tcb_info()) + } else { + Err(Error::::CollateralInvalid.into()) + } + } + + fn ensure_timestamp_within_24_hours(report_timestamp: u64) -> DispatchResultWithPostInfo { + let elapsed_time = Self::now() + .checked_sub(&T::Moment::saturated_from(report_timestamp)) + .ok_or("Underflow while calculating elapsed time since report creation")?; + + if elapsed_time < T::MomentsPerDay::get() { + Ok(().into()) + } else { + Err(Error::::RemoteAttestationTooOld.into()) + } + } + + fn finalize_block( + sender: T::AccountId, + shard: ShardIdentifier, + confirmation: SidechainBlockConfirmation, + ) { + LatestSidechainBlockConfirmation::::insert(shard, confirmation); + WorkerForShard::::insert(shard, sender.clone()); + Self::deposit_event(Event::SidechainBlockFinalized { + who: sender, + sidechain_block_number: confirmation.block_number, + }); + } + + fn now() -> T::Moment { + pallet_timestamp::Pallet::::now() + } +} + +#[cfg(test)] +mod mock; +#[cfg(test)] +mod tests; diff --git a/pallets/teebag/src/mock.rs b/pallets/teebag/src/mock.rs new file mode 100644 index 0000000000..bcaac7e06f --- /dev/null +++ b/pallets/teebag/src/mock.rs @@ -0,0 +1,185 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![allow(dead_code, unused_imports, const_item_mutation)] +use crate::{self as pallet_teebag, OperationalMode}; +use frame_support::{ + assert_ok, construct_runtime, + pallet_prelude::GenesisBuild, + parameter_types, + traits::{OnFinalize, OnInitialize}, +}; +use frame_system as system; +use frame_system::EnsureRoot; +use sp_core::H256; +use sp_keyring::AccountKeyring; +use sp_runtime::{ + generic, + traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, +}; + +pub type Signature = sp_runtime::MultiSignature; +pub type AccountId = <::Signer as IdentifyAccount>::AccountId; +pub type Address = sp_runtime::MultiAddress; + +pub type BlockNumber = u32; +pub type Header = generic::Header; +pub type Block = generic::Block; +pub type UncheckedExtrinsic = + generic::UncheckedExtrinsic; + +pub type SignedExtra = ( + frame_system::CheckSpecVersion, + frame_system::CheckTxVersion, + frame_system::CheckGenesis, + frame_system::CheckEra, + frame_system::CheckNonce, + frame_system::CheckWeight, +); + +construct_runtime!( + pub enum Test where + Block = Block, + NodeBlock = Block, + UncheckedExtrinsic = UncheckedExtrinsic, + { + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Teebag: pallet_teebag, + } +); + +parameter_types! { + pub const BlockHashCount: u32 = 250; +} +impl frame_system::Config for Test { + type BaseCallFilter = frame_support::traits::Everything; + type BlockWeights = (); + type BlockLength = (); + type DbWeight = (); + type RuntimeOrigin = RuntimeOrigin; + type Index = u64; + type RuntimeCall = RuntimeCall; + type BlockNumber = BlockNumber; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type Header = Header; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; +} + +pub type Balance = u64; + +parameter_types! { + pub const ExistentialDeposit: u64 = 1; +} + +impl pallet_balances::Config for Test { + type MaxLocks = (); + type Balance = u64; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ExistentialDeposit; + type AccountStore = System; + type WeightInfo = (); + type MaxReserves = (); + type ReserveIdentifier = (); + type HoldIdentifier = (); + type FreezeIdentifier = (); + type MaxHolds = (); + type MaxFreezes = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 6000 / 2; +} + +pub type Moment = u64; + +impl pallet_timestamp::Config for Test { + type Moment = Moment; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +parameter_types! { + pub const MomentsPerDay: u64 = 86_400_000; // [ms/d] +} + +impl pallet_teebag::Config for Test { + type RuntimeEvent = RuntimeEvent; + type MomentsPerDay = MomentsPerDay; + type SetAdminOrigin = EnsureRoot; +} + +// This function basically just builds a genesis storage key/value store according to +// our desired mockup. RA from enclave compiled in debug mode is allowed +pub fn new_test_ext(is_dev_mode: bool) -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::default().build_storage::().unwrap(); + pallet_balances::GenesisConfig:: { + balances: vec![(AccountKeyring::Alice.to_account_id(), 1 << 60)], + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut genesis_config: pallet_teebag::GenesisConfig = crate::GenesisConfig { + allow_sgx_debug_mode: true, + admin: Some(AccountKeyring::Alice.to_account_id()), + mode: OperationalMode::Production, + }; + + if is_dev_mode { + genesis_config.mode = OperationalMode::Development; + } + + GenesisBuild::::assimilate_storage(&genesis_config, &mut t).unwrap(); + + let mut ext: sp_io::TestExternalities = t.into(); + ext.execute_with(|| { + System::set_block_number(1); + }); + ext +} + +/// Helper method for the OnTimestampSet to be called +pub fn set_timestamp(t: u64) { + let _ = pallet_timestamp::Pallet::::set(RuntimeOrigin::none(), t); +} + +/// Run until a particular block. +pub fn run_to_block(n: u32) { + while System::block_number() < n { + if System::block_number() > 1 { + System::on_finalize(System::block_number()); + } + Timestamp::on_finalize(System::block_number()); + System::set_block_number(System::block_number() + 1); + System::on_initialize(System::block_number()); + } +} diff --git a/pallets/teebag/src/quoting_enclave.rs b/pallets/teebag/src/quoting_enclave.rs new file mode 100644 index 0000000000..b05c64623a --- /dev/null +++ b/pallets/teebag/src/quoting_enclave.rs @@ -0,0 +1,78 @@ +/* +Copyright 2021 Integritee AG and Supercomputing Systems AG + +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. + +*/ + +// `QuotingEnclave` primitive part, copied from Integritee + +use crate::{MrSigner, QeTcb, Vec}; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::RuntimeDebug; + +/// This represents all the collateral data that we need to store on chain in order to verify +/// the quoting enclave validity of another enclave that wants to register itself on chain +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct QuotingEnclave { + // Todo: make timestamp: Moment + pub issue_date: u64, // unix epoch in milliseconds + // Todo: make timestamp: Moment + pub next_update: u64, // unix epoch in milliseconds + pub miscselect: [u8; 4], + pub miscselect_mask: [u8; 4], + pub attributes: [u8; 16], + pub attributes_mask: [u8; 16], + pub mrsigner: MrSigner, + pub isvprodid: u16, + /// Contains only the TCB versions that are considered UpToDate + pub tcb: Vec, +} + +impl QuotingEnclave { + #[allow(clippy::too_many_arguments)] + pub fn new( + issue_date: u64, + next_update: u64, + miscselect: [u8; 4], + miscselect_mask: [u8; 4], + attributes: [u8; 16], + attributes_mask: [u8; 16], + mrsigner: MrSigner, + isvprodid: u16, + tcb: Vec, + ) -> Self { + Self { + issue_date, + next_update, + miscselect, + miscselect_mask, + attributes, + attributes_mask, + mrsigner, + isvprodid, + tcb, + } + } + + pub fn attributes_flags_mask_as_u64(&self) -> u64 { + let slice_as_array: [u8; 8] = self.attributes_mask[0..8].try_into().unwrap(); + u64::from_le_bytes(slice_as_array) + } + + pub fn attributes_flags_as_u64(&self) -> u64 { + let slice_as_array: [u8; 8] = self.attributes[0..8].try_into().unwrap(); + u64::from_le_bytes(slice_as_array) + } +} diff --git a/pallets/teebag/src/sgx_verify/AttestationReportSigningCACert.pem b/pallets/teebag/src/sgx_verify/AttestationReportSigningCACert.pem new file mode 100644 index 0000000000..948b4c0cdd --- /dev/null +++ b/pallets/teebag/src/sgx_verify/AttestationReportSigningCACert.pem @@ -0,0 +1,31 @@ +-----BEGIN CERTIFICATE----- +MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV +BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV +BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0 +YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy +MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL +U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD +DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G +CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e +LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh +rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT +L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe +NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ +byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H +afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf +6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM +RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX +MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50 +L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW +BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr +NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq +hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir +IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ +sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi +zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra +Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA +152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB +3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O +DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv +DaVzWh5aiEx+idkSGMnX +-----END CERTIFICATE----- diff --git a/pallets/teebag/src/sgx_verify/collateral.rs b/pallets/teebag/src/sgx_verify/collateral.rs new file mode 100644 index 0000000000..76f9ad5e0c --- /dev/null +++ b/pallets/teebag/src/sgx_verify/collateral.rs @@ -0,0 +1,277 @@ +/* + Copyright 2022 Integritee AG and Supercomputing Systems AG + + 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. + +*/ + +#![cfg_attr(not(feature = "std"), no_std)] +pub extern crate alloc; + +use crate::{Fmspc, MrSigner, Pcesvn, QeTcb, QuotingEnclave, TcbInfoOnChain, TcbVersionStatus}; +use alloc::string::String; +use chrono::prelude::{DateTime, Utc}; +use serde::{de::Error, Deserialize, Deserializer, Serialize, Serializer}; +use sp_std::prelude::*; + +/// The data structures in here are designed such that they can be used to serialize/deserialize +/// the "TCB info" and "enclave identity" collateral data in JSON format provided by intel +/// See https://api.portal.trustedservices.intel.com/documentation for further information and examples + +#[derive(Serialize, Deserialize)] +pub struct Tcb { + isvsvn: u16, +} + +impl Tcb { + pub fn is_valid(&self) -> bool { + // At the time of writing this code everything older than 6 is outdated + // Intel does the same check in their DCAP implementation + self.isvsvn >= 6 + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TcbLevel { + tcb: Tcb, + /// Intel does not verify the tcb_date in their code and their API documentation also does + /// not mention it needs verification. + tcb_date: DateTime, + tcb_status: String, + #[serde(rename = "advisoryIDs")] + #[serde(skip_serializing_if = "Option::is_none")] + advisory_ids: Option>, +} + +impl TcbLevel { + pub fn is_valid(&self) -> bool { + // UpToDate is the only valid status (the other being OutOfDate and Revoked) + // A possible extension would be to also verify that the advisory_ids list is empty, + // but I think this could also lead to all TcbLevels being invalid + self.tcb.is_valid() && self.tcb_status == "UpToDate" + } +} + +#[derive(Serialize, Deserialize)] +struct TcbComponent { + svn: u8, + #[serde(skip_serializing_if = "Option::is_none")] + category: Option, + #[serde(rename = "type")] //type is a keyword so we rename the field + #[serde(skip_serializing_if = "Option::is_none")] + tcb_type: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct TcbFull { + sgxtcbcomponents: [TcbComponent; 16], + pcesvn: Pcesvn, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TcbLevelFull { + tcb: TcbFull, + /// Intel does not verify the tcb_date in their code and their API documentation also does + /// not mention it needs verification. + tcb_date: DateTime, + tcb_status: String, + #[serde(rename = "advisoryIDs")] + #[serde(skip_serializing_if = "Option::is_none")] + advisory_ids: Option>, +} + +impl TcbLevelFull { + pub fn is_valid(&self) -> bool { + // A possible extension would be to also verify that the advisory_ids list is empty, + // but I think this could also lead to all TcbLevels being invalid + self.tcb_status == "UpToDate" || self.tcb_status == "SWHardeningNeeded" + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnclaveIdentity { + id: String, + version: u16, + issue_date: DateTime, + next_update: DateTime, + tcb_evaluation_data_number: u16, + #[serde(deserialize_with = "deserialize_from_hex::<_, 4>")] + #[serde(serialize_with = "serialize_to_hex::<_, 4>")] + miscselect: [u8; 4], + #[serde(deserialize_with = "deserialize_from_hex::<_, 4>")] + #[serde(serialize_with = "serialize_to_hex::<_, 4>")] + miscselect_mask: [u8; 4], + #[serde(deserialize_with = "deserialize_from_hex::<_, 16>")] + #[serde(serialize_with = "serialize_to_hex::<_, 16>")] + attributes: [u8; 16], + #[serde(deserialize_with = "deserialize_from_hex::<_, 16>")] + #[serde(serialize_with = "serialize_to_hex::<_, 16>")] + attributes_mask: [u8; 16], + #[serde(deserialize_with = "deserialize_from_hex::<_, 32>")] + #[serde(serialize_with = "serialize_to_hex::<_, 32>")] + mrsigner: MrSigner, + pub isvprodid: u16, + pub tcb_levels: Vec, +} + +fn serialize_to_hex(x: &[u8; N], s: S) -> Result +where + S: Serializer, +{ + s.serialize_str(&hex::encode(x).to_uppercase()) +} + +fn deserialize_from_hex<'de, D, const N: usize>(deserializer: D) -> Result<[u8; N], D::Error> +where + D: Deserializer<'de>, +{ + let s: &str = Deserialize::deserialize(deserializer)?; + let hex = hex::decode(s).map_err(|_| D::Error::custom("Failed to deserialize hex string"))?; + hex.try_into().map_err(|_| D::Error::custom("Invalid hex length")) +} + +impl EnclaveIdentity { + /// This extracts the necessary information into the struct that we actually store in the chain + pub fn to_quoting_enclave(&self) -> QuotingEnclave { + let mut valid_tcbs: Vec = Vec::new(); + for tcb in &self.tcb_levels { + if tcb.is_valid() { + valid_tcbs.push(QeTcb::new(tcb.tcb.isvsvn)); + } + } + QuotingEnclave::new( + self.issue_date + .timestamp_millis() + .try_into() + .expect("no support for negative unix timestamps"), + self.next_update + .timestamp_millis() + .try_into() + .expect("no support for negative unix timestamps"), + self.miscselect, + self.miscselect_mask, + self.attributes, + self.attributes_mask, + self.mrsigner, + self.isvprodid, + valid_tcbs, + ) + } + + pub fn is_valid(&self, timestamp_millis: i64) -> bool { + self.id == "QE" && + self.version == 2 && + self.issue_date.timestamp_millis() < timestamp_millis && + timestamp_millis < self.next_update.timestamp_millis() + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TcbInfo { + id: String, + version: u8, + issue_date: DateTime, + next_update: DateTime, + #[serde(deserialize_with = "deserialize_from_hex::<_, 6>")] + #[serde(serialize_with = "serialize_to_hex::<_, 6>")] + pub fmspc: Fmspc, + pce_id: String, + tcb_type: u16, + tcb_evaluation_data_number: u16, + tcb_levels: Vec, +} + +impl TcbInfo { + /// This extracts the necessary information into a tuple (`(Key, Value)`) that we actually store + /// in the chain + pub fn to_chain_tcb_info(&self) -> (Fmspc, TcbInfoOnChain) { + let valid_tcbs: Vec = self + .tcb_levels + .iter() + // Only store TCB levels on chain that are currently valid + .filter(|tcb| tcb.is_valid()) + .map(|tcb| { + let mut components = [0u8; 16]; + for (i, t) in tcb.tcb.sgxtcbcomponents.iter().enumerate() { + components[i] = t.svn; + } + TcbVersionStatus::new(components, tcb.tcb.pcesvn) + }) + .collect(); + ( + self.fmspc, + TcbInfoOnChain::new( + self.issue_date + .timestamp_millis() + .try_into() + .expect("no support for negative unix timestamps"), + self.next_update + .timestamp_millis() + .try_into() + .expect("no support for negative unix timestamps"), + valid_tcbs, + ), + ) + } + + pub fn is_valid(&self, timestamp_millis: i64) -> bool { + self.id == "SGX" && + self.version == 3 && + self.issue_date.timestamp_millis() < timestamp_millis && + timestamp_millis < self.next_update.timestamp_millis() + } +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct TcbInfoSigned { + pub tcb_info: TcbInfo, + pub signature: String, +} + +#[derive(Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct EnclaveIdentitySigned { + pub enclave_identity: EnclaveIdentity, + pub signature: String, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn tcb_level_is_valid() { + let t: TcbLevel = serde_json::from_str( + r#"{"tcb":{"isvsvn":6}, "tcbDate":"2021-11-10T00:00:00Z", "tcbStatus":"UpToDate" }"#, + ) + .unwrap(); + assert!(t.is_valid()); + + let t: TcbLevel = serde_json::from_str( + r#"{"tcb":{"isvsvn":6}, "tcbDate":"2021-11-10T00:00:00Z", "tcbStatus":"OutOfDate" }"#, + ) + .unwrap(); + assert!(!t.is_valid()); + + let t: TcbLevel = serde_json::from_str( + r#"{"tcb":{"isvsvn":5}, "tcbDate":"2021-11-10T00:00:00Z", "tcbStatus":"UpToDate" }"#, + ) + .unwrap(); + assert!(!t.is_valid()); + } +} diff --git a/pallets/teebag/src/sgx_verify/ephemeral_key.rs b/pallets/teebag/src/sgx_verify/ephemeral_key.rs new file mode 100644 index 0000000000..29bc989704 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/ephemeral_key.rs @@ -0,0 +1,31 @@ +use super::{utils::length_from_raw_data, CertDer}; +use sp_std::convert::TryFrom; + +pub struct EphemeralKey<'a>(&'a [u8]); + +pub const PRIME256V1_OID: &[u8; 10] = &[0x06, 0x08, 0x2A, 0x86, 0x48, 0xCE, 0x3D, 0x03, 0x01, 0x07]; +impl<'a> TryFrom> for EphemeralKey<'a> { + type Error = &'static str; + + fn try_from(value: CertDer<'a>) -> Result { + let cert_der = value.0; + + let mut offset = cert_der + .windows(PRIME256V1_OID.len()) + .position(|window| window == PRIME256V1_OID) + .ok_or("Certificate does not contain 'PRIME256V1_OID'")?; + + offset += PRIME256V1_OID.len() + 1; // OID length + TAG (0x03) + + // Obtain Public Key length + let len = length_from_raw_data(cert_der, &mut offset)?; + + // Obtain Public Key + offset += 1; + let pub_k = cert_der.get(offset + 2..offset + len).ok_or("Index out of bounds")?; // skip "00 04" + + #[cfg(test)] + println!("verifyRA ephemeral public key: {:x?}", pub_k); + Ok(EphemeralKey(pub_k)) + } +} diff --git a/pallets/teebag/src/sgx_verify/fuzz/Cargo.lock b/pallets/teebag/src/sgx_verify/fuzz/Cargo.lock new file mode 100644 index 0000000000..d9f131e6a9 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/Cargo.lock @@ -0,0 +1,3589 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + +[[package]] +name = "addr2line" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ecd88a8c8378ca913a680cd98f0f13ac67383d35993f86c90a70e3f137816b" +dependencies = [ + "gimli 0.26.2", +] + +[[package]] +name = "addr2line" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a76fd60b23679b7d19bd066031410fb7e458ccc5e958eb5c325888ce4baedc97" +dependencies = [ + "gimli 0.27.2", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +dependencies = [ + "cfg-if", + "getrandom 0.2.8", + "once_cell", + "version_check", +] + +[[package]] +name = "aho-corasick" +version = "0.7.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac" +dependencies = [ + "memchr", +] + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "ansi_term" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" +dependencies = [ + "winapi", +] + +[[package]] +name = "anyhow" +version = "1.0.70" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" + +[[package]] +name = "arbitrary" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0224938f92e7aef515fac2ff2d18bd1115c1394ddf4a092e0c87e8be9499ee5" + +[[package]] +name = "array-bytes" +version = "4.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52f63c5c1316a16a4b35eaac8b76a98248961a533f061684cb2a7cb0eafb6c6" + +[[package]] +name = "arrayref" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b4930d2cb77ce62f89ee5d5289b4ac049559b1c45539271f5ed4fdc7db34545" + +[[package]] +name = "arrayvec" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "async-trait" +version = "0.1.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.4", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "backtrace" +version = "0.3.67" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233d376d6d185f2a3093e58f283f60f880315b6c60075b01f36b3b85154564ca" +dependencies = [ + "addr2line 0.19.0", + "cc", + "cfg-if", + "libc", + "miniz_oxide", + "object 0.30.2", + "rustc-demangle", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base58" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6107fe1be6682a68940da878d9e9f5e90ca5745b3dec9fd1bb393c8777d4f581" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "block-buffer" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" +dependencies = [ + "block-padding", + "byte-tools", + "byteorder", + "generic-array 0.12.4", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "block-padding" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa79dedbb091f449f1f39e53edf88d5dbe95f895dae6135a8d7b881fb5af73f5" +dependencies = [ + "byte-tools", +] + +[[package]] +name = "bounded-collections" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a071c348a5ef6da1d3a87166b408170b46002382b1dda83992b5c2208cefb370" +dependencies = [ + "log", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "bumpalo" +version = "3.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byte-tools" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3b5ca7a04898ad4bcd41c90c5285445ff5b791899bb1b0abdd2a2aa791211d7" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" + +[[package]] +name = "cc" +version = "1.0.78" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a20104e2335ce8a659d6dd92a51a767a0c062599c73b343fd152cb401e828c3d" +dependencies = [ + "jobserver", +] + +[[package]] +name = "cfg-expr" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0aacacf4d96c24b2ad6eb8ee6df040e4f27b0d0b39a5710c30091baa830485db" +dependencies = [ + "smallvec", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "chrono" +version = "0.4.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +dependencies = [ + "iana-time-zone", + "num-integer", + "num-traits", + "serde", + "winapi", +] + +[[package]] +name = "codespan-reporting" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3538270d33cc669650c4b093848450d380def10c331d38c768e34cac80576e6e" +dependencies = [ + "termcolor", + "unicode-width", +] + +[[package]] +name = "const-oid" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520fbf3c07483f94e3e3ca9d0cfd913d7718ef2483d2cfd91c0d9e91474ab913" + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + +[[package]] +name = "cpp_demangle" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eeaa953eaad386a53111e47172c2fedba671e5684c8dd601a5f474f4f118710f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "cpufeatures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d997bd5e24a5928dd43e46dc529867e207907fe0b239c3477d924f7f2ca320" +dependencies = [ + "libc", +] + +[[package]] +name = "cranelift-entity" +version = "0.93.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f42ea692c7b450ad18b8c9889661505d51c09ec4380cf1c2d278dbb2da22cae1" +dependencies = [ + "serde", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array 0.14.6", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array 0.14.6", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.6", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array 0.14.6", + "subtle", +] + +[[package]] +name = "curve25519-dalek" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b85542f99a2dfa2a1b8e192662741c9859a846b296bef1c92ef9b58b5a216" +dependencies = [ + "byteorder", + "digest 0.8.1", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "curve25519-dalek" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b9fdf9972b2bd6af2d913799d9ebc165ea4d2e65878e329d9c6b372c4491b61" +dependencies = [ + "byteorder", + "digest 0.9.0", + "rand_core 0.5.1", + "subtle", + "zeroize", +] + +[[package]] +name = "cxx" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c00419335c41018365ddf7e4d5f1c12ee3659ddcf3e01974650ba1de73d038" +dependencies = [ + "cc", + "cxxbridge-flags", + "cxxbridge-macro", + "link-cplusplus", +] + +[[package]] +name = "cxx-build" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb8307ad413a98fff033c8545ecf133e3257747b3bae935e7602aab8aa92d4ca" +dependencies = [ + "cc", + "codespan-reporting", + "once_cell", + "proc-macro2", + "quote", + "scratch", + "syn 2.0.4", +] + +[[package]] +name = "cxxbridge-flags" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edc52e2eb08915cb12596d29d55f0b5384f00d697a646dbd269b6ecb0fbd9d31" + +[[package]] +name = "cxxbridge-macro" +version = "1.0.93" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "631569015d0d8d54e6c241733f944042623ab6df7bc3be7466874b05fcdb1c5f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.4", +] + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "der_derive", + "flagset", + "zeroize", +] + +[[package]] +name = "der_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef71ddb5b3a1f53dee24817c8f70dfa1cb29e804c18d88c228d4bc9c86ee3b9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive-syn-parse" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e79116f119dd1dba1abf1f3405f03b9b0e79a27a3883864bfebded8a3dc768cd" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "digest" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5" +dependencies = [ + "generic-array 0.12.4", +] + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array 0.14.6", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.4", + "crypto-common", + "subtle", +] + +[[package]] +name = "downcast-rs" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ea835d29036a4087793836fa931b08837ad5e957da9e23886b29586fb9b6650" + +[[package]] +name = "dyn-clonable" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e9232f0e607a262ceb9bd5141a3dfb3e4db6994b31989bbfd845878cba59fd4" +dependencies = [ + "dyn-clonable-impl", + "dyn-clone", +] + +[[package]] +name = "dyn-clonable-impl" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "558e40ea573c374cf53507fd240b7ee2f5477df7cfebdb97323ec61c719399c5" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "dyn-clone" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68b0cf012f1230e43cd00ebb729c6bb58707ecfa8ad08b52ef3a4ccd2697fc30" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "ed25519" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369" +dependencies = [ + "signature", +] + +[[package]] +name = "ed25519-dalek" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c762bae6dcaf24c4c84667b8579785430908723d5c889f469d76a41d59cc7a9d" +dependencies = [ + "curve25519-dalek 3.2.0", + "ed25519", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "ed25519-zebra" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" +dependencies = [ + "curve25519-dalek 3.2.0", + "hashbrown 0.12.3", + "hex", + "rand_core 0.6.4", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "either" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e5c1c8368803113bf0c9584fc495a58b86dc8a29edbf8fe877d21d9507e797" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint", + "der", + "digest 0.10.6", + "ff", + "generic-array 0.14.6", + "group", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "environmental" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48c92028aaa870e83d51c64e5d4e0b6981b360c522198c23959f219a4e1b15b" + +[[package]] +name = "errno" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" +dependencies = [ + "libc", + "windows-sys 0.48.0", +] + +[[package]] +name = "fake-simd" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e88a8acf291dafb59c2d96e8f59828f3838bb1a70398823ade51a84de6a6deed" + +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rand 0.8.5", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "flagset" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda653ca797810c02f7ca4b804b40b8b95ae046eb989d356bce17919a8c25499" + +[[package]] +name = "form_urlencoded" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "frame-metadata" +version = "15.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df6bb8542ef006ef0de09a5c4420787d79823c0ed7924225822362fd2bf2ff2d" +dependencies = [ + "cfg-if", + "parity-scale-codec", + "scale-info", + "serde", +] + +[[package]] +name = "frame-support" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "bitflags", + "frame-metadata", + "frame-support-procedural", + "impl-trait-for-tuples", + "k256", + "log", + "once_cell", + "parity-scale-codec", + "paste", + "scale-info", + "serde", + "smallvec", + "sp-api", + "sp-arithmetic", + "sp-core", + "sp-core-hashing-proc-macro", + "sp-inherents", + "sp-io", + "sp-runtime", + "sp-staking", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-weights", + "tt-call", +] + +[[package]] +name = "frame-support-procedural" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "Inflector", + "cfg-expr", + "derive-syn-parse", + "frame-support-procedural-tools", + "itertools", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "frame-support-procedural-tools" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "frame-support-procedural-tools-derive", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "frame-support-procedural-tools-derive" +version = "3.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" + +[[package]] +name = "futures-executor" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", + "num_cpus", +] + +[[package]] +name = "futures-io" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" + +[[package]] +name = "futures-macro" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "futures-sink" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" + +[[package]] +name = "futures-task" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" + +[[package]] +name = "futures-util" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c05aeb6a22b8f62540c194aac980f2115af067bfe15a0734d7277a768d396b31" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.0+wasi-snapshot-preview1", +] + +[[package]] +name = "gimli" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22030e2c5a68ec659fde1e949a745124b48e6fa8b045b7ed5bd1fe4ccc5c4e5d" +dependencies = [ + "fallible-iterator", + "stable_deref_trait", +] + +[[package]] +name = "gimli" +version = "0.27.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0a93d233ebf96623465aad4046a8d3aa4da22d4f4beba5388838c8a434bbb4" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.6", +] + +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" +dependencies = [ + "ahash 0.8.3", +] + +[[package]] +name = "hermit-abi" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" +dependencies = [ + "libc", +] + +[[package]] +name = "hermit-abi" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fed44880c466736ef9a5c5b5facefb5ed0785676d0c02d612db14e54f0d84286" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.6", + "hmac 0.8.1", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c17cc76786e99f8d2f055c11159e7f0091c42474dcc3189fbab96072e873e6d" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" +dependencies = [ + "cxx", + "cxx-build", +] + +[[package]] +name = "idna" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-serde" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc88fc67028ae3db0c853baa36269d398d5f45b6982f95549ff5def78c935cd" +dependencies = [ + "serde", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indexmap" +version = "1.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + +[[package]] +name = "integer-sqrt" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "276ec31bcb4a9ee45f58bec6f9ec700ae4cf4f4f8f2fa7e06cb406bd5ffdd770" +dependencies = [ + "num-traits", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi 0.3.1", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" + +[[package]] +name = "jobserver" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936cfd212a0155903bcbc060e316fb6cc7cbf2e1907329391ebadc1fe0ce77c2" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "445dde2150c55e483f3d8416706b97ec8e8237c307e5b7b4b8dd15e6af2a0730" +dependencies = [ + "wasm-bindgen", +] + +[[package]] +name = "k256" +version = "0.11.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72c1e0b51e7ec0a97369623508396067a486bd0cbed95a2659a4b863d28cfc8b" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "sha2 0.10.6", +] + +[[package]] +name = "keccak" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3afef3b6eff9ce9d8ff9b3601125eec7f0c8cbac7abd14f355d053fa56c98768" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" + +[[package]] +name = "libc" +version = "0.2.140" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99227334921fae1a979cf0bfdfcc6b3e5ce376ef57e16fb6fb3ea2ed6095f80c" + +[[package]] +name = "libfuzzer-sys" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8fff891139ee62800da71b7fd5b508d570b9ad95e614a53c6f453ca08366038" +dependencies = [ + "arbitrary", + "cc", + "once_cell", +] + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "link-cplusplus" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecd207c9c713c34f95a097a5b029ac2ce6010530c7b49d7fea24d977dede04f5" +dependencies = [ + "cc", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + +[[package]] +name = "matchers" +version = "0.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f099785f7595cc4b4553a174ce30dd7589ef93391ff414dbb67f62392b9e0ce1" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "memchr" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" + +[[package]] +name = "memfd" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b20a59d985586e4a5aef64564ac77299f8586d8be6cf9106a5a40207e8908efb" +dependencies = [ + "rustix", +] + +[[package]] +name = "memoffset" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" +dependencies = [ + "autocfg", +] + +[[package]] +name = "memory-db" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e0c7cba9ce19ac7ffd2053ac9f49843bbd3f4318feedfd74e85c19d5fb0ba66" +dependencies = [ + "hash-db", + "hashbrown 0.12.3", +] + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "merlin" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e261cf0f8b3c42ded9f7d2bb59dea03aa52bc8a1cbc7482f9fc3fd1229d3b42" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.5.1", + "zeroize", +] + +[[package]] +name = "miniz_oxide" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b275950c28b37e794e8c55d88aeb5e139d0ce23fdbbeda68f8d7174abdf9e8fa" +dependencies = [ + "adler", +] + +[[package]] +name = "nohash-hasher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451" + +[[package]] +name = "num-bigint" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f93ab6289c7b344a8a9f60f88d80aa20032336fe78da341afc91c8a2341fc75f" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-format" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a652d9771a63711fd3c3deb670acfbe5c30a4072e664d7a3bf5a9e1056ac72c3" +dependencies = [ + "arrayvec 0.7.2", + "itoa", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b" +dependencies = [ + "hermit-abi 0.2.6", + "libc", +] + +[[package]] +name = "object" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21158b2c33aa6d4561f1c0a6ea283ca92bc54802a93b263e910746d679a7eb53" +dependencies = [ + "crc32fast", + "hashbrown 0.12.3", + "indexmap", + "memchr", +] + +[[package]] +name = "object" +version = "0.30.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b8c786513eb403643f2a88c244c2aaa270ef2153f55094587d0c48a3cf22a83" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "opaque-debug" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "parity-scale-codec" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +dependencies = [ + "arrayvec 0.7.2", + "bitvec", + "byte-slice-cast", + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec-derive", + "serde", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parity-wasm" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1ad0aff30c1da14b1254fcb2af73e1fa9a28670e584a626f53a369d0e157304" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9069cbb9f99e3a5083476ccb29ceb1de18b9118cafa53e90c9551235de2b9521" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.45.0", +] + +[[package]] +name = "paste" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" + +[[package]] +name = "pbkdf2" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d95f5254224e617595d2cc3cc73ff0a5eaf2637519e25f03388154e9378b6ffa" +dependencies = [ + "crypto-mac 0.11.1", +] + +[[package]] +name = "pbkdf2" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "percent-encoding" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" + +[[package]] +name = "pin-project-lite" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-serde", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro2" +version = "1.0.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d0e1ae9e836cc3beddd63db0df682593d7e2d3d891ae8c9083d2113e1744224" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quote" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.8", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags", +] + +[[package]] +name = "ref-cast" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f43faa91b1c8b36841ee70e97188a869d37ae21759da6846d4be66de5bf7b12c" +dependencies = [ + "ref-cast-impl", +] + +[[package]] +name = "ref-cast-impl" +version = "1.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d2275aab483050ab2a7364c1a46604865ee7d6906684e08db0f090acf74f9e7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.4", +] + +[[package]] +name = "regex" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cce168fea28d3e05f158bda4576cf0c844d5045bc2cc3620fa0292ed5bb5814c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint", + "hmac 0.12.1", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.16.20" +source = "git+https://github.com/betrusted-io/ring-xous?branch=0.16.20-cleanup#4296c2e7904898766cf7d8d589759a129794783b" +dependencies = [ + "cc", + "libc", + "log", + "once_cell", + "rkyv", + "spin", + "untrusted 0.7.1", + "winapi", + "xous", + "xous-api-names", + "xous-ipc", +] + +[[package]] +name = "rkyv" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70de01b38fe7baba4ecdd33b777096d2b326993d8ea99bc5b6ede691883d3010" +dependencies = [ + "memoffset", + "ptr_meta", + "rkyv_derive", +] + +[[package]] +name = "rkyv_derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95a169f6bc5a81033e86ed39d0f4150e2608160b73d2b93c6e8e6a3efa873f14" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ef03e0a2b150c7a90d01faf6254c9c48a41e95fb2a8c2ac1c6f0d2b9aefc342" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustix" +version = "0.36.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6da3636faa25820d8648e0e31c5d519bbb01f72fdf57131f0f5f7da5fed36eab" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustls-pki-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47003264dea418db67060fa420ad16d0d2f8f0a0360d825c00e177ac52cb5d8" + +[[package]] +name = "rustls-webpki" +version = "0.102.0-alpha.3" +source = "git+https://github.com/rustls/webpki?rev=da923ed#da923edaab56f599971e58773617fb574cd019dc" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted 0.9.0", +] + +[[package]] +name = "ryu" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041" + +[[package]] +name = "scale-info" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7d66a1128282b7ef025a8ead62a4a9fcf017382ec53b8ffbf4d7bf77bd3c60" +dependencies = [ + "bitvec", + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", + "serde", +] + +[[package]] +name = "scale-info-derive" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abf2c68b89cafb3b8d918dd07b42be0da66ff202cf1155c5739a4e0c1ea0dc19" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "schnellru" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "772575a524feeb803e5b0fcbc6dd9f367e579488197c94c6e4023aad2305774d" +dependencies = [ + "ahash 0.8.3", + "cfg-if", + "hashbrown 0.13.2", +] + +[[package]] +name = "schnorrkel" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "021b403afe70d81eea68f6ea12f6b3c9588e5d536a94c3bf80f15e7faa267862" +dependencies = [ + "arrayref", + "arrayvec 0.5.2", + "curve25519-dalek 2.1.3", + "getrandom 0.1.16", + "merlin", + "rand 0.7.3", + "rand_core 0.5.1", + "sha2 0.8.2", + "subtle", + "zeroize", +] + +[[package]] +name = "scopeguard" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" + +[[package]] +name = "scratch" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1" + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array 0.14.6", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "secp256k1" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9512ffd81e3a3503ed401f79c33168b9148c75038956039166cd750eaa037c3" +dependencies = [ + "secp256k1-sys", +] + +[[package]] +name = "secp256k1-sys" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83080e2c2fc1006e625be82e5d1eb6a43b7fd9578b617fcc55814daf286bba4b" +dependencies = [ + "cc", +] + +[[package]] +name = "secrecy" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bd1c54ea06cfd2f6b63219704de0b9b4f72dcc2b8fdef820be6cd799780e91e" +dependencies = [ + "zeroize", +] + +[[package]] +name = "serde" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb2f3770c8bce3bcda7e149193a069a0f4365bda1fa5cd88e03bca26afc1216c" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.160" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "291a097c63d8497e00160b166a967a4a79c64f3facdd01cbd7502231688d77df" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.4", +] + +[[package]] +name = "serde_json" +version = "1.0.94" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sgx-verify" +version = "0.1.4" +dependencies = [ + "base64", + "chrono", + "der", + "frame-support", + "hex", + "parity-scale-codec", + "ring", + "rustls-webpki", + "scale-info", + "serde", + "serde_json", + "sp-core", + "sp-std", + "teerex-primitives", + "x509-cert", +] + +[[package]] +name = "sgx-verify-fuzz" +version = "0.0.0" +dependencies = [ + "base64", + "libfuzzer-sys", + "parity-scale-codec", + "rustls-webpki", + "serde_json", + "sgx-verify", +] + +[[package]] +name = "sha2" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a256f46ea78a0c0d9ff00077504903ac881a1dafdc20da66545699e7776b3e69" +dependencies = [ + "block-buffer 0.7.3", + "digest 0.8.1", + "fake-simd", + "opaque-debug 0.2.3", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug 0.3.0", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdf0c33fae925bdc080598b84bc15c55e7b9a4a43b3c704da051f977469691c9" +dependencies = [ + "digest 0.10.6", + "keccak", +] + +[[package]] +name = "sharded-slab" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "900fba806f70c630b0a382d0d825e17a0f19fcd059a2ade1ff237bcddf446b31" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest 0.10.6", + "rand_core 0.6.4", +] + +[[package]] +name = "slab" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6528351c9bc8ab22353f9d776db39a20288e8d6c37ef8cfe3317cf875eecfc2d" +dependencies = [ + "autocfg", +] + +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + +[[package]] +name = "sp-api" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "sp-api-proc-macro", + "sp-core", + "sp-runtime", + "sp-state-machine", + "sp-std", + "sp-trie", + "sp-version", + "thiserror", +] + +[[package]] +name = "sp-api-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "blake2", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sp-application-crypto" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-std", +] + +[[package]] +name = "sp-arithmetic" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "integer-sqrt", + "num-traits", + "parity-scale-codec", + "scale-info", + "serde", + "sp-std", + "static_assertions", +] + +[[package]] +name = "sp-core" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "array-bytes", + "base58", + "bitflags", + "blake2", + "bounded-collections", + "dyn-clonable", + "ed25519-zebra", + "futures", + "hash-db", + "hash256-std-hasher", + "impl-serde", + "lazy_static", + "libsecp256k1", + "log", + "merlin", + "parity-scale-codec", + "parking_lot", + "primitive-types", + "rand 0.8.5", + "regex", + "scale-info", + "schnorrkel", + "secp256k1", + "secrecy", + "serde", + "sp-core-hashing", + "sp-debug-derive", + "sp-externalities", + "sp-runtime-interface", + "sp-std", + "sp-storage", + "ss58-registry", + "substrate-bip39", + "thiserror", + "tiny-bip39", + "zeroize", +] + +[[package]] +name = "sp-core-hashing" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "blake2", + "byteorder", + "digest 0.10.6", + "sha2 0.10.6", + "sha3", + "sp-std", + "twox-hash", +] + +[[package]] +name = "sp-core-hashing-proc-macro" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "proc-macro2", + "quote", + "sp-core-hashing", + "syn 1.0.109", +] + +[[package]] +name = "sp-debug-derive" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sp-externalities" +version = "0.13.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "environmental", + "parity-scale-codec", + "sp-std", + "sp-storage", +] + +[[package]] +name = "sp-inherents" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "async-trait", + "impl-trait-for-tuples", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", + "thiserror", +] + +[[package]] +name = "sp-io" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "bytes", + "ed25519", + "ed25519-dalek", + "futures", + "libsecp256k1", + "log", + "parity-scale-codec", + "secp256k1", + "sp-core", + "sp-externalities", + "sp-keystore", + "sp-runtime-interface", + "sp-state-machine", + "sp-std", + "sp-tracing", + "sp-trie", + "tracing", + "tracing-core", +] + +[[package]] +name = "sp-keystore" +version = "0.13.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "async-trait", + "futures", + "merlin", + "parity-scale-codec", + "parking_lot", + "schnorrkel", + "sp-core", + "sp-externalities", + "thiserror", +] + +[[package]] +name = "sp-panic-handler" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "backtrace", + "lazy_static", + "regex", +] + +[[package]] +name = "sp-runtime" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "either", + "hash256-std-hasher", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "paste", + "rand 0.8.5", + "scale-info", + "serde", + "sp-application-crypto", + "sp-arithmetic", + "sp-core", + "sp-io", + "sp-std", + "sp-weights", +] + +[[package]] +name = "sp-runtime-interface" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "bytes", + "impl-trait-for-tuples", + "parity-scale-codec", + "primitive-types", + "sp-externalities", + "sp-runtime-interface-proc-macro", + "sp-std", + "sp-storage", + "sp-tracing", + "sp-wasm-interface", + "static_assertions", +] + +[[package]] +name = "sp-runtime-interface-proc-macro" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "Inflector", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sp-staking" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std", +] + +[[package]] +name = "sp-state-machine" +version = "0.13.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "hash-db", + "log", + "parity-scale-codec", + "parking_lot", + "rand 0.8.5", + "smallvec", + "sp-core", + "sp-externalities", + "sp-panic-handler", + "sp-std", + "sp-trie", + "thiserror", + "tracing", +] + +[[package]] +name = "sp-std" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" + +[[package]] +name = "sp-storage" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "ref-cast", + "serde", + "sp-debug-derive", + "sp-std", +] + +[[package]] +name = "sp-tracing" +version = "6.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "parity-scale-codec", + "sp-std", + "tracing", + "tracing-core", + "tracing-subscriber", +] + +[[package]] +name = "sp-trie" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "ahash 0.8.3", + "hash-db", + "hashbrown 0.12.3", + "lazy_static", + "memory-db", + "nohash-hasher", + "parity-scale-codec", + "parking_lot", + "scale-info", + "schnellru", + "sp-core", + "sp-std", + "thiserror", + "tracing", + "trie-db", + "trie-root", +] + +[[package]] +name = "sp-version" +version = "5.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "impl-serde", + "parity-scale-codec", + "parity-wasm", + "scale-info", + "serde", + "sp-core-hashing-proc-macro", + "sp-runtime", + "sp-std", + "sp-version-proc-macro", + "thiserror", +] + +[[package]] +name = "sp-version-proc-macro" +version = "4.0.0-dev" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "parity-scale-codec", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sp-wasm-interface" +version = "7.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "anyhow", + "impl-trait-for-tuples", + "log", + "parity-scale-codec", + "sp-std", + "wasmi", + "wasmtime", +] + +[[package]] +name = "sp-weights" +version = "4.0.0" +source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#1837f423b494254e1d27834b1c9da34b2c0c2375" +dependencies = [ + "parity-scale-codec", + "scale-info", + "serde", + "smallvec", + "sp-arithmetic", + "sp-core", + "sp-debug-derive", + "sp-std", +] + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "ss58-registry" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecf0bd63593ef78eca595a7fc25e9a443ca46fe69fd472f8f09f5245cdcd769d" +dependencies = [ + "Inflector", + "num-format", + "proc-macro2", + "quote", + "serde", + "serde_json", + "unicode-xid", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "substrate-bip39" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49eee6965196b32f882dd2ee85a92b1dbead41b04e53907f269de3b0dc04733c" +dependencies = [ + "hmac 0.11.0", + "pbkdf2 0.8.0", + "schnorrkel", + "sha2 0.9.9", + "zeroize", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c622ae390c9302e214c31013517c2061ecb2699935882c60a9b37f82f8625ae" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "synstructure" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f36bdaa60a83aca3921b5259d5400cbf5e90fc51931376a9bd4a0eb79aa7210f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "unicode-xid", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "target-lexicon" +version = "0.12.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae9980cab1db3fceee2f6c6f643d5d8de2997c58ee8d25fb0cc8a9e9e7348e5" + +[[package]] +name = "teerex-primitives" +version = "0.1.0" +dependencies = [ + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", +] + +[[package]] +name = "termcolor" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "thiserror" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.4", +] + +[[package]] +name = "thread_local" +version = "1.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" +dependencies = [ + "cfg-if", + "once_cell", +] + +[[package]] +name = "tiny-bip39" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" +dependencies = [ + "anyhow", + "hmac 0.12.1", + "once_cell", + "pbkdf2 0.11.0", + "rand 0.8.5", + "rustc-hash", + "sha2 0.10.6", + "thiserror", + "unicode-normalization", + "wasm-bindgen", + "zeroize", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc18466501acd8ac6a3f615dd29a3438f8ca6bb3b19537138b3106e575621274" +dependencies = [ + "indexmap", + "toml_datetime", + "winnow", +] + +[[package]] +name = "tracing" +version = "0.1.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ce8c33a8d48bd45d624a6e523445fd21ec13d3653cd51f681abf67418f54eb8" +dependencies = [ + "cfg-if", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4017f8f45139870ca7e672686113917c71c7a6e02d4924eda67186083c03081a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tracing-core" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24eb03ba0eab1fd845050058ce5e616558e8f8d8fca633e6b163fe25c797213a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ddad33d2d10b1ed7eb9d1f518a5674713876e97e5bb9b7345a7984fbb4f922" +dependencies = [ + "lazy_static", + "log", + "tracing-core", +] + +[[package]] +name = "tracing-serde" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6b213177105856957181934e4920de57730fc69bf42c37ee5bb664d406d9e1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.2.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e0d2eaa99c3c2e41547cfa109e910a68ea03823cccad4a0525dcbc9b01e8c71" +dependencies = [ + "ansi_term", + "chrono", + "lazy_static", + "matchers", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "trie-db" +version = "0.25.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3390c0409daaa6027d6681393316f4ccd3ff82e1590a1e4725014e3ae2bf1920" +dependencies = [ + "hash-db", + "hashbrown 0.13.2", + "log", + "rustc-hex", + "smallvec", +] + +[[package]] +name = "trie-root" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a36c5ca3911ed3c9a5416ee6c679042064b93fc637ded67e25f92e68d783891" +dependencies = [ + "hash-db", +] + +[[package]] +name = "tt-call" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4f195fd851901624eee5a58c4bb2b4f06399148fcd0ed336e6f1cb60a9881df" + +[[package]] +name = "twox-hash" +version = "1.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fee6b57c6a41524a810daee9286c02d7752c4253064d0b05472833a438f675" +dependencies = [ + "cfg-if", + "digest 0.10.6", + "rand 0.8.5", + "static_assertions", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unicode-bidi" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460" + +[[package]] +name = "unicode-ident" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-width" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" + +[[package]] +name = "unicode-xid" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f962df74c8c05a667b5ee8bcf162993134c104e96440b663c8daa176dc772d8c" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "wasm-bindgen" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31f8dcbc21f30d9b8f2ea926ecb58f6b91192c17e9d33594b3df58b2007ca53b" +dependencies = [ + "cfg-if", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ce90fd5bcc06af55a641a86428ee4229e44e07033963a2290a8e241607ccb9" +dependencies = [ + "bumpalo", + "log", + "once_cell", + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c21f77c0bedc37fd5dc21f897894a5ca01e7bb159884559461862ae90c0b4c5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aff81306fcac3c7515ad4e177f521b5c9a15f2b08f4e32d823066102f35a5f6" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0046fef7e28c3804e5e38bfa31ea2a0f73905319b677e57ebe37e49358989b5d" + +[[package]] +name = "wasmi" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06c326c93fbf86419608361a2c925a31754cf109da1b8b55737070b4d6669422" +dependencies = [ + "parity-wasm", + "wasmi-validation", + "wasmi_core", +] + +[[package]] +name = "wasmi-validation" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ff416ad1ff0c42e5a926ed5d5fab74c0f098749aa0ad8b2a34b982ce0e867b" +dependencies = [ + "parity-wasm", +] + +[[package]] +name = "wasmi_core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d20cb3c59b788653d99541c646c561c9dd26506f25c0cebfe810659c54c6d7" +dependencies = [ + "downcast-rs", + "libm", + "memory_units", + "num-rational", + "num-traits", +] + +[[package]] +name = "wasmparser" +version = "0.100.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64b20236ab624147dfbb62cf12a19aaf66af0e41b8398838b66e997d07d269d4" +dependencies = [ + "indexmap", + "url", +] + +[[package]] +name = "wasmtime" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a222f5fa1e14b2cefc286f1b68494d7a965f4bf57ec04c59bb62673d639af6" +dependencies = [ + "anyhow", + "bincode", + "cfg-if", + "indexmap", + "libc", + "log", + "object 0.29.0", + "once_cell", + "paste", + "psm", + "serde", + "target-lexicon", + "wasmparser", + "wasmtime-environ", + "wasmtime-jit", + "wasmtime-runtime", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4407a7246e7d2f3d8fb1cf0c72fda8dbafdb6dd34d555ae8bea0e5ae031089cc" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-environ" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b8b50962eae38ee319f7b24900b7cf371f03eebdc17400c1dc8575fc10c9a7" +dependencies = [ + "anyhow", + "cranelift-entity", + "gimli 0.26.2", + "indexmap", + "log", + "object 0.29.0", + "serde", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-jit" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffaed4f9a234ba5225d8e64eac7b4a5d13b994aeb37353cde2cbeb3febda9eaa" +dependencies = [ + "addr2line 0.17.0", + "anyhow", + "bincode", + "cfg-if", + "cpp_demangle", + "gimli 0.26.2", + "log", + "object 0.29.0", + "rustc-demangle", + "serde", + "target-lexicon", + "wasmtime-environ", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasmtime-jit-debug" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eed41cbcbf74ce3ff6f1d07d1b707888166dc408d1a880f651268f4f7c9194b2" +dependencies = [ + "once_cell", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a28ae1e648461bfdbb79db3efdaee1bca5b940872e4175390f465593a2e54c" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e704b126e4252788ccfc3526d4d4511d4b23c521bf123e447ac726c14545217b" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "indexmap", + "libc", + "log", + "mach", + "memfd", + "memoffset", + "paste", + "rand 0.8.5", + "rustix", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-jit-debug", + "windows-sys 0.42.0", +] + +[[package]] +name = "wasmtime-types" +version = "6.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e5572c5727c1ee7e8f28717aaa8400e4d22dcbd714ea5457d85b5005206568" +dependencies = [ + "cranelift-entity", + "serde", + "thiserror", + "wasmparser", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +dependencies = [ + "winapi", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdacb41e6a96a052c6cb63a144f24900236121c6f63f4f8219fef5977ecb0c25" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23d020b441f92996c80d94ae9166e8501e59c7bb56121189dc9eab3bd8216966" +dependencies = [ + "memchr", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "x509-cert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d224a125dec5adda27d0346b9cae9794830279c4f9c27e4ab0b6c408d54012" +dependencies = [ + "const-oid", + "der", + "flagset", + "spki", +] + +[[package]] +name = "xous" +version = "0.9.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8a9f0a696320940ab2652fa1d20c98dc59eb7ba4591eeb91a3b8e40bc9255a1" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "xous-api-log" +version = "0.1.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03e07c190c743d6d9e076f715333e94c48de41b99078343d174c707803df28c7" +dependencies = [ + "log", + "num-derive", + "num-traits", + "xous", + "xous-ipc", +] + +[[package]] +name = "xous-api-names" +version = "0.9.52" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d8361077e67966d25922056284d17d042cbb1c96a7ebc2584eb8181427cbb0" +dependencies = [ + "log", + "num-derive", + "num-traits", + "rkyv", + "xous", + "xous-api-log", + "xous-ipc", +] + +[[package]] +name = "xous-ipc" +version = "0.9.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ee1d318dacbd6242e4e2291dee7c4532249e5a0845de05d264c20fc871a0a1a" +dependencies = [ + "bitflags", + "rkyv", + "xous", +] + +[[package]] +name = "zeroize" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c394b5bd0c6f669e7275d9c20aa90ae064cb22e75a1cad54e1b34088034b149f" +dependencies = [ + "zeroize_derive", +] + +[[package]] +name = "zeroize_derive" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44bf07cb3e50ea2003396695d58bf46bc9887a1f362260446fad6bc4e79bd36c" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "synstructure", +] diff --git a/pallets/teebag/src/sgx_verify/fuzz/Cargo.toml b/pallets/teebag/src/sgx_verify/fuzz/Cargo.toml new file mode 100644 index 0000000000..7742a90c27 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/Cargo.toml @@ -0,0 +1,58 @@ +[package] +name = "sgx-verify-fuzz" +version = "0.0.0" +publish = false +edition = "2021" + +[package.metadata] +cargo-fuzz = true + +[dependencies] +base64 = { version = "0.13", default-features = false, features = ["alloc"] } +codec = { version = "3.0.0", default-features = false, features = ["derive"], package = "parity-scale-codec" } +libfuzzer-sys = "0.4" +serde_json = { version = "1.0" } +webpki = { git = "https://github.com/rustls/webpki", version = "=0.102.0-alpha.3", rev = "da923ed", package = "rustls-webpki", default-features = false, features = ["alloc", "ring"] } + +[dependencies.sgx-verify] +path = ".." + +# Prevent this from interfering with workspaces +[workspace] +members = ["."] + +[profile.release] +debug = 1 + +[[bin]] +name = "decode_quote" +path = "fuzz_targets/decode_quote.rs" +test = false +doc = false + +[[bin]] +name = "deserialize_json" +path = "fuzz_targets/deserialize_json.rs" +test = false +doc = false + +[[bin]] +name = "signature_check" +path = "fuzz_targets/signature_check.rs" +test = false +doc = false + +[[bin]] +name = "extract_tcb_info" +path = "fuzz_targets/extract_tcb_info.rs" +test = false +doc = false + +[[bin]] +name = "verify_ias_report" +path = "fuzz_targets/verify_ias_report.rs" +test = false +doc = false + +[patch.crates-io] +ring = { git = "https://github.com/betrusted-io/ring-xous", branch = "0.16.20-cleanup" } diff --git a/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/decode_quote.rs b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/decode_quote.rs new file mode 100644 index 0000000000..eea9f6dd86 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/decode_quote.rs @@ -0,0 +1,15 @@ +#![no_main] + +use codec::{Decode}; +use libfuzzer_sys::fuzz_target; +use sgx_verify::DcapQuote; + +fuzz_target!(|data: &[u8]| { + let mut copy = data; + let _quote: Result = Decode::decode(&mut copy); + + // This assert is commented out because the fuzzer manages to find a "valid" quote that can + // at least be decoded into memory. We would need additional verification steps (for example signature) + // to enable this check. + //assert!(quote.is_err()); +}); diff --git a/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/deserialize_json.rs b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/deserialize_json.rs new file mode 100644 index 0000000000..d5ac8e06fc --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/deserialize_json.rs @@ -0,0 +1,11 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sgx_verify::collateral::{EnclaveIdentity, TcbInfo}; + +fuzz_target!(|data: &[u8]| { + let enclave: Result = serde_json::from_slice(data); + assert!(enclave.is_err()); + let tcb_info: Result = serde_json::from_slice(data); + assert!(tcb_info.is_err()); +}); diff --git a/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/extract_tcb_info.rs b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/extract_tcb_info.rs new file mode 100644 index 0000000000..3f082eed1d --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/extract_tcb_info.rs @@ -0,0 +1,8 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sgx_verify::extract_tcb_info; + +fuzz_target!(|data: &[u8]| { + assert!(extract_tcb_info(data).is_err()); +}); diff --git a/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/signature_check.rs b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/signature_check.rs new file mode 100644 index 0000000000..054a3f7b15 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/signature_check.rs @@ -0,0 +1,25 @@ +#![no_main] +#![feature(core_panic)] + +pub extern crate alloc; +extern crate core; + +use libfuzzer_sys::fuzz_target; +use sgx_verify::deserialize_enclave_identity; + +fuzz_target!(|data: &[u8]| { + if data.len() < 64 { + return + } + + let cert = include_str!("../../test/dcap/qe_identity_cert.pem"); + let cert = cert.replace('\n', ""); + let decoded_cert = base64::decode(&cert).unwrap(); + let cert_der = webpki::types::CertificateDer::from(decoded_cert.as_slice()); + let cert = webpki::EndEntityCert::try_from(&cert_der).unwrap(); + let quoting_enclave = br#"{"id":"QE","version":2,"issueDate":"2022-10-18T21:55:07Z","nextUpdate":"2022-11-17T21:55:07Z","tcbEvaluationDataNumber":12,"miscselect":"00000000","miscselectMask":"FFFFFFFF","attributes":"11000000000000000000000000000000","attributesMask":"FBFFFFFFFFFFFFFF0000000000000000","mrsigner":"8C4F5775D796503E96137F77C68A829A0056AC8DED70140B081B094490C57BFF","isvprodid":1,"tcbLevels":[{"tcb":{"isvsvn":6},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"UpToDate"},{"tcb":{"isvsvn":5},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"isvsvn":4},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"isvsvn":2},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"isvsvn":1},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate"}]}"#; + let signature = &data[0..64]; + + let res = deserialize_enclave_identity("ing_enclave[..], &signature, &cert); + assert!(res.is_err(), "Found a valid signature"); +}); diff --git a/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/verify_ias_report.rs b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/verify_ias_report.rs new file mode 100644 index 0000000000..99610e5375 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/fuzz/fuzz_targets/verify_ias_report.rs @@ -0,0 +1,9 @@ +#![no_main] + +use libfuzzer_sys::fuzz_target; +use sgx_verify::verify_ias_report; + +fuzz_target!(|data: &[u8]| { + // Check test that there is now panic and that the provided data is not a valid IAS report + assert!(verify_ias_report(data).is_err()); +}); diff --git a/pallets/teebag/src/sgx_verify/mod.rs b/pallets/teebag/src/sgx_verify/mod.rs new file mode 100644 index 0000000000..32b0ff6af8 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/mod.rs @@ -0,0 +1,882 @@ +/* + Copyright 2021 Integritee AG and Supercomputing Systems AG + + 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. + +*/ + +//! Contains all the logic for understanding and verifying SGX remote attestation reports. +//! +//! Intel's documentation is scattered across different documents: +//! +//! "Intel® Software Guard Extensions: PCK Certificate and Certificate Revocation List Profile +//! Specification", further denoted as `PCK_Certificate_CRL_Spec-1.1`. +//! +//! * https://download.01.org/intel-sgx/dcap-1.2/linux/docs/Intel_SGX_PCK_Certificate_CRL_Spec-1.1.pdf +//! +//! Intel® SGX Developer Guide, further denoted as `SGX_Developer_Guide`: +//! +//! * https://download.01.org/intel-sgx/linux-1.5/docs/Intel_SGX_Developer_Guide.pdf + +#![cfg_attr(not(feature = "std"), no_std)] +pub extern crate alloc; + +use self::{ + collateral::{EnclaveIdentity, TcbInfo}, + netscape_comment::NetscapeComment, + utils::length_from_raw_data, +}; +use crate::{ + Cpusvn, Fmspc, MrEnclave, MrSigner, Pcesvn, QuotingEnclave, SgxBuildMode, TcbVersionStatus, +}; +use alloc::string::String; +use chrono::DateTime; +use codec::{Decode, Encode, Input}; +use core::time::Duration; +use der::asn1::ObjectIdentifier; +use frame_support::{ensure, traits::Len}; +use ring::signature::{self}; +use scale_info::TypeInfo; +use serde_json::Value; +use sp_std::{ + convert::{TryFrom, TryInto}, + prelude::*, +}; +use x509_cert::Certificate; + +pub mod collateral; +mod ephemeral_key; +mod netscape_comment; +#[cfg(test)] +mod tests; +mod utils; + +const SGX_REPORT_DATA_SIZE: usize = 64; +#[derive(Debug, Encode, Decode, PartialEq, Eq, Copy, Clone, TypeInfo)] +#[repr(C)] +pub struct SgxReportData { + d: [u8; SGX_REPORT_DATA_SIZE], +} + +#[derive(Debug, Encode, Decode, PartialEq, Eq, Copy, Clone, TypeInfo)] +#[repr(C)] +pub struct SGXAttributes { + flags: u64, + xfrm: u64, +} + +/// This is produced by an SGX platform, when it wants to be attested. +#[derive(Debug, Decode, Clone, TypeInfo)] +#[repr(C)] +pub struct DcapQuote { + header: DcapQuoteHeader, + body: SgxReportBody, + signature_data_len: u32, + quote_signature_data: EcdsaQuoteSignature, +} + +/// All the documentation about this can be found in the `PCK_Certificate_CRL_Spec-1.1` page 62. +#[derive(Debug, Encode, Decode, Copy, Clone, TypeInfo)] +#[repr(C)] +pub struct DcapQuoteHeader { + /// Version of the Quote data structure. + /// + /// This is version 3 for the DCAP ECDSA attestation. + version: u16, + /// Type of the Attestation Key used by the Quoting Enclave. + /// • Supported values: + /// - 2 (ECDSA-256-with-P-256 curve) + /// - 3 (ECDSA-384-with-P-384 curve) (Note: currently not supported) + attestation_key_type: u16, + /// Reserved field, value 0. + reserved: u32, + /// Security Version of the Quoting Enclave currently loaded on the platform. + qe_svn: u16, + /// Security Version of the Provisioning Certification Enclave currently loaded on the + /// platform. + pce_svn: u16, + /// Unique identifier of the QE Vendor. + /// + /// This will usually be Intel's Quoting enclave with the ID: 939A7233F79C4CA9940A0DB3957F0607. + qe_vendor_id: [u8; 16], + /// Custom user-defined data. + user_data: [u8; 20], +} + +const ATTESTATION_KEY_SIZE: usize = 64; +const REPORT_SIGNATURE_SIZE: usize = 64; + +#[derive(Debug, Decode, Clone, TypeInfo)] +#[repr(C)] +pub struct EcdsaQuoteSignature { + isv_enclave_report_signature: [u8; REPORT_SIGNATURE_SIZE], + ecdsa_attestation_key: [u8; ATTESTATION_KEY_SIZE], + qe_report: SgxReportBody, + qe_report_signature: [u8; REPORT_SIGNATURE_SIZE], + qe_authentication_data: QeAuthenticationData, + qe_certification_data: QeCertificationData, +} + +#[derive(Debug, Clone, TypeInfo)] +#[repr(C)] +pub struct QeAuthenticationData { + size: u16, + certification_data: Vec, +} + +impl Decode for QeAuthenticationData { + fn decode(input: &mut I) -> Result { + let mut size_buf: [u8; 2] = [0; 2]; + input.read(&mut size_buf)?; + let size = u16::from_le_bytes(size_buf); + + let mut certification_data = vec![0; size.into()]; + input.read(&mut certification_data)?; + + Ok(Self { size, certification_data }) + } +} + +#[derive(Debug, Clone, TypeInfo)] +#[repr(C)] +pub struct QeCertificationData { + certification_data_type: u16, + size: u32, + certification_data: Vec, +} + +impl Decode for QeCertificationData { + fn decode(input: &mut I) -> Result { + let mut certification_data_type_buf: [u8; 2] = [0; 2]; + input.read(&mut certification_data_type_buf)?; + let certification_data_type = u16::from_le_bytes(certification_data_type_buf); + + let mut size_buf: [u8; 4] = [0; 4]; + input.read(&mut size_buf)?; + let size = u32::from_le_bytes(size_buf); + // This is an arbitrary limit to prevent out of memory issues. Intel does not specify a max + // value + if size > 65_000 { + return Result::Err(codec::Error::from( + "Certification data too long. Max 65000 bytes are allowed", + )) + } + + // Safety: The try_into() can only fail due to overflow on a 16-bit system, but we anyway + // ensure the value is small enough above. + let mut certification_data = vec![0; size.try_into().unwrap()]; + input.read(&mut certification_data)?; + + Ok(Self { certification_data_type, size, certification_data }) + } +} + +// see Intel SGX SDK https://github.com/intel/linux-sgx/blob/master/common/inc/sgx_report.h +const SGX_REPORT_BODY_RESERVED1_BYTES: usize = 12; +const SGX_REPORT_BODY_RESERVED2_BYTES: usize = 32; +const SGX_REPORT_BODY_RESERVED3_BYTES: usize = 32; +const SGX_REPORT_BODY_RESERVED4_BYTES: usize = 42; +const SGX_FLAGS_DEBUG: u64 = 0x0000000000000002; + +/// SGX report about an enclave. +/// +/// We don't verify all of the fields, as some contain business logic specific data that is +/// not related to the overall validity of an enclave. We only check security related fields. The +/// only exception to this is the quoting enclave, where we validate specific fields against known +/// values. +#[derive(Debug, Encode, Decode, Copy, Clone, TypeInfo)] +#[repr(C)] +pub struct SgxReportBody { + /// Security version of the CPU. + /// + /// Reflects the processors microcode update version. + cpu_svn: [u8; 16], /* ( 0) Security Version of the CPU */ + /// State Save Area (SSA) extended feature set. Flags used for specific exception handling + /// settings. Unless, you know what you are doing these should all be 0. + /// + /// See: https://cdrdv2-public.intel.com/671544/exception-handling-in-intel-sgx.pdf. + misc_select: [u8; 4], /* ( 16) Which fields defined in SSA.MISC */ + /// Unused reserved bytes. + reserved1: [u8; SGX_REPORT_BODY_RESERVED1_BYTES], /* ( 20) */ + /// Extended Product ID of an enclave. + isv_ext_prod_id: [u8; 16], /* ( 32) ISV assigned Extended Product ID */ + /// Attributes, defines features that should be enabled for an enclave. + /// + /// Here, we only check if the Debug mode is enabled. + /// + /// More details in `SGX_Developer_Guide` under `Debug (Opt-in) Enclave Consideration` on page + /// 24. + attributes: SGXAttributes, /* ( 48) Any special Capabilities the Enclave possess */ + /// Enclave measurement. + /// + /// A single 256-bit hash that identifies the code and initial data to + /// be placed inside the enclave, the expected order and position in which they are to be + /// placed, and the security properties of those pages. More details in `SGX_Developer_Guide` + /// page 6. + mr_enclave: MrEnclave, /* ( 64) The value of the enclave's ENCLAVE measurement */ + /// Unused reserved bytes. + reserved2: [u8; SGX_REPORT_BODY_RESERVED2_BYTES], /* ( 96) */ + /// The enclave author’s public key. + /// + /// More details in `SGX_Developer_Guide` page 6. + mr_signer: MrSigner, /* (128) The value of the enclave's SIGNER measurement */ + /// Unused reserved bytes. + reserved3: [u8; SGX_REPORT_BODY_RESERVED3_BYTES], /* (160) */ + /// Config ID of an enclave. + /// + /// Todo: #142 - Investigate the relevancy of this value. + config_id: [u8; 64], /* (192) CONFIGID */ + /// The Product ID of the enclave. + /// + /// The Independent Software Vendor (ISV) should configure a unique ISVProdID for each product + /// that may want to share sealed data between enclaves signed with a specific `MRSIGNER`. + isv_prod_id: u16, /* (256) Product ID of the Enclave */ + /// ISV security version of the enclave. + /// + /// This is the enclave author's responsibility to increase it whenever a security related + /// update happened. Here, we will only check it for the `Quoting Enclave` to ensure that the + /// quoting enclave is recent enough. + /// + /// More details in `SGX_Developer_Guide` page 6. + isv_svn: u16, /* (258) Security Version of the Enclave */ + /// Config Security version of the enclave. + config_svn: u16, /* (260) CONFIGSVN */ + /// Unused reserved bytes. + reserved4: [u8; SGX_REPORT_BODY_RESERVED4_BYTES], /* (262) */ + /// Family ID assigned by the ISV. + /// + /// Todo: #142 - Investigate the relevancy of this value. + isv_family_id: [u8; 16], /* (304) ISV assigned Family ID */ + /// Custom data to be defined by the enclave author. + /// + /// We use this to provide the public key of the enclave that is to be registered on the chain. + /// Doing this, will prove that the public key is from a legitimate SGX enclave when it is + /// verified together with the remote attestation. + report_data: SgxReportData, /* (320) Data provided by the user */ +} + +impl SgxReportBody { + pub fn sgx_build_mode(&self) -> SgxBuildMode { + #[cfg(test)] + println!("attributes flag : {:x}", self.attributes.flags); + if self.attributes.flags & SGX_FLAGS_DEBUG == SGX_FLAGS_DEBUG { + SgxBuildMode::Debug + } else { + SgxBuildMode::Production + } + } + + fn verify_misc_select_field(&self, o: &QuotingEnclave) -> bool { + for i in 0..self.misc_select.len() { + if (self.misc_select[i] & o.miscselect_mask[i]) != + (o.miscselect[i] & o.miscselect_mask[i]) + { + return false + } + } + true + } + + fn verify_attributes_field(&self, o: &QuotingEnclave) -> bool { + let attributes_flags = self.attributes.flags; + + let quoting_enclave_attributes_mask = o.attributes_flags_mask_as_u64(); + let quoting_enclave_attributes_flags = o.attributes_flags_as_u64(); + + (attributes_flags & quoting_enclave_attributes_mask) == quoting_enclave_attributes_flags + } + + pub fn verify(&self, o: &QuotingEnclave) -> bool { + if self.isv_prod_id != o.isvprodid || self.mr_signer != o.mrsigner { + return false + } + if !self.verify_misc_select_field(o) { + return false + } + if !self.verify_attributes_field(o) { + return false + } + for tcb in &o.tcb { + // If the enclave isvsvn is bigger than one of the + if self.isv_svn >= tcb.isvsvn { + return true + } + } + false + } +} +// see Intel SGX SDK https://github.com/intel/linux-sgx/blob/master/common/inc/sgx_quote.h +#[derive(Encode, Decode, Copy, Clone, TypeInfo)] +#[repr(C)] +pub struct SgxQuote { + version: u16, /* 0 */ + sign_type: u16, /* 2 */ + epid_group_id: u32, /* 4 */ + qe_svn: u16, /* 8 */ + pce_svn: u16, /* 10 */ + xeid: u32, /* 12 */ + basename: [u8; 32], /* 16 */ + report_body: SgxReportBody, /* 48 */ +} + +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, sp_core::RuntimeDebug, TypeInfo, Default)] +pub enum SgxStatus { + #[default] + #[codec(index = 0)] + Invalid, + #[codec(index = 1)] + Ok, + #[codec(index = 2)] + GroupOutOfDate, + #[codec(index = 3)] + GroupRevoked, + #[codec(index = 4)] + ConfigurationNeeded, +} + +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, sp_core::RuntimeDebug, TypeInfo)] +pub struct SgxReport { + pub mr_enclave: MrEnclave, + pub pubkey: [u8; 32], + pub status: SgxStatus, + pub timestamp: u64, // unix timestamp in milliseconds + pub build_mode: SgxBuildMode, +} + +type SignatureAlgorithms = &'static [&'static dyn webpki::types::SignatureVerificationAlgorithm]; +static SUPPORTED_SIG_ALGS: SignatureAlgorithms = &[ + webpki::ring::RSA_PKCS1_2048_8192_SHA256, + webpki::ring::RSA_PKCS1_2048_8192_SHA384, + webpki::ring::RSA_PKCS1_2048_8192_SHA512, + webpki::ring::RSA_PKCS1_3072_8192_SHA384, +]; + +//pub const IAS_REPORT_CA: &[u8] = include_bytes!("../AttestationReportSigningCACert.pem"); + +pub static IAS_SERVER_ROOTS: &[webpki::types::TrustAnchor<'static>; 1] = &[ + /* + * -----BEGIN CERTIFICATE----- + * MIIFSzCCA7OgAwIBAgIJANEHdl0yo7CUMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNV + * BAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNV + * BAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0 + * YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwIBcNMTYxMTE0MTUzNzMxWhgPMjA0OTEy + * MzEyMzU5NTlaMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwL + * U2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQD + * DCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwggGiMA0G + * CSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQCfPGR+tXc8u1EtJzLA10Feu1Wg+p7e + * LmSRmeaCHbkQ1TF3Nwl3RmpqXkeGzNLd69QUnWovYyVSndEMyYc3sHecGgfinEeh + * rgBJSEdsSJ9FpaFdesjsxqzGRa20PYdnnfWcCTvFoulpbFR4VBuXnnVLVzkUvlXT + * L/TAnd8nIZk0zZkFJ7P5LtePvykkar7LcSQO85wtcQe0R1Raf/sQ6wYKaKmFgCGe + * NpEJUmg4ktal4qgIAxk+QHUxQE42sxViN5mqglB0QJdUot/o9a/V/mMeH8KvOAiQ + * byinkNndn+Bgk5sSV5DFgF0DffVqmVMblt5p3jPtImzBIH0QQrXJq39AT8cRwP5H + * afuVeLHcDsRp6hol4P+ZFIhu8mmbI1u0hH3W/0C2BuYXB5PC+5izFFh/nP0lc2Lf + * 6rELO9LZdnOhpL1ExFOq9H/B8tPQ84T3Sgb4nAifDabNt/zu6MmCGo5U8lwEFtGM + * RoOaX4AS+909x00lYnmtwsDVWv9vBiJCXRsCAwEAAaOByTCBxjBgBgNVHR8EWTBX + * MFWgU6BRhk9odHRwOi8vdHJ1c3RlZHNlcnZpY2VzLmludGVsLmNvbS9jb250ZW50 + * L0NSTC9TR1gvQXR0ZXN0YXRpb25SZXBvcnRTaWduaW5nQ0EuY3JsMB0GA1UdDgQW + * BBR4Q3t2pn680K9+QjfrNXw7hwFRPDAfBgNVHSMEGDAWgBR4Q3t2pn680K9+Qjfr + * NXw7hwFRPDAOBgNVHQ8BAf8EBAMCAQYwEgYDVR0TAQH/BAgwBgEB/wIBADANBgkq + * hkiG9w0BAQsFAAOCAYEAeF8tYMXICvQqeXYQITkV2oLJsp6J4JAqJabHWxYJHGir + * IEqucRiJSSx+HjIJEUVaj8E0QjEud6Y5lNmXlcjqRXaCPOqK0eGRz6hi+ripMtPZ + * sFNaBwLQVV905SDjAzDzNIDnrcnXyB4gcDFCvwDFKKgLRjOB/WAqgscDUoGq5ZVi + * zLUzTqiQPmULAQaB9c6Oti6snEFJiCQ67JLyW/E83/frzCmO5Ru6WjU4tmsmy8Ra + * Ud4APK0wZTGtfPXU7w+IBdG5Ez0kE1qzxGQaL4gINJ1zMyleDnbuS8UicjJijvqA + * 152Sq049ESDz+1rRGc2NVEqh1KaGXmtXvqxXcTB+Ljy5Bw2ke0v8iGngFBPqCTVB + * 3op5KBG3RjbF6RRSzwzuWfL7QErNC8WEy5yDVARzTA5+xmBc388v9Dm21HGfcC8O + * DD+gT9sSpssq0ascmvH49MOgjt1yoysLtdCtJW/9FZpoOypaHx0R+mJTLwPXVMrv + * DaVzWh5aiEx+idkSGMnX + * -----END CERTIFICATE----- + */ + webpki::types::TrustAnchor { + subject: webpki::types::Der::from_slice(b"1\x0b0\t\x06\x03U\x04\x06\x13\x02US1\x0b0\t\x06\x03U\x04\x08\x0c\x02CA1\x140\x12\x06\x03U\x04\x07\x0c\x0bSanta Clara1\x1a0\x18\x06\x03U\x04\n\x0c\x11Intel Corporation100.\x06\x03U\x04\x03\x0c\'Intel SGX Attestation Report Signing CA"), + subject_public_key_info: webpki::types::Der::from_slice(b"0\r\x06\t*\x86H\x86\xf7\r\x01\x01\x01\x05\x00\x03\x82\x01\x8f\x000\x82\x01\x8a\x02\x82\x01\x81\x00\x9f@u1@N6\xb3\x15b7\x99\xaa\x82Pt@\x97T\xa2\xdf\xe8\xf5\xaf\xd5\xfec\x1e\x1f\xc2\xaf8\x08\x90o(\xa7\x90\xd9\xdd\x9f\xe0`\x93\x9b\x12W\x90\xc5\x80]\x03}\xf5j\x99S\x1b\x96\xdei\xde3\xed\"l\xc1 }\x10B\xb5\xc9\xab\x7f@O\xc7\x11\xc0\xfeGi\xfb\x95x\xb1\xdc\x0e\xc4i\xea\x1a%\xe0\xff\x99\x14\x88n\xf2i\x9b#[\xb4\x84}\xd6\xff@\xb6\x06\xe6\x17\x07\x93\xc2\xfb\x98\xb3\x14X\x7f\x9c\xfd%sb\xdf\xea\xb1\x0b;\xd2\xd9vs\xa1\xa4\xbdD\xc4S\xaa\xf4\x7f\xc1\xf2\xd3\xd0\xf3\x84\xf7J\x06\xf8\x9c\x08\x9f\r\xa6\xcd\xb7\xfc\xee\xe8\xc9\x82\x1a\x8eT\xf2\\\x04\x16\xd1\x8cF\x83\x9a_\x80\x12\xfb\xdd=\xc7M%by\xad\xc2\xc0\xd5Z\xffo\x06\"B]\x1b\x02\x03\x01\x00\x01"), + name_constraints: None + }, +]; + +/// The needed code for a trust anchor can be extracted using `webpki` with something like this: +/// println!("{:?}", webpki::TrustAnchor::try_from_cert_der(&root_cert)); +#[allow(clippy::zero_prefixed_literal)] +pub static DCAP_SERVER_ROOTS: &[webpki::types::TrustAnchor<'static>; 1] = + &[webpki::types::TrustAnchor { + subject: webpki::types::Der::from_slice(&[ + 49, 26, 48, 24, 06, 03, 85, 04, 03, 12, 17, 73, 110, 116, 101, 108, 32, 83, 71, 88, 32, + 82, 111, 111, 116, 32, 67, 65, 49, 26, 48, 24, 06, 03, 85, 04, 10, 12, 17, 73, 110, + 116, 101, 108, 32, 67, 111, 114, 112, 111, 114, 97, 116, 105, 111, 110, 49, 20, 48, 18, + 06, 03, 85, 04, 07, 12, 11, 83, 97, 110, 116, 97, 32, 67, 108, 97, 114, 97, 49, 11, 48, + 09, 06, 03, 85, 04, 08, 12, 02, 67, 65, 49, 11, 48, 09, 06, 03, 85, 04, 06, 19, 02, 85, + 83, + ]), + subject_public_key_info: webpki::types::Der::from_slice(&[ + 48, 19, 06, 07, 42, 134, 72, 206, 61, 02, 01, 06, 08, 42, 134, 72, 206, 61, 03, 01, 07, + 03, 66, 00, 04, 11, 169, 196, 192, 192, 200, 97, 147, 163, 254, 35, 214, 176, 44, 218, + 16, 168, 187, 212, 232, 142, 72, 180, 69, 133, 97, 163, 110, 112, 85, 37, 245, 103, + 145, 142, 46, 220, 136, 228, 13, 134, 11, 208, 204, 78, 226, 106, 172, 201, 136, 229, + 05, 169, 83, 85, 140, 69, 63, 107, 09, 04, 174, 115, 148, + ]), + name_constraints: None, + }]; + +/// Contains an unvalidated ias remote attestation certificate. +/// +/// Wrapper to implemented parsing and verification traits on it. +pub struct CertDer<'a>(&'a [u8]); + +/// Encode two 32-byte values in DER format +/// This is meant for 256 bit ECC signatures or public keys +pub fn encode_as_der(data: &[u8]) -> Result, &'static str> { + if data.len() != 64 { + return Result::Err("Key must be 64 bytes long") + } + let mut sequence = der::asn1::SequenceOf::::new(); + sequence + .add(der::asn1::UIntRef::new(&data[0..32]).map_err(|_| "Invalid public key")?) + .map_err(|_| "Invalid public key")?; + sequence + .add(der::asn1::UIntRef::new(&data[32..]).map_err(|_| "Invalid public key")?) + .map_err(|_| "Invalid public key")?; + // 72 should be enough in all cases. 2 + 2 x (32 + 3) + let mut asn1 = vec![0u8; 72]; + let mut writer = der::SliceWriter::new(&mut asn1); + writer.encode(&sequence).map_err(|_| "Could not encode public key to DER")?; + Ok(writer.finish().map_err(|_| "Could not convert public key to DER")?.to_vec()) +} + +/// Extracts the specified data into a `EnclaveIdentity` instance. +/// Also verifies that the data matches the given signature, was produced by the given certificate +/// and matches the data +pub fn deserialize_enclave_identity( + data: &[u8], + signature: &[u8], + certificate: &webpki::EndEntityCert, +) -> Result { + let signature = encode_as_der(signature)?; + verify_signature(certificate, data, &signature, webpki::ring::ECDSA_P256_SHA256)?; + serde_json::from_slice(data).map_err(|_| "Deserialization failed") +} + +/// Extracts the specified data into a `TcbInfo` instance. +/// Also verifies that the data matches the given signature, was produced by the given certificate +/// and matches the data +pub fn deserialize_tcb_info( + data: &[u8], + signature: &[u8], + certificate: &webpki::EndEntityCert, +) -> Result { + let signature = encode_as_der(signature)?; + verify_signature(certificate, data, &signature, webpki::ring::ECDSA_P256_SHA256)?; + serde_json::from_slice(data).map_err(|_| "Deserialization failed") +} + +/// Extract a list of certificates from a byte vec. The certificates must be separated by +/// `-----BEGIN CERTIFICATE-----` and `-----END CERTIFICATE-----` markers +pub fn extract_certs(cert_chain: &[u8]) -> Vec> { + // The certificates should be valid UTF-8 but if not we continue. The certificate verification + // will fail at a later point. + let certs_concat = String::from_utf8_lossy(cert_chain); + let certs_concat = certs_concat.replace('\n', ""); + let certs_concat = certs_concat.replace("-----BEGIN CERTIFICATE-----", ""); + // Use the end marker to split the string into certificates + let parts = certs_concat.split("-----END CERTIFICATE-----"); + parts.filter(|p| !p.is_empty()).filter_map(|p| base64::decode(p).ok()).collect() +} + +/// Verifies that the `leaf_cert` in combination with the `intermediate_certs` establishes +/// a valid certificate chain that is rooted in one of the trust anchors that was compiled into to +/// the pallet +pub fn verify_certificate_chain<'a>( + leaf_cert: &webpki::EndEntityCert<'a>, + intermediate_certs: &[webpki::types::CertificateDer<'a>], + verification_time: u64, +) -> Result<(), &'static str> { + let time = + webpki::types::UnixTime::since_unix_epoch(Duration::from_secs(verification_time / 1000)); + let sig_algs = &[webpki::ring::ECDSA_P256_SHA256]; + leaf_cert + .verify_for_usage( + sig_algs, + DCAP_SERVER_ROOTS, + intermediate_certs, + time, + webpki::KeyUsage::client_auth(), + None, + ) + .map_err(|_| "Invalid certificate chain")?; + Ok(()) +} +#[allow(unused)] +pub fn extract_tcb_info_from_raw_dcap_quote( + dcap_quote_raw: &[u8], +) -> Result<(Fmspc, TcbVersionStatus), &'static str> { + let mut dcap_quote_clone = dcap_quote_raw; + let quote: DcapQuote = + Decode::decode(&mut dcap_quote_clone).map_err(|_| "Failed to decode attestation report")?; + + ensure!(quote.header.version == 3, "Only support for version 3"); + ensure!(quote.header.attestation_key_type == 2, "Only support for ECDSA-256"); + ensure!( + quote.quote_signature_data.qe_certification_data.certification_data_type == 5, + "Only support for PEM formatted PCK Cert Chain" + ); + + let certs = extract_certs("e.quote_signature_data.qe_certification_data.certification_data); + + let (fmspc, tcb_info) = extract_tcb_info(&certs[0])?; + + Ok((fmspc, tcb_info)) +} + +pub fn verify_dcap_quote( + dcap_quote_raw: &[u8], + verification_time: u64, + qe: &QuotingEnclave, +) -> Result<(Fmspc, TcbVersionStatus, SgxReport), &'static str> { + let mut dcap_quote_clone = dcap_quote_raw; + let quote: DcapQuote = + Decode::decode(&mut dcap_quote_clone).map_err(|_| "Failed to decode attestation report")?; + + #[cfg(test)] + println!("{:?}", quote); + + ensure!(quote.header.version == 3, "Only support for version 3"); + ensure!(quote.header.attestation_key_type == 2, "Only support for ECDSA-256"); + ensure!( + quote.quote_signature_data.qe_certification_data.certification_data_type == 5, + "Only support for PEM formatted PCK Cert Chain" + ); + ensure!(quote.quote_signature_data.qe_report.verify(qe), "Enclave rejected by quoting enclave"); + let mut xt_signer_array = [0u8; 32]; + xt_signer_array.copy_from_slice("e.body.report_data.d[..32]); + + let certs = extract_certs("e.quote_signature_data.qe_certification_data.certification_data); + ensure!(certs.len() >= 2, "Certificate chain must have at least two certificates"); + let intermediate_certificate_slices: Vec = + certs[1..].iter().map(|c| c.as_slice().into()).collect(); + let leaf_cert_der = webpki::types::CertificateDer::from(certs[0].as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der) + .map_err(|_| "Failed to parse leaf certificate")?; + verify_certificate_chain(&leaf_cert, &intermediate_certificate_slices, verification_time)?; + + let (fmspc, tcb_info) = extract_tcb_info(&certs[0])?; + + // For this part some understanding of the document (Especially chapter A.4: Quote Format) + // Intel® Software Guard Extensions (Intel® SGX) Data Center Attestation Primitives: ECDSA Quote + // Library API https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_SGX_ECDSA_QuoteLibReference_DCAP_API.pdf + + const AUTHENTICATION_DATA_SIZE: usize = 32; // This is actually variable but assume 32 for now. This is also hard-coded to 32 in the Intel + // DCAP repo + const DCAP_QUOTE_HEADER_SIZE: usize = core::mem::size_of::(); + const REPORT_SIZE: usize = core::mem::size_of::(); + const QUOTE_SIGNATURE_DATA_LEN_SIZE: usize = core::mem::size_of::(); + + let attestation_key_offset = DCAP_QUOTE_HEADER_SIZE + + REPORT_SIZE + + QUOTE_SIGNATURE_DATA_LEN_SIZE + + REPORT_SIGNATURE_SIZE; + let authentication_data_offset = attestation_key_offset + + ATTESTATION_KEY_SIZE + + REPORT_SIZE + + REPORT_SIGNATURE_SIZE + + core::mem::size_of::(); //Size of the QE authentication data. We ignore this for now and assume 32. See + // AUTHENTICATION_DATA_SIZE + let mut hash_data = [0u8; ATTESTATION_KEY_SIZE + AUTHENTICATION_DATA_SIZE]; + hash_data[0..ATTESTATION_KEY_SIZE].copy_from_slice( + &dcap_quote_raw[attestation_key_offset..(attestation_key_offset + ATTESTATION_KEY_SIZE)], + ); + hash_data[ATTESTATION_KEY_SIZE..].copy_from_slice( + &dcap_quote_raw + [authentication_data_offset..(authentication_data_offset + AUTHENTICATION_DATA_SIZE)], + ); + // Ensure that the hash matches the intel signed hash in the QE report. This establishes trust + // into the attestation key. + let hash = ring::digest::digest(&ring::digest::SHA256, &hash_data); + ensure!( + hash.as_ref() == "e.quote_signature_data.qe_report.report_data.d[0..32], + "Hashes must match" + ); + + let qe_report_offset = attestation_key_offset + ATTESTATION_KEY_SIZE; + let qe_report_slice = &dcap_quote_raw[qe_report_offset..(qe_report_offset + REPORT_SIZE)]; + let mut pub_key = [0x04u8; 65]; //Prepend 0x04 to specify uncompressed format + pub_key[1..].copy_from_slice("e.quote_signature_data.ecdsa_attestation_key); + + let peer_public_key = + signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, pub_key); + let isv_report_slice = &dcap_quote_raw[0..(DCAP_QUOTE_HEADER_SIZE + REPORT_SIZE)]; + // Verify that the enclave data matches the signature generated by the trusted attestation key. + // This establishes trust into the data of the enclave we actually want to verify + peer_public_key + .verify(isv_report_slice, "e.quote_signature_data.isv_enclave_report_signature) + .map_err(|_| "Failed to verify report signature")?; + + // Verify that the QE report was signed by Intel. This establishes trust into the QE report. + let asn1_signature = encode_as_der("e.quote_signature_data.qe_report_signature)?; + verify_signature( + &leaf_cert, + qe_report_slice, + &asn1_signature, + webpki::ring::ECDSA_P256_SHA256, + )?; + + ensure!(dcap_quote_clone.is_empty(), "There should be no bytes left over after decoding"); + let report = SgxReport { + mr_enclave: quote.body.mr_enclave, + status: SgxStatus::Ok, + pubkey: xt_signer_array, + timestamp: verification_time, + build_mode: quote.body.sgx_build_mode(), + }; + Ok((fmspc, tcb_info, report)) +} + +// make sure this function doesn't panic! +pub fn verify_ias_report(cert_der: &[u8]) -> Result { + // Before we reach here, the runtime already verified the extrinsic is properly signed by the + // extrinsic sender Hence, we skip: EphemeralKey::try_from(cert)?; + + #[cfg(test)] + println!("verifyRA: start verifying RA cert"); + + let cert = CertDer(cert_der); + let netscape = NetscapeComment::try_from(cert)?; + let sig_cert_der = webpki::types::CertificateDer::from(netscape.sig_cert.as_slice()); + let sig_cert = webpki::EndEntityCert::try_from(&sig_cert_der).map_err(|_| "Bad der")?; + + verify_signature( + &sig_cert, + netscape.attestation_raw, + &netscape.sig, + webpki::ring::RSA_PKCS1_2048_8192_SHA256, + )?; + + // FIXME: now hardcoded. but certificate renewal would have to be done manually anyway... + // chain wasm update or by some sudo call + let valid_until = webpki::types::UnixTime::since_unix_epoch(Duration::from_secs(1573419050)); + verify_server_cert(&sig_cert, valid_until)?; + + parse_report(&netscape) +} + +fn parse_report(netscape: &NetscapeComment) -> Result { + let report_raw: &[u8] = netscape.attestation_raw; + // parse attestation report + let attn_report: Value = match serde_json::from_slice(report_raw) { + Ok(report) => report, + Err(_) => return Err("RA report parsing error"), + }; + + let _ra_timestamp = match &attn_report["timestamp"] { + Value::String(time) => { + let time_fixed = time.clone() + "+0000"; + match DateTime::parse_from_str(&time_fixed, "%Y-%m-%dT%H:%M:%S%.f%z") { + Ok(d) => d.timestamp(), + Err(_) => return Err("RA report timestamp parsing error"), + } + }, + _ => return Err("Failed to fetch timestamp from attestation report"), + }; + + // in milliseconds + let ra_timestamp: u64 = (_ra_timestamp * 1000) + .try_into() + .map_err(|_| "Error converting report.timestamp to u64")?; + + #[cfg(test)] + println!("verifyRA attestation timestamp [unix epoch]: {}", ra_timestamp); + + // get quote status (mandatory field) + let ra_status = match &attn_report["isvEnclaveQuoteStatus"] { + Value::String(quote_status) => match quote_status.as_ref() { + "OK" => SgxStatus::Ok, + "GROUP_OUT_OF_DATE" => SgxStatus::GroupOutOfDate, + "GROUP_REVOKED" => SgxStatus::GroupRevoked, + "CONFIGURATION_NEEDED" => SgxStatus::ConfigurationNeeded, + _ => SgxStatus::Invalid, + }, + _ => return Err("Failed to fetch isvEnclaveQuoteStatus from attestation report"), + }; + + #[cfg(test)] + println!("verifyRA attestation status is: {:?}", ra_status); + // parse quote body + if let Value::String(quote_raw) = &attn_report["isvEnclaveQuoteBody"] { + let quote = match base64::decode(quote_raw) { + Ok(q) => q, + Err(_) => return Err("Quote Decoding Error"), + }; + #[cfg(test)] + println!("Quote read. len={}", quote.len()); + // TODO: lack security check here + let sgx_quote: SgxQuote = match Decode::decode(&mut "e[..]) { + Ok(q) => q, + Err(_) => return Err("could not decode quote"), + }; + + #[cfg(test)] + { + println!("sgx quote version = {}", sgx_quote.version); + println!("sgx quote signature type = {}", sgx_quote.sign_type); + //println!("sgx quote report_data = {:?}", sgx_quote.report_body.report_data.d[..32]); + println!("sgx quote mr_enclave = {:x?}", sgx_quote.report_body.mr_enclave); + println!("sgx quote mr_signer = {:x?}", sgx_quote.report_body.mr_signer); + println!("sgx quote report_data = {:x?}", sgx_quote.report_body.report_data.d.to_vec()); + } + + let mut xt_signer_array = [0u8; 32]; + xt_signer_array.copy_from_slice(&sgx_quote.report_body.report_data.d[..32]); + Ok(SgxReport { + mr_enclave: sgx_quote.report_body.mr_enclave, + status: ra_status, + pubkey: xt_signer_array, + timestamp: ra_timestamp, + build_mode: sgx_quote.report_body.sgx_build_mode(), + }) + } else { + Err("Failed to parse isvEnclaveQuoteBody from attestation report") + } +} + +/// * `signature` - Must be encoded in DER format. +pub fn verify_signature( + entity_cert: &webpki::EndEntityCert, + data: &[u8], + signature: &[u8], + signature_algorithm: &dyn webpki::types::SignatureVerificationAlgorithm, +) -> Result<(), &'static str> { + match entity_cert.verify_signature(signature_algorithm, data, signature) { + Ok(()) => { + #[cfg(test)] + println!("IAS signature is valid"); + Ok(()) + }, + Err(_e) => { + #[cfg(test)] + println!("RSA Signature ERROR: {}", _e); + Err("bad signature") + }, + } +} + +pub fn verify_server_cert( + sig_cert: &webpki::EndEntityCert, + timestamp_valid_until: webpki::types::UnixTime, +) -> Result<(), &'static str> { + let chain: Vec = Vec::new(); + match sig_cert.verify_for_usage( + SUPPORTED_SIG_ALGS, + IAS_SERVER_ROOTS, + &chain, + timestamp_valid_until, + webpki::KeyUsage::server_auth(), + None, + ) { + Ok(()) => { + #[cfg(test)] + println!("CA is valid"); + Ok(()) + }, + Err(_e) => { + #[cfg(test)] + println!("CA ERROR: {}", _e); + Err("CA verification failed") + }, + } +} + +/// See document "Intel® Software Guard Extensions: PCK Certificate and Certificate Revocation List +/// Profile Specification" https://download.01.org/intel-sgx/dcap-1.2/linux/docs/Intel_SGX_PCK_Certificate_CRL_Spec-1.1.pdf +const INTEL_SGX_EXTENSION_OID: ObjectIdentifier = + ObjectIdentifier::new_unwrap("1.2.840.113741.1.13.1"); +const OID_FMSPC: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113741.1.13.1.4"); +const OID_PCESVN: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113741.1.13.1.2.17"); +const OID_CPUSVN: ObjectIdentifier = ObjectIdentifier::new_unwrap("1.2.840.113741.1.13.1.2.18"); + +pub fn extract_tcb_info(cert: &[u8]) -> Result<(Fmspc, TcbVersionStatus), &'static str> { + let extension_section = get_intel_extension(cert)?; + + let fmspc = get_fmspc(&extension_section)?; + let cpusvn = get_cpusvn(&extension_section)?; + let pcesvn = get_pcesvn(&extension_section)?; + + Ok((fmspc, TcbVersionStatus::new(cpusvn, pcesvn))) +} + +fn get_intel_extension(der_encoded: &[u8]) -> Result, &'static str> { + let cert: Certificate = + der::Decode::from_der(der_encoded).map_err(|_| "Error parsing certificate")?; + let mut extension_iter = cert + .tbs_certificate + .extensions + .as_deref() + .unwrap_or(&[]) + .iter() + .filter(|e| e.extn_id == INTEL_SGX_EXTENSION_OID) + .map(|e| e.extn_value); + + let extension = extension_iter.next(); + ensure!( + extension.is_some() && extension_iter.next().is_none(), + "There should only be one section containing Intel extensions" + ); + // SAFETY: Ensured above that extension.is_some() == true + Ok(extension.unwrap().to_vec()) +} + +fn get_fmspc(der: &[u8]) -> Result { + let bytes_oid = OID_FMSPC.as_bytes(); + let mut offset = der + .windows(bytes_oid.len()) + .position(|window| window == bytes_oid) + .ok_or("Certificate does not contain 'FMSPC_OID'")?; + offset += 12; // length oid (10) + asn1 tag (1) + asn1 length10 (1) + + let fmspc_size = core::mem::size_of::() / core::mem::size_of::(); + let data = der.get(offset..offset + fmspc_size).ok_or("Index out of bounds")?; + data.try_into().map_err(|_| "FMSPC must be 6 bytes long") +} + +fn get_cpusvn(der: &[u8]) -> Result { + let bytes_oid = OID_CPUSVN.as_bytes(); + let mut offset = der + .windows(bytes_oid.len()) + .position(|window| window == bytes_oid) + .ok_or("Certificate does not contain 'CPUSVN_OID'")?; + offset += 13; // length oid (11) + asn1 tag (1) + asn1 length10 (1) + + // CPUSVN is specified to have length 16 + let len = 16; + let data = der.get(offset..offset + len).ok_or("Index out of bounds")?; + data.try_into().map_err(|_| "CPUSVN must be 16 bytes long") +} + +fn get_pcesvn(der: &[u8]) -> Result { + let bytes_oid = OID_PCESVN.as_bytes(); + let mut offset = der + .windows(bytes_oid.len()) + .position(|window| window == bytes_oid) + .ok_or("Certificate does not contain 'PCESVN_OID'")?; + // length oid + asn1 tag (1 byte) + offset += bytes_oid.len() + 1; + // PCESVN can be 1 or 2 bytes + let len = length_from_raw_data(der, &mut offset)?; + offset += 1; // length_from_raw_data does not move the offset when the length is encoded in a single byte + ensure!(len == 1 || len == 2, "PCESVN must be 1 or 2 bytes"); + let data = der.get(offset..offset + len).ok_or("Index out of bounds")?; + if data.len() == 1 { + Ok(u16::from(data[0])) + } else { + // Unwrap is fine here as we check the length above + // DER integers are encoded in big endian + Ok(u16::from_be_bytes(data.try_into().unwrap())) + } +} diff --git a/pallets/teebag/src/sgx_verify/netscape_comment.rs b/pallets/teebag/src/sgx_verify/netscape_comment.rs new file mode 100644 index 0000000000..3d39d22e19 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/netscape_comment.rs @@ -0,0 +1,48 @@ +use super::{utils::length_from_raw_data, CertDer}; +use frame_support::ensure; +use sp_std::{convert::TryFrom, prelude::Vec}; + +pub struct NetscapeComment<'a> { + pub attestation_raw: &'a [u8], + pub sig: Vec, + pub sig_cert: Vec, +} + +pub const NS_CMT_OID: &[u8; 11] = + &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x86, 0xF8, 0x42, 0x01, 0x0D]; + +impl<'a> TryFrom> for NetscapeComment<'a> { + type Error = &'static str; + + fn try_from(value: CertDer<'a>) -> Result { + // Search for Netscape Comment OID + let cert_der = value.0; + + let mut offset = cert_der + .windows(NS_CMT_OID.len()) + .position(|window| window == NS_CMT_OID) + .ok_or("Certificate does not contain 'ns_cmt_oid'")?; + + offset += 12; // 11 + TAG (0x04) + + #[cfg(test)] + println!("netscape"); + // Obtain Netscape Comment length + let len = length_from_raw_data(cert_der, &mut offset)?; + // Obtain Netscape Comment + offset += 1; + let netscape_raw = cert_der + .get(offset..offset + len) + .ok_or("Index out of bounds")? + .split(|x| *x == 0x7C) // 0x7C is the character '|' + .collect::>(); + ensure!(netscape_raw.len() == 3, "Invalid netscape payload"); + + let sig = base64::decode(netscape_raw[1]).map_err(|_| "Signature Decoding Error")?; + + let sig_cert = base64::decode_config(netscape_raw[2], base64::STANDARD) + .map_err(|_| "Cert Decoding Error")?; + + Ok(NetscapeComment { attestation_raw: netscape_raw[0], sig, sig_cert }) + } +} diff --git a/pallets/teebag/src/sgx_verify/test/dcap/dcap_quote_cert.der b/pallets/teebag/src/sgx_verify/test/dcap/dcap_quote_cert.der new file mode 100644 index 0000000000..accd665a19 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/dcap_quote_cert.der @@ -0,0 +1,25 @@ +MIIEjjCCBDSgAwIBAgIVAMyWqlD3mkxu2FhYuPtrCp2bId06MAoGCCqGSM49BAMC +MHExIzAhBgNVBAMMGkludGVsIFNHWCBQQ0sgUHJvY2Vzc29yIENBMRowGAYDVQQK +DBFJbnRlbCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNV +BAgMAkNBMQswCQYDVQQGEwJVUzAeFw0yMjA1MjMxNTA3MDRaFw0yOTA1MjMxNTA3 +MDRaMHAxIjAgBgNVBAMMGUludGVsIFNHWCBQQ0sgQ2VydGlmaWNhdGUxGjAYBgNV +BAoMEUludGVsIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkG +A1UECAwCQ0ExCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE +7pMyqXdHOoVduZAG8j3Wliu0FYWhT+tjjYj9Tdlmr51x8iudHDGTxVU2oeZCnhea +tQuqCBJ0hV7A6gLn5fvXbqOCAqgwggKkMB8GA1UdIwQYMBaAFNDoqtp11/kuSReY +PHsUZdDV8llNMGwGA1UdHwRlMGMwYaBfoF2GW2h0dHBzOi8vYXBpLnRydXN0ZWRz +ZXJ2aWNlcy5pbnRlbC5jb20vc2d4L2NlcnRpZmljYXRpb24vdjMvcGNrY3JsP2Nh +PXByb2Nlc3NvciZlbmNvZGluZz1kZXIwHQYDVR0OBBYEFPW1Uov5Ucy1jHgCeBpx +b6/tkgpoMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMIIB1AYJKoZIhvhN +AQ0BBIIBxTCCAcEwHgYKKoZIhvhNAQ0BAQQQs95DBukqMBDQJyrEH4oTxDCCAWQG +CiqGSIb4TQENAQIwggFUMBAGCyqGSIb4TQENAQIBAgERMBAGCyqGSIb4TQENAQIC +AgERMBAGCyqGSIb4TQENAQIDAgECMBAGCyqGSIb4TQENAQIEAgEEMBAGCyqGSIb4 +TQENAQIFAgEBMBEGCyqGSIb4TQENAQIGAgIAgDAQBgsqhkiG+E0BDQECBwIBBzAQ +BgsqhkiG+E0BDQECCAIBADAQBgsqhkiG+E0BDQECCQIBADAQBgsqhkiG+E0BDQEC +CgIBADAQBgsqhkiG+E0BDQECCwIBADAQBgsqhkiG+E0BDQECDAIBADAQBgsqhkiG ++E0BDQECDQIBADAQBgsqhkiG+E0BDQECDgIBADAQBgsqhkiG+E0BDQECDwIBADAQ +BgsqhkiG+E0BDQECEAIBADAQBgsqhkiG+E0BDQECEQIBCzAfBgsqhkiG+E0BDQEC +EgQQERECBAGABwAAAAAAAAAAADAQBgoqhkiG+E0BDQEDBAIAADAUBgoqhkiG+E0B +DQEEBAYAkG6hAAAwDwYKKoZIhvhNAQ0BBQoBADAKBggqhkjOPQQDAgNIADBFAiB4 +20uxl1Ncxh6j1CtI1cJHsZxvWg00c1eRWWY2prTWPQIhAIhmmQUOcyRxubRUyGHW +/SbMjV5v6ZVVQn2IIuZUWM64 \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/pck_crl.der b/pallets/teebag/src/sgx_verify/test/dcap/pck_crl.der new file mode 100644 index 0000000000..5dad4cd40a --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/pck_crl.der @@ -0,0 +1 @@ +308201cd30820173020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3232313032333231353534345a170d3232313132323231353534345a3081a030330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3232313032333231353534345a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3232313032333231353534345a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3232313032333231353534345a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d040302034800304502200809ebf5477e3129f8efa8f3c67b4c204c879919efa78e08c7510a3631c0fe410221008e9cd32a3a1d97242a46cee7589013d220d7bf426607275af6fd3f17f78282a3 \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/pck_crl_issuer_chain.pem b/pallets/teebag/src/sgx_verify/test/dcap/pck_crl_issuer_chain.pem new file mode 100644 index 0000000000..265ae12bda --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/pck_crl_issuer_chain.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIICmDCCAj6gAwIBAgIVANDoqtp11/kuSReYPHsUZdDV8llNMAoGCCqGSM49BAMC +MGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD +b3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw +CQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHExIzAh +BgNVBAMMGkludGVsIFNHWCBQQ0sgUHJvY2Vzc29yIENBMRowGAYDVQQKDBFJbnRl +bCBDb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNB +MQswCQYDVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABL9q+NMp2IOg +tdl1bk/uWZ5+TGQm8aCi8z78fs+fKCQ3d+uDzXnVTAT2ZhDCifyIuJwvN3wNBp9i +HBSSMJMJrBOjgbswgbgwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqww +UgYDVR0fBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNl +cnZpY2VzLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFNDo +qtp11/kuSReYPHsUZdDV8llNMA4GA1UdDwEB/wQEAwIBBjASBgNVHRMBAf8ECDAG +AQH/AgEAMAoGCCqGSM49BAMCA0gAMEUCIQCJgTbtVqOyZ1m3jqiAXM6QYa6r5sWS +4y/G7y8uIJGxdwIgRqPvBSKzzQagBLQq5s5A70pdoiaRJ8z/0uDz4NgV91k= +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG +A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 +aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT +AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 +1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB +uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ +MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 +ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV +Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI +KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg +AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/qe_identity.json b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity.json new file mode 100644 index 0000000000..d977ca5fd3 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity.json @@ -0,0 +1 @@ +{"enclaveIdentity":{"id":"QE","version":2,"issueDate":"2022-12-04T22:45:33Z","nextUpdate":"2023-01-03T22:45:33Z","tcbEvaluationDataNumber":13,"miscselect":"00000000","miscselectMask":"FFFFFFFF","attributes":"11000000000000000000000000000000","attributesMask":"FBFFFFFFFFFFFFFF0000000000000000","mrsigner":"8C4F5775D796503E96137F77C68A829A0056AC8DED70140B081B094490C57BFF","isvprodid":1,"tcbLevels":[{"tcb":{"isvsvn":6},"tcbDate":"2022-11-09T00:00:00Z","tcbStatus":"UpToDate"},{"tcb":{"isvsvn":5},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00477"]},{"tcb":{"isvsvn":4},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00334","INTEL-SA-00477"]},{"tcb":{"isvsvn":2},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00219","INTEL-SA-00293","INTEL-SA-00334","INTEL-SA-00477"]},{"tcb":{"isvsvn":1},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00202","INTEL-SA-00219","INTEL-SA-00293","INTEL-SA-00334","INTEL-SA-00477"]}]},"signature":"47accba321e57c20722a0d3d1db11c9b52661239857dc578ca1bde13976ee288cf39f72111ffe445c7389ef56447c79e30e6b83a8863ed9880de5bde4a8d5c91"} \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_cert.pem b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_cert.pem new file mode 100644 index 0000000000..6624ca3837 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_cert.pem @@ -0,0 +1,14 @@ +MIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG +A1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw +b3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD +VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv +P+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju +ypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f +BEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz +LmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK +QEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG +SM49BAMCA0cAMEQCIB9C8wOAN/ImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj +ftbrNGsGU8YH211dRiYNoPPu19Zp/ze8JmhujB0oBw== \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_issuer_chain.pem b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_issuer_chain.pem new file mode 100644 index 0000000000..396841a8b8 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/qe_identity_issuer_chain.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG +A1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw +b3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD +VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv +P+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju +ypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f +BEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz +LmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK +QEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG +SM49BAMCA0cAMEQCIB9C8wOAN/ImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj +ftbrNGsGU8YH211dRiYNoPPu19Zp/ze8JmhujB0oBw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG +A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 +aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT +AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 +1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB +uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ +MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 +ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV +Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI +KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg +AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/root_ca_crl.der b/pallets/teebag/src/sgx_verify/test/dcap/root_ca_crl.der new file mode 100644 index 0000000000..0c1b57ca43 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/root_ca_crl.der @@ -0,0 +1 @@ +308201213081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3232303431393038333131385a170d3233303431393038333131385aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020348003045022100b7805acf592113584c45c8b0e11b2b8a9db462a215bbf8d4fd416539d7f5ab7502207ff56984c5199cf2b23d97d37b104ec0ebb5243674f41346887a6bdfbfdfeb42 \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/tcb_info.json b/pallets/teebag/src/sgx_verify/test/dcap/tcb_info.json new file mode 100644 index 0000000000..0bf74af582 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/tcb_info.json @@ -0,0 +1 @@ +{"tcbInfo":{"id":"SGX","version":3,"issueDate":"2022-11-17T12:45:32Z","nextUpdate":"2023-04-16T12:45:32Z","fmspc":"00906EA10000","pceId":"0000","tcbType":0,"tcbEvaluationDataNumber":12,"tcbLevels":[{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"SWHardeningNeeded","advisoryIDs":["INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"ConfigurationAndSWHardeningNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00477","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":15},{"svn":15},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":15},{"svn":15},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":14},{"svn":14},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":14},{"svn":14},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":13},{"svn":13},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":3},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":13},{"svn":13},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":6},{"svn":6},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":6},{"svn":6},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00233","INTEL-SA-00161","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":4},{"svn":4},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":5},"tcbDate":"2018-01-04T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":2},{"svn":2},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":4},"tcbDate":"2017-07-26T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00088","INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]}]},"signature":"71746f2148ecba04e35cf1ac77a7e6267ce99f6781c1031f724bb5bd94b8c1b6e4c07c01dc151692aa75be80dfba7350bb80c58314a6975189597e28e9bbc75c"} \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/dcap/tcb_info_issuer_chain.pem b/pallets/teebag/src/sgx_verify/test/dcap/tcb_info_issuer_chain.pem new file mode 100644 index 0000000000..396841a8b8 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/dcap/tcb_info_issuer_chain.pem @@ -0,0 +1,32 @@ +-----BEGIN CERTIFICATE----- +MIICizCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNTAxMFoXDTI1MDUyMTEwNTAxMFowbDEeMBwG +A1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw +b3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD +VQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv +P+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju +ypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f +BEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz +LmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK +QEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG +SM49BAMCA0cAMEQCIB9C8wOAN/ImxDtGACV246KcqjagZOR0kyctyBrsGGJVAiAj +ftbrNGsGU8YH211dRiYNoPPu19Zp/ze8JmhujB0oBw== +-----END CERTIFICATE----- +-----BEGIN CERTIFICATE----- +MIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw +aDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv +cnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ +BgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG +A1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0 +aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT +AlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7 +1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB +uzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ +MEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50 +ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV +Ur9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI +KoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg +AiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI= +-----END CERTIFICATE----- \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST4.bin b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST4.bin new file mode 100644 index 0000000000..1d3eba0f34 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST4.bin @@ -0,0 +1,2 @@ +˹E݁}R&ln6 + \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST5.bin b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST5.bin new file mode 100644 index 0000000000..1d3eba0f34 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST5.bin @@ -0,0 +1,2 @@ +˹E݁}R&ln6 + \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST6.bin b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST6.bin new file mode 100644 index 0000000000..4ccf832d61 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST6.bin @@ -0,0 +1 @@ + GҬʮ;X4\Э \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST7.bin b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST7.bin new file mode 100644 index 0000000000..d43d92f82b --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST7.bin @@ -0,0 +1 @@ +r\٪=(Ud.ږtΤBW \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST8-PRODUCTION.bin b/pallets/teebag/src/sgx_verify/test/enclave-signing-pubkey-TEST8-PRODUCTION.bin new file mode 100644 index 0000000000000000000000000000000000000000..536943ebf3985177f695bd1dce85f0f2013cd19e GIT binary patch literal 32 qcmV+*0N?*Muz$jf3Hc4#Y6g5u0BsV=%J6*F>;L&LhpzUg1qR>;a}f3b literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST4.der b/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST4.der new file mode 100644 index 0000000000000000000000000000000000000000..2e775236d6c14d5dc93b400a44d4d0a2ab195243 GIT binary patch literal 3234 zcmd5O%uFVkC6i3X zigJZgsz^Z*L0qs8E+`Zfd~6c`*RHr%uiSh0m&F;DhKR;Gx@Y?|swl(fKByujjk405fp^wL34e zT)y;rx+|CNyu#ga^(W8$4Sf1rf1tC69(nGyKmPVc@bD|IUPqKyBEEXLdZE1Q=Rf-F z*Z%eDS8s}+e*x0ZH&0#{e)rh>|MbO|zw+n5)Ia<25AJ*M);%Y1>z<~6cGuqZiD#8d z&%VZ8y6@_(d*V-?Et0bvXBfudB#F~Bj?x%TVib+hC?F^RNRnVE6bBSW)6Ci9XZ2z| z-`C-KbM6H&@B|>9z$gbrZ=lo-jC>L&ScU|>VKd+D7gc$71AqK%v2VmOF2ZJR9jbah zsl)oP?*s6rA*r%!Lq|1@j~k*Wh(hnWDZ)Cfc5AgvE3T-b-V!DIT!13M`Cu-j2*h4T z0JNXd7n=VT+{N^^1n1sw85|Ty$YHD?5duTvB!LnlEfS<4@*KuPoZ?9ar5TbXP*kD- z$Fc+?auSD%Jr0K82wFfc!4~&bdEE;o|@%5i262@ z{~ws~PDigK%Jq}X$EmzwC&XknPm$1z#tS^k{Ed;VM%Bi3mhS-V*{7ZvCiZZOZEz8x z=ByH`=w!sJAu>4R(Vh%r3Dfx#?xtM=qf!scjFQlev>Ak~G#D$mVlbJO#tJ9z6@0PY zX=922+K3r+ei>}gvgtdc^E@#gt?}NpKtQ}8W=qs?_7dA^pg48A=tzxAer(j5n*>DX zv2F?``%{eh3%zFZeL%@Ru24ghSn69B_a_4Cbc<8VYz$v2{ML0!HgndI$SsBZBxs@@ z;9ib*h>UNmad^}=O{dk0h0wf(7e+$LTeAxhLF4J#5mqozDo)r6C4!{UF`2a^`*30~ zq1AX9*e1DKKq@bHv=<#}lER$9&>H$xx}J8jUKqJG@=U>>;@MIQq-r(LGPCfkn>U}) zRaLAUo`>x^gRRQJOdUc)RFVR870pj|2xNY;l_zS13zo?7tqXO9IPxbrF{2i02#|%`(fkLEsx;=G&VHBQkgGwGlo9s$^^e zjNC-)IJ1D*DtT<^&g`f=YNrdUld*J(>A7xsp=;mJEjjuvHOOK+^xTuE>2P&XL%%_M zAMYp$aB>@Wtn;Xm_n7h?_kQLNbKS058D8KW37qsFyh4xb99y2qMH4N>TIcr{F=5-T zCQ}jw(>8NKGE+qf^!sp1xlIAhb{JmSm}BK4K&2A<9>yY0GBmb}ZwgIjq$rZjggxqMhH6pxK7w zMnrj=&zn;+K?0H6T4d!lnltXnE-qqgS#jEFHZNeyoo)wA$>~@yowcQ3wzIhj6>l(QYcq%Hy0zPpgs@}>_g-UoXq;?~svzofdRvgI2zjNea;|gC`B^4vYt3{ngp!gAb39z9yOXuU zN!}Rj(Ns7n&=VOQ5*M~YD{@HNA5g2xtpN3NJ}I`&!ITkUe=+JZys%SRJJ6-L^+L|6 zBw2Q_93%>)w2lRFdCn7W6=PnPtczvf>*xG9=h9(@v9*R7%|*YIR%4+6 zI|xSNfL@URF^}$kJ!QhMF;AliEchd!XBN9VEy&WP@md$LPPVmEKJgaP9?>>Z$QBz* zqlOw9TJ2h}X~H?q`XuUtObL~Ah853?TU$A?=xQTVZqt=mt*uM7zYNAA(af5uwyS+V zmAJ`dED$g)=>d-TZkE9!rYqiAoXj+eYEvp^kjThUJj*8N5DVa3x3*D#mG9>PABRB@ zloVMf+xeVv{h$zZ2`Wz1=ArrbRj}bJ@+yTuG#?&g>Cd8)dB1)YyBYqeiQ+?R1 zs$F83nd!R6G8rr40;wa!r4QOtGqW+gdGoD54_D+nfN=Ry1^Zlp3{oPgfo~OO&`bR$WpnCby>n~0I-aNc`@s7{^_4og1k<9}?{V@B|ue9f% O`rhE9ALHNm<$nV!>ga#~ literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST5.der b/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST5.der new file mode 100644 index 0000000000000000000000000000000000000000..bf5fe0cf0d7b10b97d63a1d887bdb29c0ab329d9 GIT binary patch literal 3234 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 SZ~W!0zy9$07v9+YVe(HRSnz59 literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST6.der b/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST6.der new file mode 100644 index 0000000000000000000000000000000000000000..889e5e0a850788545e80e93d82a0b4d0d2055b1c GIT binary patch literal 3233 zcmds4+so`$74P?*!)a|#AGB5-;JxOeW8WM-0?B$Ei%$z?K^Ofs3t zB$Kg%9N|P!iDZX;;{OtVv>J11bE^e?4$3&o=&!lWRvV>8SuelHy*q? z<^1g3Dq+ zeDbyQ_TBIQY4z*u;#*(+)z@Eq_2MIM{N%51ef}RmedT%ahj$)n!JS7A-BaH9(3kG2 zXLtXG&K|vf=aKNsx0dm(+qXEL11v+}1Wp2;rWlsx8IEB|mZDj}0i57Sz_GWUy;UtY z$-bIxwjGE71b7Zm&k@W7$8QtlZJPWn3E0lJbF7oyep%$VZj;a6TJCEpkFr^vScjrY z#?`Dk?7Izw;Xu|D$A%MaICy>_Nunfn&}}xW=EZKK5X4##>0`q#NKP>4!~Bwy|35Jg zrYhLa-9!9J|kJgU6T`>p7;@x_^c0H3N?XAlh?R*{QanYFSuqh{- zWn5D2VksxO(^la!rBNMjPEl%c?w};OV=1hcr@p-0R(d#}Vk@&4 zhIu(E+Y{zBSeTMZnfD2@&p;t|GfQ6(6iB2zQfd*$MYNHQ$_o1T@P#QkS-0lyHEB>s zIjW{n)|jDV&9j8A~k*2IZ1{I*H<}o1PR@K zM_Gj9dvQmmfDaa3Op7k=y+7?QOuJ|lconZMBKhC(id|fnu`OaHtHYI4nZo{RPuMi; zu}6czYf=Zqz9z|_zUKJ+7#IbR}Km(Ts zkq0SxO6om{N5}veJEeCkI@i)EW@GpvTk;nR6JQb;|4l@>=ec9;zDc3RV*E@z`! z!UbDc)Teli1rjPPx^Qb_G73mnT1M6?Yb!k+@R5-p!=^gahNH7dYmerA4_oi!cz7Cd zE<*XT$o$+EL}Ilw?SRXXNd%tXz~@)a8iVNF+{MpGIQnwR~g+9rZ?PJ$~4O$A>0sf`_&a@4ppG$~|7 znGg#QtB^5T9z^+NpSbIYaNBs3t%9j}*&mm^bnpqjGKfKabzgF;w(!1b-~-hYcoeI4 z7LoS7Ud-O@ulsi<7OgS8iZ)FnT7F+I0&P)8gRYkhWI-`a6$F(?s}@zH^O|d)sGVK* z0$u8{YZ_p~qqE<5+-z14PkIQJ!V$2(#qUl_I(JyIG9{u_Y~!>TyGwbG8C!YAXIsl) z`UXC;nvH0))a2-QhiFTBPHFU{UkHL!+UkkN*IR{g>NY1TW0Py$$6zE;wO@P2u9{Bg zG8&IZA_eC;+as~5lcq3>*g}{rPs4c_Rzo@SA(hg4O@kVoFiMa?~g0 z`-JzI#HymXv@l2OjXKo;z901TW)yBA$@h-K{kU2!M*INx@flHB&@Fncn;NN0sLh@w z

mHBd}aKl3=eUW_EYe6Ecv+RBY2lgyHkl>9Im@#H_(W!*m2MF5ds|a7}pti04n=H216TzWuc?Y#&tjPwk(1 z?g!stKmDWMuip9bORsWAfztJ0uZoZ1rmiyR7gDh|5K)gO~sZg`S{-B z^PPLnx!<`_z^yA^2Dh#V&z)SnaPi`W3*f%H-}#|ul%L(abaD|qj9mrqy8FbwbaLgs z(azsi8*eO%;M%+I#{nR58pCjgz})jCgHvxT!K2tC;C;7Y54{Py9<7roi#A>gX5i}G z_nhx?@xmM1om{x*1l@J%kzd{Q#TU9Cc=gjSe}#IC{r*ee`R+>}z4ys?px=G_BlWc( z|Ec)fE8lqVbFV${i)UZHa`Dd(JpKEhzWUjd`+s=j`VU@s{cEr6U-|V{Z(TlsTbFn3 zQ{H{=C!SF*Jo7rbaQ~%Sm;EPC7vbrRQ=H;(jKx@n1S~^gB+ark#c?>maf~7vf+lG| zbEl7=R*Nj!R$jI~0|6X70mvtCdW>N=aOMU9KFpCU!7;629c{LYB0s%BJbt>^)?yx{ zULBdcqKZb9SM9c~1Absgs%%?utPYHi8=@$PLJM7|UNtW^nVQcFlooz#iBaudfTqCN zV9%up#8yWFtX(qan*S8&e0f`fGxS>qf`W)MG$bXO2L!++0C-;D36f(OT%u`!a6~XH z23bjDd72Ul0VgGa6rqTb7)6OBB}x<^C5C|lqFBg58gIklC7xg?p2a1pHDNHeWdp}B zXFiaGGXP;oWJnHgtJ~)JUu8i>cxY3FF@&%Wv{trH(Z1R-7T_m3<`0acm=N@HW#ry3y*2T>t}<|3^{17i$~#v3t;CDT!k!3--k z&tG9yPs!Fn&#sUxERdO5sa(2}j4iZLxQbK|6D1FaE5S86l+rp=Y$UmeYSWl%ddD9% z!YZ0hddnm)JMm#*ST62}?JXH}hq`v`))WnQ32hN$dNoOQczmo^+hPcJfgbdogS4@x z^Kw_>-c)g9cGQ7~o_f&3-CDvBQD=5yBYZDz>JAqdc-XNeB?HqnK7tF_Yph_i^);MG z#vY`!BctIUJ8ULdxl{%v*A4SJ6e-F;DEIAh$nR2a98J-3Uga9ME|yc( zYG!-$=FO*cRTYad&%-8*VWT1#t37ClN|=H}!SE9u0+}C`@<{aw!4whS*icu%?O7T_|m!wD9Epeidh4SKM|H>{Ncl zk8KMBG}JKB2qx_h$du@JL=GVhbeSo&SyH(gPrFtZ+xf|=KWVslUJhlVAHhlugSxvL z588gODNvW*uof}i;*o;z>v{JDeiAOee%}AO%vp8r* z^Ma#`kq(c}hH-#KDWtndLX1CQ6&ewBoA!VjRc-{RpYchtbOh(D2$vA*GCa3a z8q3wCpm97jE+knV;|T~ANNWcU1o;_HoK=83hcHW*?o>bH#~GJ)Gn}h5+^Em{B{ypm z>**S1C=;GTp<<;0Vcq6p`Ub!5^h_w2BWf9BO(U4IUcYeFqz;UhOBynv5XUlta-dd8 z74$jPv5w@%Dm!jp>@X_|bO(ETn@w2Hs|Uxv4;K6$Ff)_e92Zn>Geo6}_(8U`V={6U z(zdIuC67zjrbhQP%rl!zuo!Z@pS7E)3u;cP)VNphyjWVwk;7JNnYQagj#pZitLQHOx&{^(u&`kUt#p(4i$i`EDxm_^I zj6Q}GF)OJ)PLlIRQY@2=1_-DV)FFp+osl94*KVE8%d|g8wZPTz-NDI@*6LtUhY5r4 z3NDM1DM|8+VGo~gBW^}VW);lEL?5m)WvT*fYjpa}&|gD>>+A>HQMF8loPo_S53fw< z6rF<;Yq5hX&6Xjg02D;SGi*5&VW-ADtL^lNa3vuY4sjB6v3YEF7``*4SAH3i>*CV)m{J%2yfd-bZ=gvKBq*!DgThl;r9R({uJcG^-K#6x)V=3DYhU{3V^_cRXm#&H_;3D7 aJ^crH_N@B+KQ>q2^W3A`_rCvAFa8TV?(Td5 literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST8_PRODUCTION.der b/pallets/teebag/src/sgx_verify/test/ra_dump_cert_TEST8_PRODUCTION.der new file mode 100644 index 0000000000000000000000000000000000000000..8a8144f445b46369e36195b5dfcca825160c2531 GIT binary patch literal 3385 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 C5%!G$ literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER2_MRENCLAVE2.der b/pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER2_MRENCLAVE2.der new file mode 100644 index 0000000000000000000000000000000000000000..e05c35e561456a673481764edb4c999a92ca88b2 GIT binary patch literal 3232 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 literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER3_MRENCLAVE2.der b/pallets/teebag/src/sgx_verify/test/test_ra_cert_MRSIGNER3_MRENCLAVE2.der new file mode 100644 index 0000000000000000000000000000000000000000..5ff453b3cca08deac2b8c3a93c1ad5f765d83448 GIT binary patch literal 3231 zcmds4TgdE29q)I};Z)nxinOJaqOG6!(rw+zJvq>a$vvB7cau$aH}P`JJ-eIBUb4w% zdwDqE!GelKsE9ApKJ>xMlUSjsZ&nZrD)?qmK~MzgLv5>+V!rRx)9Q=(>V%oh{AYgi zzx`%H!1XKN0@ts=Z(qD{a^b?s3Ap3(haR~hzxw#4ixBk9ypm^ zS%3Yr=>6Y#=BHfn^Si(I67%eLpZMY1UwiA#-5>7z$nUScaQ)62T)%VM)#CDpzi>l7 zx$!o8a@VEncV2tqbP=9jJtY_n0VqxoG)?0KiI6Bw(F|Znj3Fo*B~Tgz2yy!8X|>3s zUFGHL4up2zJ_gXo0AnHeRRXr|5Bd;E3hUm@uzT8yVvF(~W%y5UL+5O&j zc8a**4}=skYJ14v$NaY*r*QZrny0j=&%v-CWtQ4^hT?I7BWjPOLSw#>Qc?f5En53~1Q8XoJ*jB9c@gSQNHh1KbxuY#{)T%ZVPuBzfs7GM6I5Z-u z=CM&Vd$pDhyTnfxUSH%)DR+IFKuuZ-F=SH4I3q;FT9sJS8ZQqiIv;9kzw%;@52eC6 zrdm*8AJX>lBE@&q;t3ln7khk_ht zpe-qGrb8fclZ`Y{e2g~)mTPRN%lLsi!U3BMrz~_Vm`yF+*fpHt@KeK3csSqi7_{UJ0AjPZmxY4Pwaf!_}Bm_;&APv#M;k;!~jAgr@>ISoP%yD ztoi&LKLPrrq(8b{x zN;myi#6cEFOdEDSd$Vxw&4{AQ7ppAckWE&bbhVI}2C&c&#O^}nph3g4c`i}Az5qA# zvcgC1ew~_hu@A*96c?U!IINP~>kGR+3wxCxah7c&fP@+%41*cO#W|+-1cqfb(5L3a zc1aXUGVhyxWbdadb=ELRyctWF8o^2lgSx-BMo2ra2~cmmaV;R6$))u%ob+9R-55kU ztu<@xVpCxe7|X1!)MUs6T5<53YF}%%N=2!A^P+L05tOIB8B>i?sN{{=sA8WXTDY4wA&P8USs$>0BvW9cd5KF|Ra^CX zn^B+asc=5ntBt!^nwd~dgd?4}MkK~lCkdKKTry-i(cy8rrKUh5rY>CW{tew=Y>@1RVjp~L$8yc9-lAOl@PhVqPK*ibgJ05flLF?)|+x6Vw+ z8xvv~3#CA*(kf6HWojPrt-0yBs?eiW1n7?r_AZ~% zo>z~Kst*?20Z@*?Y>x|~uqmw41+y zvLIqyA*?~kal*!wj|{R}OQc=51zKr&p>&tQSiozio@v`^K95CqG8ywYj0>uV_2+hy zz$~Cj&RQHtvF}$SF>@fE5NhBg6J&t8Fw%{UZ-?$Ka=E~BUAG{KD%?a7ZO`3|uMeO% z3`amSd&F97d&|8Jnwh^R7`;9Qxi$Bfy9FgXnBCn+6??SfI?<9zKqB zN{k>}yLA?CGIf+`fvchWcA6i}RcjLMj6(M%lSk=oP@hX?+ z3P5(lp4yE4HN=?SVYHi6%XG{P5eM~HnWA#DqcAZ5CDw2hy%`IzSEHWU zo%9HEMLyx%Bn|pVoY*~z>y62kzX|bm+4`h1qg8I;A7Xt_ugYyW93>;$J`~|t78d== z@7vT~TWX_Z+avnmM;e(u@}l?JBjEwh W0K$Yu-;g|KOVI$sR&!?0#cUb+P#>-U literal 0 HcmV?d00001 diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin b/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin new file mode 100644 index 0000000000..5009698a26 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin @@ -0,0 +1 @@ +?_ΰoc@Re tj@V=Mk gQs]4AXc \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin b/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin new file mode 100644 index 0000000000..df8e511e12 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/test_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin @@ -0,0 +1,2 @@ +(rORpDso.QV>ojk s( IJp4O}eVp +z~Q7 \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin new file mode 100644 index 0000000000..65c2e61c25 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin @@ -0,0 +1 @@ +"ZTKd pIļ@C_^< \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin new file mode 100644 index 0000000000..8cc7d123bf --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin @@ -0,0 +1 @@ ++%Q!ڜeV{3 A/ 4u3*;8 \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin new file mode 100644 index 0000000000..19c4874d4b --- /dev/null +++ b/pallets/teebag/src/sgx_verify/test/test_ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin @@ -0,0 +1 @@ +7^䠺,(j|tv 댓ҡ \ No newline at end of file diff --git a/pallets/teebag/src/sgx_verify/tests.rs b/pallets/teebag/src/sgx_verify/tests.rs new file mode 100644 index 0000000000..e8a9e4a560 --- /dev/null +++ b/pallets/teebag/src/sgx_verify/tests.rs @@ -0,0 +1,298 @@ +#![allow(dead_code, unused_imports, const_item_mutation)] + +use super::{ + collateral::{EnclaveIdentitySigned, TcbInfoSigned}, + *, +}; +use codec::Decode; +use frame_support::assert_err; +use hex_literal::hex; + +// reproduce with "litentry-worker dump_ra" +const TEST1_CERT: &[u8] = include_bytes!("./test/test_ra_cert_MRSIGNER1_MRENCLAVE1.der"); +const TEST2_CERT: &[u8] = include_bytes!("./test/test_ra_cert_MRSIGNER2_MRENCLAVE2.der"); +const TEST3_CERT: &[u8] = include_bytes!("./test/test_ra_cert_MRSIGNER3_MRENCLAVE2.der"); +const TEST4_CERT: &[u8] = include_bytes!("./test/ra_dump_cert_TEST4.der"); +const TEST5_CERT: &[u8] = include_bytes!("./test/ra_dump_cert_TEST5.der"); +const TEST6_CERT: &[u8] = include_bytes!("./test/ra_dump_cert_TEST6.der"); +const TEST7_CERT: &[u8] = include_bytes!("./test/ra_dump_cert_TEST7.der"); +const TEST8_CERT: &[u8] = include_bytes!("./test/ra_dump_cert_TEST8_PRODUCTION.der"); + +const TEST1_SIGNER_ATTN: &[u8] = + include_bytes!("./test/test_ra_signer_attn_MRSIGNER1_MRENCLAVE1.bin"); +const TEST2_SIGNER_ATTN: &[u8] = + include_bytes!("./test/test_ra_signer_attn_MRSIGNER2_MRENCLAVE2.bin"); +const TEST3_SIGNER_ATTN: &[u8] = + include_bytes!("./test/test_ra_signer_attn_MRSIGNER3_MRENCLAVE2.bin"); + +// reproduce with "litentry-worker signing-key" +const TEST1_SIGNER_PUB: &[u8] = + include_bytes!("./test/test_ra_signer_pubkey_MRSIGNER1_MRENCLAVE1.bin"); +const TEST2_SIGNER_PUB: &[u8] = + include_bytes!("./test/test_ra_signer_pubkey_MRSIGNER2_MRENCLAVE2.bin"); +const TEST3_SIGNER_PUB: &[u8] = + include_bytes!("./test/test_ra_signer_pubkey_MRSIGNER3_MRENCLAVE2.bin"); +const TEST4_SIGNER_PUB: &[u8] = include_bytes!("./test/enclave-signing-pubkey-TEST4.bin"); +// equal to TEST4! +const TEST5_SIGNER_PUB: &[u8] = include_bytes!("./test/enclave-signing-pubkey-TEST5.bin"); +const TEST6_SIGNER_PUB: &[u8] = include_bytes!("./test/enclave-signing-pubkey-TEST6.bin"); +const TEST7_SIGNER_PUB: &[u8] = include_bytes!("./test/enclave-signing-pubkey-TEST7.bin"); +const QE_IDENTITY_CERT: &str = include_str!("./test/dcap/qe_identity_cert.pem"); +const DCAP_QUOTE_CERT: &str = include_str!("./test/dcap/dcap_quote_cert.der"); +const PCK_CRL: &[u8] = include_bytes!("./test/dcap/pck_crl.der"); + +// reproduce with "make mrenclave" in worker repo root +const TEST1_MRENCLAVE: &[u8] = &[ + 62, 252, 187, 232, 60, 135, 108, 204, 87, 78, 35, 169, 241, 237, 106, 217, 251, 241, 99, 189, + 138, 157, 86, 136, 77, 91, 93, 23, 192, 104, 140, 167, +]; +const TEST2_MRENCLAVE: &[u8] = &[ + 4, 190, 230, 132, 211, 129, 59, 237, 101, 78, 55, 174, 144, 177, 91, 134, 1, 240, 27, 174, 81, + 139, 8, 22, 32, 241, 228, 103, 189, 43, 44, 102, +]; +const TEST3_MRENCLAVE: &[u8] = &[ + 4, 190, 230, 132, 211, 129, 59, 237, 101, 78, 55, 174, 144, 177, 91, 134, 1, 240, 27, 174, 81, + 139, 8, 22, 32, 241, 228, 103, 189, 43, 44, 102, +]; + +// MRSIGNER is 83d719e77deaca1470f6baf62a4d774303c899db69020f9c70ee1dfc08c7ce9e +const TEST4_MRENCLAVE: MrEnclave = + hex!("7a3454ec8f42e265cb5be7dfd111e1d95ac6076ed82a0948b2e2a45cf17b62a0"); +const TEST5_MRENCLAVE: MrEnclave = + hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d1"); +// equal to TEST5! +const TEST6_MRENCLAVE: MrEnclave = + hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d1"); +// equal to TEST6! +const TEST7_MRENCLAVE: MrEnclave = + hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d1"); + +// production mode +// MRSIGNER is 117f95f65f06afb5764b572156b8b525c6230db7d6b1c94e8ebdb7fba068f4e8 +const TEST8_MRENCLAVE: MrEnclave = + hex!("bcf66abfc6b3ef259e9ecfe4cf8df667a7f5a546525dee16822741b38f6e6050"); + +// unix epoch. must be later than this +const TEST1_TIMESTAMP: i64 = 1580587262i64; +/// Collateral test data mus be valid at this time (2022-10-11 14:01:02) for the tests to work +const COLLATERAL_VERIFICATION_TIMESTAMP: u64 = 1665489662000; + +//const CERT: &[u8] = +// b"0\x82\x0c\x8c0\x82\x0c2\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020\ +// x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0\x1e\x17\r190617124609Z\x17\r190915124609Z0\x121\ +// x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\ +// x03\x01\x07\x03B\0\x04RT\x16\x16 +// \xef_\xd8\xe7\xc3\xb7\x03\x1d\xd6:\x1fF\xe3\xf2b!\xa9/\x8b\xd4\x82\x8f\xd1\xff[\x9c\x97\xbc\xf27\ +// xb8,L\x8a\x01\xb0r;;\xa9\x83\xdc\x86\x9f\x1d%y\xf4;I\xe4Y\xc80'$K[\xd6\xa3\x82\x0bw0\x82\x0bs0\ +// x82\x0bo\x06\t`\x86H\x01\x86\xf8B\x01\r\x04\x82\x0b`{\"id\":\" +// 117077750682263877593646412006783680848\",\"timestamp\":\"2019-06-17T12:46:04.002066\",\"version\ +// ":3,\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\",\"platformInfoBlob\":\" +// 1502006504000900000909020401800000000000000000000008000009000000020000000000000B401A355B313FC939B4F48A54349C914A32A3AE2C4871BFABF22E960C55635869FC66293A3D9B2D58ED96CA620B65D669A444C80291314EF691E896F664317CF80C\ +// ",\"isvEnclaveQuoteBody\":\"AgAAAEALAAAIAAcAAAAAAOE6wgoHKsZsnVWSrsWX9kky0kWt9K4xcan0fQ996Ct+CAj// +// wGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHAAAAAAAAAFJJYIbPVot9NzRCjW2z9+k+9K8BsHQKzVMEHOR14hNbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD1xnnferKFHD2uvYqTXdDA8iZ22kCD5xw7h38CMfOngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSVBYWIO9f2OfDtwMd1jofRuPyYiGpL4vUgo/ +// R/1ucl7zyN7gsTIoBsHI7O6mD3IafHSV59DtJ5FnIMCckS1vW\"}|EbPFH/ThUaS/ +// dMZoDKC5EgmdUXUORFtQzF49Umi1P55oeESreJaUvmA0sg/ +// ATSTn5t2e+e6ZoBQIUbLHjcWLMLzK4pJJUeHhok7EfVgoQ378i+eGR9v7ICNDGX7a1rroOe0s1OKxwo/ +// 0hid2KWvtAUBvf1BDkqlHy025IOiXWhXFLkb/ +// qQwUZDWzrV4dooMfX5hfqJPi1q9s18SsdLPmhrGBheh9keazeCR9hiLhRO9TbnVgR9zJk43SPXW+pHkbNigW+2STpVAi5ugWaSwBOdK11ZjaEU1paVIpxQnlW1D6dj1Zc3LibMH+ly9ZGrbYtuJks4eRnjPhroPXxlJWpQ==|MIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIwMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1NhbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwkSW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/ +// Es/BA+tbeCTUR106AL1ENcWA4FX3K+E9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtIdcv/ +// uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuvLUK7eyRPfJW/ +// ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV+W9tOhAImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt++qO/ +// 6+KAXJuKwZqjRlEtSEz8gZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGhMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN+s1fDuHAVE8MA4GA1UdDwEB/ +// wQEAwIGwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJlcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4rRq+ZKE+7k50/ +// OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/ +// LpFa9lpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYvWLrtXXfFBSSPD4Afn7+3/ +// XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUdZseZCcaZZZn65tdqee8UXZlDvx0+NdO0LR+5pFy+juM0wWbu59MvzcmTXbjsi7HY6zd53Yq5K244fwFHRQ8eOB0IWB+4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW72uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN+KwPbpA39+xOsStjhP9N1Y1a2tQAVo+yVgLgV2Hws73Fc0o3wC78qPEA+v2aRs/ +// Be3ZFDgDyghc/1fgU+7C+P6kbqd4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA==0\n\x06\x08*\x86H\ +// xce=\x04\x03\x02\x03H\00E\x02!\0\xae6\x06\t@Sy\x8f\x8ec\x9d\xdci^Ex*\x92}\xdcG\x15A\x97\xd7\xd7\ +// xd1\xccx\xe0\x1e\x08\x02 +// \x15Q\xa0BT\xde'~\xec\xbd\x027\xd3\xd8\x83\xf7\xe6Z\xc5H\xb4D\xf7\xe2\r\xa7\xe4^f\x10\x85p"; +const CERT_FAKE_QUOTE_STATUS: &[u8] = b"0\x82\x0c\x8c0\x82\x0c2\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0\x1e\x17\r190617124609Z\x17\r190915124609Z0\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x03B\0\x04RT\x16\x16 \xef_\xd8\xe7\xc3\xb7\x03\x1d\xd6:\x1fF\xe3\xf2b!\xa9/\x8b\xd4\x82\x8f\xd1\xff[\x9c\x97\xbc\xf27\xb8,L\x8a\x01\xb0r;;\xa9\x83\xdc\x86\x9f\x1d%y\xf4;I\xe4Y\xc80'$K[\xd6\xa3\x82\x0bw0\x82\x0bs0\x82\x0bo\x06\t`\x86H\x01\x86\xf8B\x01\r\x04\x82\x0b`{\"id\":\"117077750682263877593646412006783680848\",\"timestamp\":\"2019-06-17T12:46:04.002066\",\"version\":3,\"isvEnclaveQuoteStatus\":\"OK\",\"platformInfoBlob\":\"1602006504000900000909020401800000000000000000000008000009000000020000000000000B401A355B313FC939B4F48A54349C914A32A3AE2C4871BFABF22E960C55635869FC66293A3D9B2D58ED96CA620B65D669A444C80291314EF691E896F664317CF80C\",\"isvEnclaveQuoteBody\":\"AgAAAEALAAAIAAcAAAAAAOE6wgoHKsZsnVWSrsWX9kky0kWt9K4xcan0fQ996Ct+CAj//wGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHAAAAAAAAAFJJYIbPVot9NzRCjW2z9+k+9K8BsHQKzVMEHOR14hNbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD1xnnferKFHD2uvYqTXdDA8iZ22kCD5xw7h38CMfOngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSVBYWIO9f2OfDtwMd1jofRuPyYiGpL4vUgo/R/1ucl7zyN7gsTIoBsHI7O6mD3IafHSV59DtJ5FnIMCckS1vW\"}|EbPFH/ThUaS/dMZoDKC5EgmdUXUORFtQzF49Umi1P55oeESreJaUvmA0sg/ATSTn5t2e+e6ZoBQIUbLHjcWLMLzK4pJJUeHhok7EfVgoQ378i+eGR9v7ICNDGX7a1rroOe0s1OKxwo/0hid2KWvtAUBvf1BDkqlHy025IOiXWhXFLkb/qQwUZDWzrV4dooMfX5hfqJPi1q9s18SsdLPmhrGBheh9keazeCR9hiLhRO9TbnVgR9zJk43SPXW+pHkbNigW+2STpVAi5ugWaSwBOdK11ZjaEU1paVIpxQnlW1D6dj1Zc3LibMH+ly9ZGrbYtuJks4eRnjPhroPXxlJWpQ==|MIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIwMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1NhbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwkSW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA+tbeCTUR106AL1ENcWA4FX3K+E9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtIdcv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuvLUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV+W9tOhAImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt++qO/6+KAXJuKwZqjRlEtSEz8gZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGhMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN+s1fDuHAVE8MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJlcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4rRq+ZKE+7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9lpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYvWLrtXXfFBSSPD4Afn7+3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUdZseZCcaZZZn65tdqee8UXZlDvx0+NdO0LR+5pFy+juM0wWbu59MvzcmTXbjsi7HY6zd53Yq5K244fwFHRQ8eOB0IWB+4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW72uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN+KwPbpA39+xOsStjhP9N1Y1a2tQAVo+yVgLgV2Hws73Fc0o3wC78qPEA+v2aRs/Be3ZFDgDyghc/1fgU+7C+P6kbqd4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA==0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03H\x000E\x02!\0\xae6\x06\t@Sy\x8f\x8ec\x9d\xdci^Ex*\x92}\xdcG\x15A\x97\xd7\xd7\xd1\xccx\xe0\x1e\x08\x02 \x15Q\xa0BT\xde'~\xec\xbd\x027\xd3\xd8\x83\xf7\xe6Z\xc5H\xb4D\xf7\xe2\r\xa7\xe4^f\x10\x85p"; +const CERT_WRONG_PLATFORM_BLOB: &[u8] = b"0\x82\x0c\x8c0\x82\x0c2\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0\x1e\x17\r190617124609Z\x17\r190915124609Z0\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x03B\0\x04RT\x16\x16 \xef_\xd8\xe7\xc3\xb7\x03\x1d\xd6:\x1fF\xe3\xf2b!\xa9/\x8b\xd4\x82\x8f\xd1\xff[\x9c\x97\xbc\xf27\xb8,L\x8a\x01\xb0r;;\xa9\x83\xdc\x86\x9f\x1d%y\xf4;I\xe4Y\xc80'$K[\xd6\xa3\x82\x0bw0\x82\x0bs0\x82\x0bo\x06\t`\x86H\x01\x86\xf8B\x01\r\x04\x82\x0b`{\"id\":\"117077750682263877593646412006783680848\",\"timestamp\":\"2019-06-17T12:46:04.002066\",\"version\":3,\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\",\"platformInfoBlob\":\"1602006504000900000909020401800000000000000000000008000009000000020000000000000B401A355B313FC939B4F48A54349C914A32A3AE2C4871BFABF22E960C55635869FC66293A3D9B2D58ED96CA620B65D669A444C80291314EF691E896F664317CF80C\",\"isvEnclaveQuoteBody\":\"AgAAAEALAAAIAAcAAAAAAOE6wgoHKsZsnVWSrsWX9kky0kWt9K4xcan0fQ996Ct+CAj//wGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHAAAAAAAAAFJJYIbPVot9NzRCjW2z9+k+9K8BsHQKzVMEHOR14hNbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD1xnnferKFHD2uvYqTXdDA8iZ22kCD5xw7h38CMfOngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSVBYWIO9f2OfDtwMd1jofRuPyYiGpL4vUgo/R/1ucl7zyN7gsTIoBsHI7O6mD3IafHSV59DtJ5FnIMCckS1vW\"}|EbPFH/ThUaS/dMZoDKC5EgmdUXUORFtQzF49Umi1P55oeESreJaUvmA0sg/ATSTn5t2e+e6ZoBQIUbLHjcWLMLzK4pJJUeHhok7EfVgoQ378i+eGR9v7ICNDGX7a1rroOe0s1OKxwo/0hid2KWvtAUBvf1BDkqlHy025IOiXWhXFLkb/qQwUZDWzrV4dooMfX5hfqJPi1q9s18SsdLPmhrGBheh9keazeCR9hiLhRO9TbnVgR9zJk43SPXW+pHkbNigW+2STpVAi5ugWaSwBOdK11ZjaEU1paVIpxQnlW1D6dj1Zc3LibMH+ly9ZGrbYtuJks4eRnjPhroPXxlJWpQ==|MIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIwMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1NhbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwkSW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA+tbeCTUR106AL1ENcWA4FX3K+E9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtIdcv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuvLUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV+W9tOhAImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt++qO/6+KAXJuKwZqjRlEtSEz8gZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGhMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN+s1fDuHAVE8MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJlcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4rRq+ZKE+7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9lpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEqRHz91kpG6Uvyn2tLmnIdJbPE4vYvWLrtXXfFBSSPD4Afn7+3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUdZseZCcaZZZn65tdqee8UXZlDvx0+NdO0LR+5pFy+juM0wWbu59MvzcmTXbjsi7HY6zd53Yq5K244fwFHRQ8eOB0IWB+4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW72uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN+KwPbpA39+xOsStjhP9N1Y1a2tQAVo+yVgLgV2Hws73Fc0o3wC78qPEA+v2aRs/Be3ZFDgDyghc/1fgU+7C+P6kbqd4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA==0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03H\x000E\x02!\0\xae6\x06\t@Sy\x8f\x8ec\x9d\xdci^Ex*\x92}\xdcG\x15A\x97\xd7\xd7\xd1\xccx\xe0\x1e\x08\x02 \x15Q\xa0BT\xde'~\xec\xbd\x027\xd3\xd8\x83\xf7\xe6Z\xc5H\xb4D\xf7\xe2\r\xa7\xe4^f\x10\x85p"; +const CERT_WRONG_SIG: &[u8] = b"0\x82\x0c\x8c0\x82\x0c2\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0\x1e\x17\r190617124609Z\x17\r190915124609Z0\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x03B\0\x04RT\x16\x16 \xef_\xd8\xe7\xc3\xb7\x03\x1d\xd6:\x1fF\xe3\xf2b!\xa9/\x8b\xd4\x82\x8f\xd1\xff[\x9c\x97\xbc\xf27\xb8,L\x8a\x01\xb0r;;\xa9\x83\xdc\x86\x9f\x1d%y\xf4;I\xe4Y\xc80'$K[\xd6\xa3\x82\x0bw0\x82\x0bs0\x82\x0bo\x06\t`\x86H\x01\x86\xf8B\x01\r\x04\x82\x0b`{\"id\":\"117077750682263877593646412006783680848\",\"timestamp\":\"2019-06-17T12:46:04.002066\",\"version\":3,\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\",\"platformInfoBlob\":\"1602006504000900000909020401800000000000000000000008000009000000020000000000000B401A355B313FC939B4F48A54349C914A32A3AE2C4871BFABF22E960C55635869FC66293A3D9B2D58ED96CA620B65D669A444C80291314EF691E896F664317CF80C\",\"isvEnclaveQuoteBody\":\"AgAAAEALAAAIAAcAAAAAAOE6wgoHKsZsnVWSrsWX9kky0kWt9K4xcan0fQ996Ct+CAj//wGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABwAAAAAAAAAHAAAAAAAAAFJJYIbPVot9NzRCjW2z9+k+9K8BsHQKzVMEHOR14hNbAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACD1xnnferKFHD2uvYqTXdDA8iZ22kCD5xw7h38CMfOngAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSVBYWIO9f2OfDtwMd1jofRuPyYiGpL4vUgo/R/1ucl7zyN7gsTIoBsHI7O6mD3IafHSV59DtJ5FnIMCckS1vW\"}|EbPFH/ThUaS/dMZoDKC5EgmdUXUORFtQzF49Umi1P55oeESreJaUvmA0sg/ATSTn5t2e+e6ZoBQIUbLHjcWLMLzK4pJJUeHhok7EfVgoQ378i+eGR9v7ICNDGX7a1rroOe0s1OKxwo/0hid2KWvtAUBvf1BDkqlHy025IOiXWhXFLkb/qQwUZDWzrV4dooMfX5hfqJPi1q9s18SsdLPmhrGBheh9keazeCR9hiLhRO9TbnVgR9zJk43SPXW+pHkbNigW+2STpVAi5ugWaSwBOdK11ZjaEU1paVIpxQnlW1D6dj1Zc3LibMH+ly9ZGrbYtuJks4eRnjPhroPXxlJWpQ==|MIIEoTCCAwmgAwIBAgIJANEHdl0yo7CWMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEUMBIGA1UEBwwLU2FudGEgQ2xhcmExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0aW9uMTAwLgYDVQQDDCdJbnRlbCBTR1ggQXR0ZXN0YXRpb24gUmVwb3J0IFNpZ25pbmcgQ0EwHhcNMTYxMTIyMDkzNjU4WhcNMjYxMTIwMDkzNjU4WjB7MQswCQYDVQQGEwJVUzELMAkGA1UECAwCQ0ExFDASBgNVBAcMC1NhbnRhIENsYXJhMRowGAYDVQQKDBFJbnRlbCBDb3Jwb3JhdGlvbjEtMCsGA1UEAwwkSW50ZWwgU0dYIEF0dGVzdGF0aW9uIFJlcG9ydCBTaWduaW5nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqXot4OZuphR8nudFrAFiaGxxkgma/Es/BA+tbeCTUR106AL1ENcWA4FX3K+E9BBL0/7X5rj5nIgX/R/1ubhkKWw9gfqPG3KeAtIdcv/uTO1yXv50vqaPvE1CRChvzdS/ZEBqQ5oVvLTPZ3VEicQjlytKgN9cLnxbwtuvLUK7eyRPfJW/ksddOzP8VBBniolYnRCD2jrMRZ8nBM2ZWYwnXnwYeOAHV+W9tOhAImwRwKF/95yAsVwd21ryHMJBcGH70qLagZ7Ttyt++qO/6+KAXJuKwZqjRlEtSEz8gZQeFfVYgcwSfo96oSMAzVr7V0L6HSDLRnpb6xxmbPdqNol4tQIDAQABo4GkMIGhMB8GA1UdIwQYMBaAFHhDe3amfrzQr35CN+s1fDuHAVE8MA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMGAGA1UdHwRZMFcwVaBToFGGT2h0dHA6Ly90cnVzdGVkc2VydmljZXMuaW50ZWwuY29tL2NvbnRlbnQvQ1JML1NHWC9BdHRlc3RhdGlvblJlcG9ydFNpZ25pbmdDQS5jcmwwDQYJKoZIhvcNAQELBQADggGBAGcIthtcK9IVRz4rRq+ZKE+7k50/OxUsmW8aavOzKb0iCx07YQ9rzi5nU73tME2yGRLzhSViFs/LpFa9lpQL6JL1aQwmDR74TxYGBAIi5f4I5TJoCCEaRHz91kpG6Uvyn2tLmnIdJbPE4vYvWLrtXXfFBSSPD4Afn7+3/XUggAlc7oCTizOfbbtOFlYA4g5KcYgS1J2ZAeMQqbUdZseZCcaZZZn65tdqee8UXZlDvx0+NdO0LR+5pFy+juM0wWbu59MvzcmTXbjsi7HY6zd53Yq5K244fwFHRQ8eOB0IWB+4PfM7FeAApZvlfqlKOlLcZL2uyVmzRkyR5yW72uo9mehX44CiPJ2fse9Y6eQtcfEhMPkmHXI01sN+KwPbpA39+xOsStjhP9N1Y1a2tQAVo+yVgLgV2Hws73Fc0o3wC78qPEA+v2aRs/Be3ZFDgDyghc/1fgU+7C+P6kbqd4poyb6IW8KCJbxfMJvkordNOgOUUxndPHEi/tb/U7uLjLOgPA==0\n\x06\x08*\x86H\xce=\x04\x03\x02\x03H\x000E\x02!\0\xae6\x06\t@Sy\x8f\x8ec\x9d\xdci^Ex*\x92}\xdcG\x15A\x97\xd7\xd7\xd1\xccx\xe0\x1e\x08\x02 \x15Q\xa0BT\xde'~\xec\xbd\x027\xd3\xd8\x83\xf7\xe6Z\xc5H\xb4D\xf7\xe2\r\xa7\xe4^f\x10\x85p"; +const CERT_TOO_SHORT1: &[u8] = b"0\x82\x0c\x8c0\x82\x0c2\xa0\x03\x02\x01\x02\x02\x01\x010\n\x06\x08*\x86H\xce=\x04\x03\x020\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0\x1e\x17\r190617124609Z\x17\r190915124609Z0\x121\x100\x0e\x06\x03U\x04\x03\x0c\x07MesaTEE0Y0\x13\x06\x07*\x86H\xce=\x02\x01\x06\x08*\x86H\xce=\x03\x01\x07\x03B\0\x04RT\x16\x16 \xef_\xd8\xe7\xc3\xb7\x03\x1d\xd6:\x1fF\xe3\xf2b!\xa9/\x8b\xd4\x82\x8f\xd1\xff[\x9c\x97\xbc\xf27\xb8,L\x8a\x01\xb0r;;\xa9\x83\xdc\x86\x9f\x1d%y\xf4;I\xe4Y\xc80'$K[\xd6\xa3\x82\x0bw0\x82\x0bs0\x82\x0bo\x06\t`\x86H\x01\x86\xf8B\x01\r\x04\x82\x0b`{\"id\":\"117077750682263877593646412006783680848\",\"timestamp\":\"2019-06-17T12:46:04.002066\",\"version\":3,\"isvEnclaveQuoteStatus\":\"GROUP_OUT_OF_DATE\",\"platformInfoBlob\":\"1602006504000900000909020401800000000000000000000008000009000000020000000000000B401A355B313FC939B4F48A54349C91\x03\x02\x03H\x000E\x02!\0\xae6\x06\t@Sy\x8f\x8ec\x9d\xdci^Ex*\x92}\xdcG\x15A\x97\xd7\xd7\xd1\xccx\xe0\x1e\x08\x02 \x15Q\xa0BT\xde'~\xec\xbd\x027\xd3\xd8\x83\xf7\xe6Z\xc5H\xb4D\xf7\xe2\r\xa7\xe4^f\x10\x85p"; +const CERT_TOO_SHORT2: &[u8] = b"0\x82\x0c\x8c0"; + +#[test] +fn verify_ias_report_should_work() { + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + let report = verify_ias_report(TEST4_CERT); + let report = report.unwrap(); + assert_eq!(report.mr_enclave, TEST4_MRENCLAVE); + assert!(report.timestamp >= TEST1_TIMESTAMP.try_into().unwrap()); + assert_eq!(report.pubkey, TEST4_SIGNER_PUB); + //assert_eq!(report.status, SgxStatus::GroupOutOfDate); + assert_eq!(report.status, SgxStatus::ConfigurationNeeded); + assert_eq!(report.build_mode, SgxBuildMode::Debug); +} + +#[test] +fn verify_zero_length_cert_returns_err() { + // CERT empty, argument 2 and 3 are wrong too! + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + assert!(verify_ias_report(&Vec::new()[..]).is_err()) +} + +#[test] +fn verify_wrong_cert_is_err() { + // CERT wrong, argument 2 and 3 are wrong too! + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + assert!(verify_ias_report(CERT_WRONG_PLATFORM_BLOB).is_err()) +} + +#[test] +fn verify_wrong_fake_enclave_quote_is_err() { + // quote wrong, argument 2 and 3 are wrong too! + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + assert!(verify_ias_report(CERT_FAKE_QUOTE_STATUS).is_err()) +} + +#[test] +fn verify_wrong_sig_is_err() { + // sig wrong, argument 2 and 3 are wrong too! + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + assert!(verify_ias_report(CERT_WRONG_SIG).is_err()) +} + +#[test] +fn verify_short_cert_is_err() { + let _signer_attn: [u32; 16] = Decode::decode(&mut TEST1_SIGNER_ATTN).unwrap(); + assert!(verify_ias_report(CERT_TOO_SHORT1).is_err()); + assert!(verify_ias_report(CERT_TOO_SHORT2).is_err()); +} + +#[test] +fn fix_incorrect_handling_of_iterator() { + // In `verify_ias_report` we called `iter.next()` with unwrap three times, which could fail + // for certain invalid reports as the one in this test. This test verifies that the issue + // has been fixed. + // + // For context, see: https://github.com/integritee-network/pallet-teerex/issues/35 + + let report: [u8; 56] = [ + 224, 224, 224, 224, 224, 224, 224, 224, 235, 2, 0, 1, 5, 40, 0, 8, 255, 6, 8, 42, 134, 72, + 206, 61, 3, 1, 7, 0, 2, 183, 64, 48, 48, 0, 1, 10, 23, 3, 6, 9, 96, 134, 72, 1, 134, 248, + 66, 1, 13, 0, 0, 0, 13, 1, 14, 177, + ]; + + assert_err!(verify_ias_report(&report), "Invalid netscape payload"); +} + +#[test] +fn verify_sgx_build_mode_works() { + //verify report from enclave in debug mode + let report = verify_ias_report(TEST4_CERT); + let report = report.unwrap(); + assert_eq!(report.build_mode, SgxBuildMode::Debug); + //verify report from enclave in production mode + let report = verify_ias_report(TEST8_CERT); + let report = report.unwrap(); + assert_eq!(report.build_mode, SgxBuildMode::Production); +} + +#[test] +fn decode_qe_authentication_data() { + assert!(QeAuthenticationData::decode(&mut &[0u8][..]).is_err()); + assert!(QeAuthenticationData::decode(&mut &[1u8][..]).is_err()); + assert_eq!(0, QeAuthenticationData::decode(&mut &[0u8, 0][..]).unwrap().size); + let d = QeAuthenticationData::decode(&mut &[1u8, 0, 5][..]).unwrap(); + assert_eq!(1, d.size); + assert_eq!(5, d.certification_data[0]); +} + +#[test] +fn decode_qe_certification_data() { + assert!(QeCertificationData::decode(&mut &[0u8][..]).is_err()); + assert!(QeCertificationData::decode(&mut &[1u8, 0, 0, 0, 0][..]).is_err()); + assert_eq!(0, QeCertificationData::decode(&mut &[0u8, 0, 0, 0, 0, 0][..]).unwrap().size); + let d = QeCertificationData::decode(&mut &[0u8, 0, 1, 0, 0, 0, 5][..]).unwrap(); + assert_eq!(1, d.size); + assert_eq!(5, d.certification_data[0]); + assert!(QeCertificationData::decode(&mut &[0u8, 0, 2, 0, 0, 0, 5][..]).is_err()); +} + +#[test] +fn deserialize_qe_identity_works() { + let certs = extract_certs(include_bytes!("./test/dcap/qe_identity_issuer_chain.pem")); + let intermediate_slices: Vec = + certs[1..].iter().map(|c| c.as_slice().into()).collect(); + let leaf_cert_der = webpki::types::CertificateDer::from(certs[0].as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der).unwrap(); + verify_certificate_chain(&leaf_cert, &intermediate_slices, COLLATERAL_VERIFICATION_TIMESTAMP) + .unwrap(); + let json: EnclaveIdentitySigned = + serde_json::from_slice(include_bytes!("./test/dcap/qe_identity.json")).unwrap(); + let json_data = serde_json::to_vec(&json.enclave_identity).unwrap(); + let signature = hex::decode(json.signature).unwrap(); + + let e = deserialize_enclave_identity(&json_data, &signature, &leaf_cert).unwrap(); + assert_eq!(1, e.isvprodid); + assert_eq!(5, e.tcb_levels.len()); +} + +#[test] +fn deserialize_tcb_info_works() { + let certs = extract_certs(include_bytes!("./test/dcap/tcb_info_issuer_chain.pem")); + let intermediate_slices: Vec = + certs[1..].iter().map(|c| c.as_slice().into()).collect(); + let leaf_cert_der = webpki::types::CertificateDer::from(certs[0].as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der).unwrap(); + verify_certificate_chain(&leaf_cert, &intermediate_slices, COLLATERAL_VERIFICATION_TIMESTAMP) + .unwrap(); + let json: TcbInfoSigned = + serde_json::from_slice(include_bytes!("./test/dcap/tcb_info.json")).unwrap(); + + let json_data = serde_json::to_vec(&json.tcb_info).unwrap(); + let signature = hex::decode(json.signature).unwrap(); + + let _e = deserialize_tcb_info(&json_data, &signature, &leaf_cert).unwrap(); + assert_eq!(hex!("00906EA10000"), json.tcb_info.fmspc); +} + +#[test] +fn verify_tcb_info_signature() { + let cert = QE_IDENTITY_CERT.replace('\n', ""); + let leaf_cert = base64::decode(cert).unwrap(); + let leaf_cert_der = webpki::types::CertificateDer::from(leaf_cert.as_slice()); + let leaf_cert = webpki::EndEntityCert::try_from(&leaf_cert_der).unwrap(); + let data = br#"{"version":2,"issueDate":"2022-10-18T21:45:02Z","nextUpdate":"2022-11-17T21:45:02Z","fmspc":"00906EA10000","pceId":"0000","tcbType":0,"tcbEvaluationDataNumber":12,"tcbLevels":[{"tcb":{"sgxtcbcomp01svn":17,"sgxtcbcomp02svn":17,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":7,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"SWHardeningNeeded"},{"tcb":{"sgxtcbcomp01svn":17,"sgxtcbcomp02svn":17,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":7,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":17,"sgxtcbcomp02svn":17,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"ConfigurationAndSWHardeningNeeded"},{"tcb":{"sgxtcbcomp01svn":17,"sgxtcbcomp02svn":17,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":15,"sgxtcbcomp02svn":15,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":7,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":15,"sgxtcbcomp02svn":15,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":14,"sgxtcbcomp02svn":14,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":7,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":14,"sgxtcbcomp02svn":14,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":13,"sgxtcbcomp02svn":13,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":3,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":13,"sgxtcbcomp02svn":13,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":6,"sgxtcbcomp02svn":6,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":1,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":6,"sgxtcbcomp02svn":6,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":5,"sgxtcbcomp02svn":5,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":1,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":5,"sgxtcbcomp02svn":5,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":1,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":5,"sgxtcbcomp02svn":5,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":5,"sgxtcbcomp02svn":5,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded"},{"tcb":{"sgxtcbcomp01svn":4,"sgxtcbcomp02svn":4,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":5},"tcbDate":"2018-01-04T00:00:00Z","tcbStatus":"OutOfDate"},{"tcb":{"sgxtcbcomp01svn":2,"sgxtcbcomp02svn":2,"sgxtcbcomp03svn":2,"sgxtcbcomp04svn":4,"sgxtcbcomp05svn":1,"sgxtcbcomp06svn":128,"sgxtcbcomp07svn":0,"sgxtcbcomp08svn":0,"sgxtcbcomp09svn":0,"sgxtcbcomp10svn":0,"sgxtcbcomp11svn":0,"sgxtcbcomp12svn":0,"sgxtcbcomp13svn":0,"sgxtcbcomp14svn":0,"sgxtcbcomp15svn":0,"sgxtcbcomp16svn":0,"pcesvn":4},"tcbDate":"2017-07-26T00:00:00Z","tcbStatus":"OutOfDate"}]}"#; + let signature = hex!("e0cc3102e9ffdb21cf156ba30f13d027210ab11f3bff349e670e4c49b2f0cb6889c7eeb436149c7efe53e15c97e6ec3fc9f34c3440e732a4c760f8eb91834a36"); + let signature = encode_as_der(&signature).unwrap(); + verify_signature(&leaf_cert, data, &signature, webpki::ring::ECDSA_P256_SHA256).unwrap(); +} + +/// This is demo code of how a CRL certificate can be parsed and how the revoked serials can be +/// extracted The part that is missing/open is how to verify the certificate chain of the CRL +/// TODO: Implement CRL handling +#[test] +fn parse_pck_crl() { + let crl_decoded = hex::decode(PCK_CRL).unwrap(); + let crl: x509_cert::crl::CertificateList = der::Decode::from_der(&crl_decoded).unwrap(); + + let mut serials = vec![]; + if let Some(certs) = crl.tbs_cert_list.revoked_certificates { + for c in certs { + let serial = c.serial_number.as_bytes().to_vec(); + serials.push(serial); + } + } + assert_eq!(3, serials.len()); +} + +#[test] +fn parse_pck_certificate() { + let der = DCAP_QUOTE_CERT.replace('\n', ""); + let der = base64::decode(der).unwrap(); + + let ext = get_intel_extension(&der).unwrap(); + assert_eq!(453, ext.len()); + + let fmspc = get_fmspc(&ext).unwrap(); + assert_eq!(hex!("00906EA10000"), fmspc); + + let cpusvn = get_cpusvn(&ext).unwrap(); + assert_eq!(hex!("11110204018007000000000000000000"), cpusvn); + + let pcesvn = get_pcesvn(&ext).unwrap(); + assert_eq!(u16::from_be_bytes(hex!("000B")), pcesvn); +} diff --git a/pallets/teebag/src/sgx_verify/utils.rs b/pallets/teebag/src/sgx_verify/utils.rs new file mode 100644 index 0000000000..14a1315a0a --- /dev/null +++ b/pallets/teebag/src/sgx_verify/utils.rs @@ -0,0 +1,38 @@ +fn safe_indexing_one(data: &[u8], idx: usize) -> Result { + let elt = data.get(idx).ok_or("Index out of bounds")?; + Ok(*elt as usize) +} + +pub fn length_from_raw_data(data: &[u8], offset: &mut usize) -> Result { + let mut len = safe_indexing_one(data, *offset)?; + if len > 0x80 { + len = (safe_indexing_one(data, *offset + 1)?) * 0x100 + + (safe_indexing_one(data, *offset + 2)?); + *offset += 2; + } + Ok(len) +} + +#[cfg(test)] +mod test { + use super::*; + use frame_support::assert_err; + + #[test] + fn index_equal_length_returns_err() { + // It was discovered a panic occurs if `index == data.len()` due to out of bound + // indexing. Here the fix is tested. + // + // For context see: https://github.com/integritee-network/pallet-teerex/issues/34 + let data: [u8; 7] = [0, 1, 2, 3, 4, 5, 6]; + assert_err!(safe_indexing_one(&data, data.len()), "Index out of bounds"); + } + + #[test] + fn safe_indexing_works() { + let data: [u8; 7] = [0, 1, 2, 3, 4, 5, 6]; + assert_eq!(safe_indexing_one(&data, 0), Ok(0)); + assert_eq!(safe_indexing_one(&data, 3), Ok(3)); + assert!(safe_indexing_one(&data, 10).is_err()); + } +} diff --git a/pallets/teebag/src/tcb.rs b/pallets/teebag/src/tcb.rs new file mode 100644 index 0000000000..0c6ba53156 --- /dev/null +++ b/pallets/teebag/src/tcb.rs @@ -0,0 +1,103 @@ +/* +Copyright 2021 Integritee AG and Supercomputing Systems AG + +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. + +*/ + +// `Tcb...` primitive part, copied from Integritee + +use crate::{Cpusvn, Pcesvn, Vec}; +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +use sp_core::RuntimeDebug; + +/// The list of valid TCBs for an enclave. +#[derive(Encode, Decode, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct QeTcb { + pub isvsvn: u16, +} + +impl QeTcb { + pub fn new(isvsvn: u16) -> Self { + Self { isvsvn } + } +} + +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct TcbVersionStatus { + pub cpusvn: Cpusvn, + pub pcesvn: Pcesvn, +} + +impl TcbVersionStatus { + pub fn new(cpusvn: Cpusvn, pcesvn: Pcesvn) -> Self { + Self { cpusvn, pcesvn } + } + + pub fn verify_examinee(&self, examinee: &TcbVersionStatus) -> bool { + for (v, r) in self.cpusvn.iter().zip(examinee.cpusvn.iter()) { + if *v > *r { + return false + } + } + self.pcesvn <= examinee.pcesvn + } +} + +/// This represents all the collateral data that we need to store on chain in order to verify +/// the quoting enclave validity of another enclave that wants to register itself on chain +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct TcbInfoOnChain { + // Todo: make timestamp: Moment + pub issue_date: u64, // unix epoch in milliseconds + // Todo: make timestamp: Moment + pub next_update: u64, // unix epoch in milliseconds + tcb_levels: Vec, +} + +impl TcbInfoOnChain { + pub fn new(issue_date: u64, next_update: u64, tcb_levels: Vec) -> Self { + Self { issue_date, next_update, tcb_levels } + } + + pub fn verify_examinee(&self, examinee: &TcbVersionStatus) -> bool { + for tb in &self.tcb_levels { + if tb.verify_examinee(examinee) { + return true + } + } + false + } +} + +#[cfg(test)] +mod tests { + use super::*; + use hex_literal::hex; + + #[test] + fn tcb_full_is_valid() { + // The strings are the hex encodings of the 16-byte CPUSVN numbers + let reference = TcbVersionStatus::new(hex!("11110204018007000000000000000000"), 7); + assert!(reference.verify_examinee(&reference)); + assert!(reference + .verify_examinee(&TcbVersionStatus::new(hex!("11110204018007000000000000000000"), 7))); + assert!(reference + .verify_examinee(&TcbVersionStatus::new(hex!("21110204018007000000000000000001"), 7))); + assert!(!reference + .verify_examinee(&TcbVersionStatus::new(hex!("10110204018007000000000000000000"), 6))); + assert!(!reference + .verify_examinee(&TcbVersionStatus::new(hex!("11110204018007000000000000000000"), 6))); + } +} diff --git a/pallets/teebag/src/tests.rs b/pallets/teebag/src/tests.rs new file mode 100644 index 0000000000..937cf27c8f --- /dev/null +++ b/pallets/teebag/src/tests.rs @@ -0,0 +1,414 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +// TODO: add `sidechain_block_imported` tests + +#![allow(dead_code, unused_imports)] +use crate::{ + mock::*, AttestationType, DcapProvider, Enclave, EnclaveRegistry, Error, Event as TeebagEvent, + MaxEnclaveCount, ScheduledEnclave, SgxBuildMode, WorkerType, H256, +}; +use frame_support::{assert_noop, assert_ok}; +use hex_literal::hex; +use sp_keyring::AccountKeyring; +use sp_runtime::AccountId32; +use test_utils::{get_signer, ias::consts::*}; + +const VALID_TIMESTAMP: Moment = 1671606747000; + +fn default_enclave() -> Enclave { + Enclave::default() + .with_attestation_type(AttestationType::Ignore) + .with_url(URL.to_vec()) + .with_last_seen_timestamp(pallet_timestamp::Pallet::::now()) +} + +fn register_quoting_enclave() { + let quoting_enclave = br#"{"id":"QE","version":2,"issueDate":"2022-12-04T22:45:33Z","nextUpdate":"2023-01-03T22:45:33Z","tcbEvaluationDataNumber":13,"miscselect":"00000000","miscselectMask":"FFFFFFFF","attributes":"11000000000000000000000000000000","attributesMask":"FBFFFFFFFFFFFFFF0000000000000000","mrsigner":"8C4F5775D796503E96137F77C68A829A0056AC8DED70140B081B094490C57BFF","isvprodid":1,"tcbLevels":[{"tcb":{"isvsvn":6},"tcbDate":"2022-11-09T00:00:00Z","tcbStatus":"UpToDate"},{"tcb":{"isvsvn":5},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00477"]},{"tcb":{"isvsvn":4},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00334","INTEL-SA-00477"]},{"tcb":{"isvsvn":2},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00219","INTEL-SA-00293","INTEL-SA-00334","INTEL-SA-00477"]},{"tcb":{"isvsvn":1},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00202","INTEL-SA-00219","INTEL-SA-00293","INTEL-SA-00334","INTEL-SA-00477"]}]}"#; + let signature = hex!("47accba321e57c20722a0d3d1db11c9b52661239857dc578ca1bde13976ee288cf39f72111ffe445c7389ef56447c79e30e6b83a8863ed9880de5bde4a8d5c91"); + let certificate_chain = include_bytes!("./sgx_verify/test/dcap/qe_identity_issuer_chain.pem"); + + let pubkey: [u8; 32] = [ + 65, 89, 193, 118, 86, 172, 17, 149, 206, 160, 174, 75, 219, 151, 51, 235, 110, 135, 20, 55, + 147, 162, 106, 110, 143, 207, 57, 64, 67, 63, 203, 95, + ]; + let signer: AccountId32 = get_signer(&pubkey); + assert_ok!(Teebag::register_quoting_enclave( + RuntimeOrigin::signed(signer), + quoting_enclave.to_vec(), + signature.to_vec(), + certificate_chain.to_vec(), + )); +} + +fn register_tcb_info() { + let tcb_info = br#"{"id":"SGX","version":3,"issueDate":"2022-11-17T12:45:32Z","nextUpdate":"2023-04-16T12:45:32Z","fmspc":"00906EA10000","pceId":"0000","tcbType":0,"tcbEvaluationDataNumber":12,"tcbLevels":[{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"SWHardeningNeeded","advisoryIDs":["INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":11},"tcbDate":"2021-11-10T00:00:00Z","tcbStatus":"ConfigurationAndSWHardeningNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":17},{"svn":17},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-11-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00477","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":15},{"svn":15},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":15},{"svn":15},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2020-06-10T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":14},{"svn":14},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":7},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":14},{"svn":14},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":10},"tcbDate":"2019-12-11T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":13},{"svn":13},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":3},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":13},{"svn":13},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":9},"tcbDate":"2019-11-13T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":6},{"svn":6},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00161","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":6},{"svn":6},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-05-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00233","INTEL-SA-00161","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":1},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":7},"tcbDate":"2019-01-09T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":5},{"svn":5},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":6},"tcbDate":"2018-08-15T00:00:00Z","tcbStatus":"OutOfDateConfigurationNeeded","advisoryIDs":["INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":4},{"svn":4},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":5},"tcbDate":"2018-01-04T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]},{"tcb":{"sgxtcbcomponents":[{"svn":2},{"svn":2},{"svn":2},{"svn":4},{"svn":1},{"svn":128},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0},{"svn":0}],"pcesvn":4},"tcbDate":"2017-07-26T00:00:00Z","tcbStatus":"OutOfDate","advisoryIDs":["INTEL-SA-00088","INTEL-SA-00106","INTEL-SA-00115","INTEL-SA-00135","INTEL-SA-00203","INTEL-SA-00161","INTEL-SA-00233","INTEL-SA-00220","INTEL-SA-00270","INTEL-SA-00293","INTEL-SA-00219","INTEL-SA-00289","INTEL-SA-00320","INTEL-SA-00329","INTEL-SA-00381","INTEL-SA-00389","INTEL-SA-00477","INTEL-SA-00334"]}]}"#; + let signature = hex!("71746f2148ecba04e35cf1ac77a7e6267ce99f6781c1031f724bb5bd94b8c1b6e4c07c01dc151692aa75be80dfba7350bb80c58314a6975189597e28e9bbc75c"); + let certificate_chain = include_bytes!("./sgx_verify/test/dcap/tcb_info_issuer_chain.pem"); + + let pubkey: [u8; 32] = [ + 65, 89, 193, 118, 86, 172, 17, 149, 206, 160, 174, 75, 219, 151, 51, 235, 110, 135, 20, 55, + 147, 162, 106, 110, 143, 207, 57, 64, 67, 63, 203, 95, + ]; + let signer: AccountId32 = get_signer(&pubkey); + assert_ok!(Teebag::register_tcb_info( + RuntimeOrigin::signed(signer), + tcb_info.to_vec(), + signature.to_vec(), + certificate_chain.to_vec(), + )); +} + +// ===================================================== +// Unittest in `Development` mode, where: +// - AttestationType::Ignore is possible +// - No scheduled enclave check +// - No sgx_build_mode check +// ===================================================== + +#[test] +fn register_enclave_dev_works_with_no_scheduled_enclave() { + new_test_ext(true).execute_with(|| { + // it works with no entry in scheduled_enclave + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + Default::default(), + TEST4_MRENCLAVE.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ignore, + )); + + let enclave = default_enclave().with_mrenclave(TEST4_MRENCLAVE); + + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); + assert_eq!( + EnclaveRegistry::::get(AccountKeyring::Alice.to_account_id()).unwrap(), + enclave + ); + }) +} + +#[test] +fn register_enclave_dev_works_with_sgx_build_mode_debug() { + new_test_ext(true).execute_with(|| { + // we'll need to use real attestation data + set_timestamp(TEST4_TIMESTAMP); + let signer4: AccountId32 = get_signer(TEST4_SIGNER_PUB); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer4.clone()), + Default::default(), + TEST4_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + + let enclave = default_enclave() + .with_mrenclave(TEST4_MRENCLAVE) + .with_last_seen_timestamp(TEST4_TIMESTAMP) + .with_register_timestamp(TEST4_TIMESTAMP) + .with_sgx_build_mode(SgxBuildMode::Debug) + .with_attestation_type(AttestationType::Ias); + + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); + assert_eq!(EnclaveRegistry::::get(signer4).unwrap(), enclave); + }) +} + +#[test] +fn parentchain_block_processed_works() { + new_test_ext(true).execute_with(|| { + Timestamp::set_timestamp(TEST7_TIMESTAMP); + + // start from block 2, otherwise we get `TimeStamp not set` error, + // because `run_to_block` calls `Timestamp::on_finalize` + run_to_block(2); + Timestamp::set_timestamp(TEST7_TIMESTAMP + 12 * 1000); + + let block_hash = H256::default(); + let merkle_root = H256::default(); + let block_number = 2; + let signer7: AccountId32 = get_signer(TEST7_SIGNER_PUB); + + // Ensure that enclave is registered + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer7.clone()), + WorkerType::BitAcross, + TEST7_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 1); + + run_to_block(3); + Timestamp::set_timestamp(TEST7_TIMESTAMP + 24 * 1000); + + assert_ok!(Teebag::parentchain_block_processed( + RuntimeOrigin::signed(signer7.clone()), + block_hash, + block_number, + merkle_root, + )); + + let expected_event = RuntimeEvent::Teebag(TeebagEvent::ParentchainBlockProcessed { + who: signer7, + block_number, + block_hash, + task_merkle_root: merkle_root, + }); + assert!(System::events().iter().any(|a| a.event == expected_event)); + }) +} + +#[test] +fn register_dcap_enclave_works() { + new_test_ext(true).execute_with(|| { + Timestamp::set_timestamp(VALID_TIMESTAMP); + register_quoting_enclave(); + register_tcb_info(); + + let pubkey: [u8; 32] = [ + 65, 89, 193, 118, 86, 172, 17, 149, 206, 160, 174, 75, 219, 151, 51, 235, 110, 135, 20, + 55, 147, 162, 106, 110, 143, 207, 57, 64, 67, 63, 203, 95, + ]; + let signer: AccountId = get_signer(&pubkey); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer.clone()), + WorkerType::Identity, + TEST1_DCAP_QUOTE.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Dcap(DcapProvider::Integritee) + )); + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); + assert_eq!(Teebag::enclave_registry(&signer).unwrap().last_seen_timestamp, VALID_TIMESTAMP); + assert_ok!(Teebag::unregister_enclave(RuntimeOrigin::signed(signer))); + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 0); + }) +} + +// ===================================================== +// Unittest in `Production` mode +// ===================================================== + +#[test] +fn register_enclave_prod_works_with_sgx_build_mode_debug() { + new_test_ext(false).execute_with(|| { + assert_ok!(Teebag::set_scheduled_enclave( + RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + WorkerType::Identity, + 0, + TEST4_MRENCLAVE + )); + + set_timestamp(TEST4_TIMESTAMP); + let signer4: AccountId32 = get_signer(TEST4_SIGNER_PUB); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer4.clone()), + Default::default(), + TEST4_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + + let enclave = default_enclave() + .with_mrenclave(TEST4_MRENCLAVE) + .with_last_seen_timestamp(TEST4_TIMESTAMP) + .with_register_timestamp(TEST4_TIMESTAMP) + .with_sgx_build_mode(SgxBuildMode::Debug) + .with_attestation_type(AttestationType::Ias); + + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); + assert_eq!(EnclaveRegistry::::get(signer4).unwrap(), enclave); + }) +} + +#[test] +fn register_enclave_prod_works_with_sgx_build_mode_production() { + new_test_ext(false).execute_with(|| { + assert_ok!(Teebag::set_scheduled_enclave( + RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + WorkerType::Identity, + 0, + TEST8_MRENCLAVE + )); + + set_timestamp(TEST8_TIMESTAMP); + let signer8: AccountId32 = get_signer(TEST8_SIGNER_PUB); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer8.clone()), + Default::default(), + TEST8_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + + let enclave = default_enclave() + .with_mrenclave(TEST8_MRENCLAVE) + .with_last_seen_timestamp(TEST8_TIMESTAMP) + .with_register_timestamp(TEST8_TIMESTAMP) + .with_sgx_build_mode(SgxBuildMode::Production) + .with_attestation_type(AttestationType::Ias); + + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); + assert_eq!(EnclaveRegistry::::get(signer8).unwrap(), enclave); + }) +} + +#[test] +fn register_enclave_prod_fails_with_wrong_attestation_type() { + new_test_ext(false).execute_with(|| { + assert_noop!( + Teebag::register_enclave( + RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + Default::default(), + TEST4_MRENCLAVE.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ignore, // only allowed in dev mode + ), + Error::::InvalidAttestationType + ); + }) +} + +#[test] +fn register_enclave_prod_fails_with_no_scheduled_enclave() { + new_test_ext(false).execute_with(|| { + Timestamp::set_timestamp(TEST4_TIMESTAMP); + let signer = get_signer(TEST4_SIGNER_PUB); + assert_noop!( + Teebag::register_enclave( + RuntimeOrigin::signed(signer), + Default::default(), + TEST4_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + ), + Error::::EnclaveNotInSchedule + ); + }) +} + +#[test] +fn register_enclave_prod_fails_with_max_limit_reached() { + new_test_ext(false).execute_with(|| { + MaxEnclaveCount::::insert(WorkerType::BitAcross, 1u64); + ScheduledEnclave::::insert((WorkerType::BitAcross, 0), TEST5_MRENCLAVE); + ScheduledEnclave::::insert((WorkerType::BitAcross, 1), TEST6_MRENCLAVE); + ScheduledEnclave::::insert((WorkerType::Identity, 0), TEST5_MRENCLAVE); + ScheduledEnclave::::insert((WorkerType::Identity, 1), TEST6_MRENCLAVE); + + let signer5: AccountId32 = get_signer(TEST5_SIGNER_PUB); + let signer6: AccountId32 = get_signer(TEST6_SIGNER_PUB); + + Timestamp::set_timestamp(TEST5_TIMESTAMP); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer5.clone()), + WorkerType::BitAcross, + TEST5_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + Timestamp::set_timestamp(TEST6_TIMESTAMP); + assert_noop!( + Teebag::register_enclave( + RuntimeOrigin::signed(signer6.clone()), + WorkerType::BitAcross, + TEST6_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + ), + Error::::MaxEnclaveCountOverflow + ); + + // re-register them as WorkerType::Identity should work though + Timestamp::set_timestamp(TEST5_TIMESTAMP); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer5), + WorkerType::Identity, + TEST5_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + + Timestamp::set_timestamp(TEST6_TIMESTAMP); + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer6), + WorkerType::Identity, + TEST6_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + )); + + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 2); + assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); + assert_eq!(Teebag::max_enclave_count(WorkerType::Identity), 3); + assert_eq!(Teebag::max_enclave_count(WorkerType::BitAcross), 1); + }) +} + +#[test] +fn register_quoting_enclave_works() { + new_test_ext(false).execute_with(|| { + let qe = Teebag::quoting_enclave_registry(); + assert_eq!(qe.mrsigner, [0u8; 32]); + assert_eq!(qe.isvprodid, 0); + Timestamp::set_timestamp(VALID_TIMESTAMP); + register_quoting_enclave(); + let qe = Teebag::quoting_enclave_registry(); + assert_eq!(qe.isvprodid, 1); + }) +} + +#[test] +fn register_tcb_info_works() { + new_test_ext(false).execute_with(|| { + Timestamp::set_timestamp(VALID_TIMESTAMP); + + register_tcb_info(); + let fmspc = hex!("00906EA10000"); + let tcb_info = Teebag::tcb_info(fmspc); + // This is the date that the is registered in register_tcb_info and represents the date + // 2023-04-16T12:45:32Z + assert_eq!(tcb_info.next_update, 1681649132000); + }) +} diff --git a/pallets/teebag/src/types.rs b/pallets/teebag/src/types.rs new file mode 100644 index 0000000000..a8f1bc6b50 --- /dev/null +++ b/pallets/teebag/src/types.rs @@ -0,0 +1,179 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use codec::{Decode, Encode}; +use scale_info::TypeInfo; +#[cfg(feature = "std")] +use serde::{Deserialize, Serialize}; +use sp_core::{RuntimeDebug, H256}; +use sp_std::prelude::*; + +pub type MrSigner = [u8; 32]; +pub type MrEnclave = [u8; 32]; +pub type Fmspc = [u8; 6]; +pub type Cpusvn = [u8; 16]; +pub type Pcesvn = u16; +pub type ShardIdentifier = H256; +pub type EnclaveFingerprint = H256; +pub type SidechainBlockNumber = u64; + +/// Different modes that control enclave registration and running: +/// - `Production`: default value. It perfroms all checks for enclave registration and runtime +/// - `Development`: the most lenient, most check are skipped during registration or runtime +/// - `Maintenance`: a placeholder value for now - maybe to stall sidechain block production +/// +/// please note: +/// `Attestation::Ignore` is only possible under `OperationalMode::Development`, but not vice versa. +/// So if you define `Attestation::Ias`, the attestation will be verified even in `Development` mode +#[derive(PartialEq, Eq, Clone, Copy, Default, Encode, Decode, Debug, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub enum OperationalMode { + #[default] + #[codec(index = 0)] + Production, + #[codec(index = 1)] + Development, + #[codec(index = 2)] + Maintenance, +} + +#[derive(Encode, Decode, Default, Clone, Copy, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum DcapProvider { + #[default] + MAA, + Intel, + Local, + Integritee, +} + +#[derive(Encode, Decode, Clone, Copy, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum AttestationType { + #[default] + Ignore, + Ias, + Dcap(DcapProvider), +} + +#[derive(Encode, Decode, Clone, Copy, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum WorkerType { + #[default] + Identity, + BitAcross, +} + +impl WorkerType { + pub fn is_sidechain(&self) -> bool { + self == &Self::Identity + } +} + +#[derive(Encode, Decode, Copy, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum SgxBuildMode { + #[default] + #[codec(index = 0)] + Production, + #[codec(index = 1)] + Debug, +} + +#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, Copy, Default, TypeInfo)] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +pub struct SidechainBlockConfirmation { + pub block_number: SidechainBlockNumber, + pub block_header_hash: H256, +} + +#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub struct Enclave { + pub worker_type: WorkerType, + pub mrenclave: MrEnclave, + pub last_seen_timestamp: u64, // unix epoch in milliseconds when it's last seen + pub register_timestamp: u64, // unix epoch in milliseconds when it's registered + pub url: Vec, // utf8 encoded url + pub shielding_pubkey: Option>, // JSON serialised enclave shielding pub key + pub vc_pubkey: Option>, + pub sgx_build_mode: SgxBuildMode, + pub attestation_type: AttestationType, +} + +impl Enclave { + pub fn new( + worker_type: WorkerType, + url: Vec, + shielding_pubkey: Option>, + vc_pubkey: Option>, + attestation_type: AttestationType, + ) -> Self { + Enclave { + worker_type, + url, + shielding_pubkey, + vc_pubkey, + attestation_type, + ..Default::default() + } + } + + pub fn with_mrenclave(mut self, mrenclave: MrEnclave) -> Self { + self.mrenclave = mrenclave; + self + } + + pub fn with_url(mut self, url: Vec) -> Self { + self.url = url; + self + } + + pub fn with_last_seen_timestamp(mut self, t: u64) -> Self { + self.last_seen_timestamp = t; + self + } + + pub fn with_attestation_type(mut self, attestation_type: AttestationType) -> Self { + self.attestation_type = attestation_type; + self + } + + pub fn with_sgx_build_mode(mut self, sgx_build_mode: SgxBuildMode) -> Self { + self.sgx_build_mode = sgx_build_mode; + self + } + + pub fn with_register_timestamp(mut self, t: u64) -> Self { + self.register_timestamp = t; + self + } +} + +// use the name `RsaRequest` to differentiate from `AesRequest` (see aes_request.rs in +// tee-worker) `Rsa` implies that the payload is RSA-encrypted (using enclave's shielding key) +#[macro_export] +macro_rules! decl_rsa_request { + ($($t:meta),*) => { + #[derive(Encode, Decode, Default, Clone, PartialEq, Eq, $($t),*)] + pub struct RsaRequest { + pub shard: ShardIdentifier, + pub payload: Vec, + } + impl RsaRequest { + pub fn new(shard: ShardIdentifier, payload: Vec) -> Self { + Self { shard, payload } + } + } + }; +} + +decl_rsa_request!(TypeInfo, RuntimeDebug); diff --git a/runtime/common/Cargo.toml b/runtime/common/Cargo.toml index 73ad2c4b6c..3e44be3b92 100644 --- a/runtime/common/Cargo.toml +++ b/runtime/common/Cargo.toml @@ -47,6 +47,7 @@ core-primitives = { path = "../../primitives/core", default-features = false } pallet-asset-manager = { path = "../../pallets/xcm-asset-manager", default-features = false } pallet-extrinsic-filter = { path = "../../pallets/extrinsic-filter", default-features = false } pallet-group = { path = "../../pallets/group", default-features = false } +pallet-teebag = { path = "../../pallets/teebag", default-features = false } pallet-teerex = { path = "../../pallets/teerex", default-features = false } teerex-primitives = { path = "../../primitives/teerex", default-features = false, optional = true } @@ -97,6 +98,7 @@ std = [ "core-primitives/std", "pallet-teerex/std", "teerex-primitives?/std", + "pallet-teebag/std", ] runtime-benchmarks = [ @@ -104,6 +106,7 @@ runtime-benchmarks = [ "pallet-teerex/runtime-benchmarks", "sp-runtime/runtime-benchmarks", "teerex-primitives", + "pallet-teebag/runtime-benchmarks", "test-utils", "frame-system/runtime-benchmarks", "frame-support/runtime-benchmarks", diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 0297efb21a..59bb72a002 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -338,7 +338,7 @@ where pub struct EnsureEnclaveSigner(PhantomData); impl EnsureOrigin for EnsureEnclaveSigner where - T: frame_system::Config + pallet_teerex::Config, + T: frame_system::Config + pallet_teerex::Config + pallet_teebag::Config, ::AccountId: From<[u8; 32]>, ::Hash: From<[u8; 32]>, { @@ -346,7 +346,8 @@ where fn try_origin(o: T::RuntimeOrigin) -> Result { o.into().and_then(|o| match o { frame_system::RawOrigin::Signed(who) - if pallet_teerex::Pallet::::ensure_registered_enclave(&who) == Ok(()) => + if pallet_teerex::Pallet::::ensure_registered_enclave(&who).is_ok() || + pallet_teebag::EnclaveRegistry::::contains_key(&who) => Ok(who), r => Err(T::RuntimeOrigin::from(r)), }) @@ -358,11 +359,6 @@ where consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}, TestEnclave, }; - // The below is the hardcode TEST8_SIGNER_PUB. We should save it for convenience. - // let TEST8_SIGNER_PUB_DIRECT: [u8; 32] = [ - // 54, 176, 127, 194, 139, 9, 249, 13, 217, 106, 6, 124, 75, 0, 109, 18, 202, 202, 240, - // 124, 214, 235, 255, 249, 47, 135, 174, 246, 167, 5, 6, 224, - // ]; let signer: ::AccountId = test_utils::get_signer(TEST8_SIGNER_PUB); if !pallet_teerex::EnclaveIndex::::contains_key(signer.clone()) { @@ -372,6 +368,11 @@ where .with_mr_enclave(TEST8_MRENCLAVE), )); } + + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + let enclave = pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE); + assert_ok!(pallet_teebag::Pallet::::add_enclave(&signer, &enclave)); + } Ok(frame_system::RawOrigin::Signed(signer).into()) } } diff --git a/runtime/litmus/src/lib.rs b/runtime/litmus/src/lib.rs index add4b7349d..34f5b08997 100644 --- a/runtime/litmus/src/lib.rs +++ b/runtime/litmus/src/lib.rs @@ -151,7 +151,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("litmus-parachain"), authoring_version: 1, // same versioning-mechanism as polkadot: use last digit for minor updates - spec_version: 9172, + spec_version: 9173, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/rococo/Cargo.toml b/runtime/rococo/Cargo.toml index e5593685ef..b896739e60 100644 --- a/runtime/rococo/Cargo.toml +++ b/runtime/rococo/Cargo.toml @@ -100,6 +100,7 @@ runtime-common = { path = '../common', default-features = false } # TEE pallets pallet-group = { path = "../../pallets/group", default-features = false } pallet-sidechain = { path = "../../pallets/sidechain", default-features = false } +pallet-teebag = { path = "../../pallets/teebag", default-features = false } pallet-teeracle = { path = "../../pallets/teeracle", default-features = false } pallet-teerex = { path = "../../pallets/teerex", default-features = false } @@ -177,6 +178,7 @@ runtime-benchmarks = [ "pallet-group/runtime-benchmarks", "pallet-identity-management/runtime-benchmarks", "pallet-teerex/runtime-benchmarks", + "pallet-teebag/runtime-benchmarks", "pallet-sidechain/runtime-benchmarks", "pallet-teeracle/runtime-benchmarks", "pallet-vc-management/runtime-benchmarks", @@ -267,6 +269,7 @@ std = [ "pallet-group/std", "pallet-identity-management/std", "pallet-teerex/std", + "pallet-teebag/std", "pallet-sidechain/std", "pallet-teeracle/std", "pallet-vc-management/std", @@ -317,6 +320,7 @@ try-runtime = [ "pallet-sudo/try-runtime", "pallet-teeracle/try-runtime", "pallet-teerex/try-runtime", + "pallet-teebag/try-runtime", "pallet-timestamp/try-runtime", "pallet-tips/try-runtime", "pallet-transaction-payment/try-runtime", diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 069c4fa555..55b5992490 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -41,6 +41,7 @@ use runtime_common::EnsureEnclaveSigner; // for TEE pub use pallet_balances::Call as BalancesCall; pub use pallet_sidechain; +pub use pallet_teebag; pub use pallet_teeracle; pub use pallet_teerex; @@ -1024,6 +1025,12 @@ impl pallet_teeracle::Config for Runtime { type MaxOracleBlobLen = ConstU32<4096>; } +impl pallet_teebag::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type MomentsPerDay = MomentsPerDay; + type SetAdminOrigin = EnsureRootOrHalfCouncil; +} + impl pallet_identity_management::Config for Runtime { type RuntimeEvent = RuntimeEvent; type WeightInfo = (); @@ -1255,6 +1262,7 @@ construct_runtime! { Teerex: pallet_teerex = 90, Sidechain: pallet_sidechain = 91, Teeracle: pallet_teeracle = 92, + Teebag: pallet_teebag = 93, // Frontier EVM: pallet_evm = 120, @@ -1332,6 +1340,7 @@ impl Contains for NormalModeFilter { RuntimeCall::Teerex(_) | RuntimeCall::Sidechain(_) | RuntimeCall::Teeracle(_) | + RuntimeCall::Teebag(_) | // ParachainStaking; Only the collator part RuntimeCall::ParachainStaking(pallet_parachain_staking::Call::join_candidates { .. }) | RuntimeCall::ParachainStaking(pallet_parachain_staking::Call::schedule_leave_candidates { .. }) | diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index 151af2fadf..fca0a616d6 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -9339,6 +9339,32 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-teebag" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "chrono 0.4.26", + "der 0.6.1", + "frame-support", + "frame-system", + "hex 0.4.3", + "log 0.4.20", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "ring 0.16.20", + "rustls-webpki", + "scale-info", + "serde 1.0.193", + "serde_json 1.0.103", + "sp-core", + "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "sp-runtime", + "sp-std 5.0.0", + "x509-cert", +] + [[package]] name = "pallet-teeracle" version = "0.1.0" @@ -11210,6 +11236,7 @@ dependencies = [ "pallet-session", "pallet-sidechain", "pallet-sudo", + "pallet-teebag", "pallet-teeracle", "pallet-teerex", "pallet-timestamp", @@ -11338,6 +11365,7 @@ dependencies = [ "pallet-group", "pallet-membership", "pallet-multisig", + "pallet-teebag", "pallet-teerex", "pallet-transaction-payment", "pallet-treasury", diff --git a/tee-worker/enclave-runtime/src/attestation.rs b/tee-worker/enclave-runtime/src/attestation.rs index 5b7f7ded3a..7702cd8f0f 100644 --- a/tee-worker/enclave-runtime/src/attestation.rs +++ b/tee-worker/enclave-runtime/src/attestation.rs @@ -533,8 +533,6 @@ fn get_shielding_pubkey() -> EnclaveResult>> { }) .ok(); - debug!("[Enclave] shielding_pubkey size: {:?}", shielding_pubkey.clone().map(|key| key.len())); - Ok(shielding_pubkey) } From 7e0588ef161a386abf4312895c25b3c9af160d08 Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Sun, 4 Feb 2024 10:44:27 +0530 Subject: [PATCH 12/64] feat: bitacross worker<>parachain external communication (#2452) * fix: some init worker * fix: adding rpc handler for bitacross worker * fix: added AESRequest for bitacross handler * fix: add placeholder pallet for bitacross * fix: add call_index into node_metadata for placeholder bitacross pallet * fix: update nodemetadatamock with placeholder calls trait * refactor: indirect calls * fix: add task context for thread * revert: accidental commit * fix: update cargo lock * fix: remove clippy warning for deprecated weights * refactor: fix clippy * refactor: remove comments and update logs * fix: update launch.py based on comments * fix: add remove and update schedule enclave * refactor: remove comments * fix: moved thread spawn to enclave_init * fix: remove submitting TOP * refactor: fix clippy * refactor: fix clippy ci * fix: update recursion limit in runtime --------- Co-authored-by: Kasper Ziemianek --- Cargo.lock | 23 +++ Cargo.toml | 1 + bitacross-worker/Cargo.lock | 142 ++++++++++++++---- bitacross-worker/Cargo.toml | 2 + .../src/integritee/mod.rs | 103 ++++--------- .../core/bc-task-receiver/Cargo.toml | 92 ++++++++++++ .../core/bc-task-receiver/src/lib.rs | 118 +++++++++++++++ .../bitacross/core/bc-task-sender/Cargo.toml | 37 +++++ .../bitacross/core/bc-task-sender/src/lib.rs | 110 ++++++++++++++ .../node-api/metadata/src/lib.rs | 14 +- .../node-api/metadata/src/metadata_mocks.rs | 21 ++- .../node-api/metadata/src/pallet_bitacross.rs | 30 ++++ bitacross-worker/enclave-runtime/Cargo.toml | 3 + .../src/initialization/global_components.rs | 2 +- .../enclave-runtime/src/initialization/mod.rs | 31 ++++ .../src/test/top_pool_tests.rs | 2 +- .../sidechain/rpc-handler/Cargo.toml | 5 + .../rpc-handler/src/direct_top_pool_api.rs | 39 +++++ pallets/bitacross-pallet/Cargo.toml | 68 +++++++++ pallets/bitacross-pallet/src/lib.rs | 68 +++++++++ runtime/rococo/Cargo.toml | 3 + runtime/rococo/src/lib.rs | 10 +- 22 files changed, 810 insertions(+), 114 deletions(-) create mode 100644 bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml create mode 100644 bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs create mode 100644 bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml create mode 100644 bitacross-worker/bitacross/core/bc-task-sender/src/lib.rs create mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs create mode 100644 pallets/bitacross-pallet/Cargo.toml create mode 100644 pallets/bitacross-pallet/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index f94641285d..2e217c5596 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6936,6 +6936,28 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-bitacross" +version = "0.1.0" +dependencies = [ + "core-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-balances", + "pallet-group", + "pallet-teerex", + "pallet-timestamp", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "teerex-primitives", + "test-utils", +] + [[package]] name = "pallet-bounties" version = "4.0.0-dev" @@ -10552,6 +10574,7 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", + "pallet-bitacross", "pallet-bounties", "pallet-bridge", "pallet-bridge-transfer", diff --git a/Cargo.toml b/Cargo.toml index dc4ee6ba27..a1f6ac55df 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,6 +17,7 @@ members = [ 'pallets/parentchain', 'pallets/test-utils', 'pallets/group', + 'pallets/bitacross-pallet', 'precompiles/*', 'primitives/common', 'primitives/core', diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 6d07382944..0402c9e279 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -32,7 +32,7 @@ dependencies = [ "derive_more", "either", "frame-metadata", - "hex", + "hex 0.4.3", "log 0.4.20", "parity-scale-codec", "scale-bits 0.4.0", @@ -590,6 +590,53 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bc-task-receiver" +version = "0.1.0" +dependencies = [ + "bc-task-sender", + "frame-support", + "futures 0.3.28", + "futures 0.3.8", + "hex 0.4.0", + "ita-sgx-runtime", + "ita-stf", + "itp-enclave-metrics", + "itp-extrinsics-factory", + "itp-node-api", + "itp-ocall-api", + "itp-sgx-crypto", + "itp-sgx-externalities", + "itp-stf-executor", + "itp-stf-state-handler", + "itp-storage", + "itp-top-pool-author", + "itp-types", + "itp-utils", + "litentry-primitives", + "log 0.4.20", + "parity-scale-codec", + "sgx_tstd", + "sp-core", + "thiserror 1.0.44", + "thiserror 1.0.9", + "threadpool 1.8.0", + "threadpool 1.8.1", +] + +[[package]] +name = "bc-task-sender" +version = "0.1.0" +dependencies = [ + "futures 0.3.28", + "futures 0.3.8", + "lazy_static", + "litentry-primitives", + "log 0.4.20", + "parity-scale-codec", + "sgx_tstd", +] + [[package]] name = "bech32" version = "0.10.0-beta" @@ -660,7 +707,7 @@ dependencies = [ "env_logger 0.9.3", "frame-metadata", "hdrhistogram", - "hex", + "hex 0.4.3", "ita-sgx-runtime", "ita-stf", "itc-rpc-client", @@ -711,7 +758,7 @@ dependencies = [ "env_logger 0.9.3", "frame-support", "futures 0.3.28", - "hex", + "hex 0.4.3", "ipfs-api", "ita-stf", "itc-parentchain", @@ -2428,7 +2475,7 @@ checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", "hashbrown 0.12.3", - "hex", + "hex 0.4.3", "rand_core 0.6.4", "sha2 0.9.9", "zeroize", @@ -3148,7 +3195,7 @@ name = "fp-account" version = "1.0.0-dev" source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" dependencies = [ - "hex", + "hex 0.4.3", "impl-serde", "libsecp256k1", "log 0.4.20", @@ -3166,7 +3213,7 @@ name = "fp-account" version = "1.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "hex", + "hex 0.4.3", "impl-serde", "libsecp256k1", "log 0.4.20", @@ -4034,6 +4081,14 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hex" +version = "0.4.0" +source = "git+https://github.com/mesalock-linux/rust-hex-sgx?tag=sgx_1.1.3#ee3266cd29b9f9c2eb69af9487f55c4f09c38f2b" +dependencies = [ + "sgx_tstd", +] + [[package]] name = "hex" version = "0.4.3" @@ -4654,7 +4709,7 @@ version = "0.9.0" dependencies = [ "frame-support", "frame-system", - "hex", + "hex 0.4.3", "hex-literal", "ita-sgx-runtime", "itp-hashing", @@ -5007,7 +5062,7 @@ checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38" name = "itp-api-client-extensions" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "itp-api-client-types", "itp-types", "sp-consensus-grandpa", @@ -5036,7 +5091,7 @@ dependencies = [ "bit-vec", "chrono 0.4.11", "chrono 0.4.26", - "hex", + "hex 0.4.3", "httparse 1.4.1", "itertools 0.10.5", "itp-ocall-api", @@ -5079,7 +5134,7 @@ name = "itp-enclave-api" version = "0.9.0" dependencies = [ "frame-support", - "hex", + "hex 0.4.3", "itc-parentchain", "itp-enclave-api-ffi", "itp-settings", @@ -5313,7 +5368,7 @@ dependencies = [ name = "itp-stf-executor" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "itc-parentchain-test", "itp-enclave-metrics", "itp-node-api", @@ -5432,7 +5487,7 @@ dependencies = [ name = "itp-test" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "itp-node-api", "itp-node-api-metadata-provider", "itp-ocall-api", @@ -5543,7 +5598,7 @@ dependencies = [ name = "itp-utils" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "litentry-hex-utils", "parity-scale-codec", ] @@ -5673,7 +5728,7 @@ version = "0.9.0" dependencies = [ "derive_more", "futures-timer", - "hex", + "hex 0.4.3", "itc-parentchain-test", "itp-settings", "itp-sgx-externalities", @@ -5735,6 +5790,7 @@ dependencies = [ name = "its-rpc-handler" version = "0.9.0" dependencies = [ + "bc-task-sender", "futures 0.3.28", "futures 0.3.8", "itp-rpc", @@ -6652,7 +6708,7 @@ dependencies = [ "bytes 1.4.0", "futures 0.3.28", "futures-timer", - "hex", + "hex 0.4.3", "if-watch", "libp2p-core 0.38.0", "libp2p-noise", @@ -6842,7 +6898,7 @@ checksum = "09fc20d2ca12cb9f044c93e3bd6d32d523e6e2ec3db4f7b2939cd99026ecd3f0" name = "litentry-hex-utils" version = "0.9.12" dependencies = [ - "hex", + "hex 0.4.3", ] [[package]] @@ -6857,7 +6913,7 @@ dependencies = [ "base64 0.13.1", "bitcoin", "core-primitives", - "hex", + "hex 0.4.3", "itp-sgx-crypto", "itp-utils", "litentry-hex-utils", @@ -7307,7 +7363,7 @@ dependencies = [ "environmental 1.1.4", "ethereum", "ethereum-types", - "hex", + "hex 0.4.3", "parity-scale-codec", "serde 1.0.193", "serde_json 1.0.103", @@ -8364,6 +8420,23 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-bitacross" +version = "0.1.0" +dependencies = [ + "core-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-teerex", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std 5.0.0", + "teerex-primitives", +] + [[package]] name = "pallet-bounties" version = "4.0.0-dev" @@ -8537,7 +8610,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "hex", + "hex 0.4.3", "impl-trait-for-tuples", "log 0.4.20", "parity-scale-codec", @@ -8561,7 +8634,7 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "hex", + "hex 0.4.3", "hex-literal", "impl-trait-for-tuples", "log 0.4.20", @@ -9170,7 +9243,7 @@ dependencies = [ "blake2", "crc32fast", "fs2", - "hex", + "hex 0.4.3", "libc", "log 0.4.20", "lz4", @@ -9585,7 +9658,7 @@ dependencies = [ "derive_more", "fatality", "futures 0.3.28", - "hex", + "hex 0.4.3", "parity-scale-codec", "polkadot-node-jaeger", "polkadot-node-primitives", @@ -10072,7 +10145,7 @@ checksum = "b1de8dacb0873f77e6aefc6d71e044761fcc68060290f5b1089fcdf84626bb69" dependencies = [ "bitflags 1.3.2", "byteorder 1.4.3", - "hex", + "hex 0.4.3", "lazy_static", "rustix 0.36.15", ] @@ -10803,6 +10876,7 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", + "pallet-bitacross", "pallet-bounties", "pallet-bridge", "pallet-bridge-transfer", @@ -11744,7 +11818,7 @@ dependencies = [ "sp-core", "sp-offchain", "sp-runtime", - "threadpool", + "threadpool 1.8.1", "tracing", ] @@ -11833,7 +11907,7 @@ dependencies = [ "array-bytes 4.2.0", "futures 0.3.28", "futures-util 0.3.28", - "hex", + "hex 0.4.3", "jsonrpsee 0.16.2", "log 0.4.20", "parity-scale-codec", @@ -12552,7 +12626,7 @@ dependencies = [ "chrono 0.4.26", "der 0.6.1", "frame-support", - "hex", + "hex 0.4.3", "parity-scale-codec", "ring 0.16.20", "rustls-webpki", @@ -13867,7 +13941,7 @@ dependencies = [ "derive_more", "frame-metadata", "frame-support", - "hex", + "hex 0.4.3", "log 0.4.20", "maybe-async", "parity-scale-codec", @@ -14164,6 +14238,14 @@ dependencies = [ "once_cell 1.18.0", ] +[[package]] +name = "threadpool" +version = "1.8.0" +source = "git+https://github.com/mesalock-linux/rust-threadpool-sgx?tag=sgx_1.1.3#098d98a85b7e2b02e2bb451a3dec0b027017ff4c" +dependencies = [ + "sgx_tstd", +] + [[package]] name = "threadpool" version = "1.8.1" @@ -14183,7 +14265,7 @@ dependencies = [ "integer-encoding", "log 0.4.20", "ordered-float", - "threadpool", + "threadpool 1.8.1", ] [[package]] @@ -14860,7 +14942,7 @@ checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder 1.4.3", "crunchy", - "hex", + "hex 0.4.3", "static_assertions", ] @@ -15585,7 +15667,7 @@ dependencies = [ "arc-swap", "async-trait", "bytes 1.4.0", - "hex", + "hex 0.4.3", "interceptor", "lazy_static", "log 0.4.20", diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 4f43662fa1..948912ce1b 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -71,6 +71,8 @@ members = [ "sidechain/state", "sidechain/validateer-fetch", "litentry/primitives", + "bitacross/core/bc-task-receiver", + "bitacross/core/bc-task-sender", ] [patch."https://github.com/apache/teaclave-sgx-sdk.git"] diff --git a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs index f27609698c..3995175f48 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs @@ -21,9 +21,7 @@ mod extrinsic_parser; use crate::{ decode_and_log_error, - indirect_calls::{ - InvokeArgs, RemoveScheduledEnclaveArgs, ShieldFundsArgs, UpdateScheduledEnclaveArgs, - }, + indirect_calls::{RemoveScheduledEnclaveArgs, UpdateScheduledEnclaveArgs}, integritee::extrinsic_parser::ParseExtrinsic, }; use codec::{Decode, Encode}; @@ -39,24 +37,18 @@ use itc_parentchain_indirect_calls_executor::{ }; use itp_node_api::metadata::NodeMetadataTrait; use itp_stf_primitives::traits::IndirectExecutor; -use itp_types::CallIndex; use log::trace; -use sp_std::vec::Vec; +use sp_core::crypto::AccountId32; /// The default indirect call (extrinsic-triggered) of the Integritee-Parachain. #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub enum IndirectCall { #[codec(index = 0)] - ShieldFunds(ShieldFundsArgs), + BitAcross(BitAcrossArgs), #[codec(index = 1)] - Invoke(InvokeArgs), - // Litentry - #[codec(index = 6)] UpdateScheduledEnclave(UpdateScheduledEnclaveArgs), - #[codec(index = 7)] + #[codec(index = 2)] RemoveScheduledEnclave(RemoveScheduledEnclaveArgs), - #[codec(index = 8)] - BatchAll(Vec), } impl> @@ -66,33 +58,37 @@ impl> fn dispatch(&self, executor: &Executor, _args: Self::Args) -> Result<()> { trace!("dispatching indirect call {:?}", self); match self { - IndirectCall::ShieldFunds(shieldfunds_args) => shieldfunds_args.dispatch(executor, ()), - IndirectCall::Invoke(invoke_args) => invoke_args.dispatch(executor, ()), - // Litentry - IndirectCall::UpdateScheduledEnclave(update_enclave_args) => - update_enclave_args.dispatch(executor, ()), - IndirectCall::RemoveScheduledEnclave(remove_enclave_args) => - remove_enclave_args.dispatch(executor, ()), - IndirectCall::BatchAll(calls) => { - for x in calls.clone() { - if let Err(e) = x.dispatch(executor, ()) { - log::warn!("Failed to execute indirect call in batch all due to: {:?}", e); - continue - } - } - Ok(()) - }, + IndirectCall::BitAcross(bitacross_args) => bitacross_args.dispatch(executor, ()), + IndirectCall::UpdateScheduledEnclave(update_scheduled_enclave_args) => + update_scheduled_enclave_args.dispatch(executor, ()), + IndirectCall::RemoveScheduledEnclave(remove_scheduled_enclave_args) => + remove_scheduled_enclave_args.dispatch(executor, ()), } } } +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] +pub struct BitAcrossArgs { + account_id: AccountId32, +} + +impl> + IndirectDispatch for BitAcrossArgs +{ + type Args = (); + fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { + log::error!("Not yet implemented"); + Ok(()) + } +} + /// Default filter we use for the Integritee-Parachain. -pub struct ShieldFundsAndInvokeFilter { +pub struct BitAcrossIndirectCallsFilter { _phantom: PhantomData, } impl FilterIntoDataFrom - for ShieldFundsAndInvokeFilter + for BitAcrossIndirectCallsFilter where ExtrinsicParser: ParseExtrinsic, { @@ -111,7 +107,7 @@ where Ok(xt) => xt, Err(e) => { log::error!( - "[ShieldFundsAndInvokeFilter] Could not parse parentchain extrinsic: {:?}", + "[BitAcrossIndirectCallsFilter] Could not parse parentchain extrinsic: {:?}", e ); return None @@ -119,55 +115,18 @@ where }; let index = xt.call_index; let call_args = &mut &xt.call_args[..]; - log::trace!( - "[ShieldFundsAndInvokeFilter] attempting to execute indirect call with index {:?}", - index - ); - if index == metadata.shield_funds_call_indexes().ok()? { - log::debug!("executing shield funds call"); - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { - log::debug!("executing invoke call"); - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::Invoke(args)) - // Litentry + + if index == metadata.placeholder_call_indexes().ok()? { + let args = decode_and_log_error::(call_args)?; + Some(IndirectCall::BitAcross(args)) } else if index == metadata.update_scheduled_enclave().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::UpdateScheduledEnclave(args)) } else if index == metadata.remove_scheduled_enclave().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::RemoveScheduledEnclave(args)) - } else if index == metadata.batch_all_call_indexes().ok()? { - parse_batch_all(call_args, metadata) } else { None } } } - -fn parse_batch_all( - call_args: &mut &[u8], - metadata: &NodeMetadata, -) -> Option { - let call_count: sp_std::vec::Vec<()> = Decode::decode(call_args).ok()?; - let mut calls: Vec = Vec::new(); - log::debug!("Received BatchAll including {} calls", call_count.len()); - for _i in 0..call_count.len() { - let index: CallIndex = Decode::decode(call_args).ok()?; - if index == metadata.shield_funds_call_indexes().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::Invoke(args)) - } else if index == metadata.update_scheduled_enclave().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::UpdateScheduledEnclave(args)) - } else if index == metadata.remove_scheduled_enclave().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::RemoveScheduledEnclave(args)) - } - } - Some(IndirectCall::BatchAll(calls)) -} diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml new file mode 100644 index 0000000000..e129517dda --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml @@ -0,0 +1,92 @@ +[package] +name = "bc-task-receiver" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# std dependencies +thiserror = { version = "1.0.26", optional = true } +threadpool = { version = "1.8.0", optional = true } + +thiserror_sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } + +futures = { version = "0.3.8", optional = true } +futures_sgx = { package = "futures", git = "https://github.com/mesalock-linux/futures-rs-sgx", optional = true } + +# sgx dependencies +hex-sgx = { package = "hex", git = "https://github.com/mesalock-linux/rust-hex-sgx", tag = "sgx_1.1.3", features = ["sgx_tstd"], optional = true } +sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", features = ["net", "thread"], optional = true } +threadpool_sgx = { git = "https://github.com/mesalock-linux/rust-threadpool-sgx", package = "threadpool", tag = "sgx_1.1.3", optional = true } + +# no_std dependencies +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +log = { version = "0.4", default-features = false } +sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } + +# internal dependencies +frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +ita-sgx-runtime = { path = "../../../app-libs/sgx-runtime", default-features = false } +ita-stf = { path = "../../../app-libs/stf", default-features = false } +itp-enclave-metrics = { path = "../../../core-primitives/enclave-metrics", default-features = false } +itp-extrinsics-factory = { path = "../../../core-primitives/extrinsics-factory", default-features = false } +itp-node-api = { path = "../../../core-primitives/node-api", default-features = false } +itp-ocall-api = { path = "../../../core-primitives/ocall-api", default-features = false } +itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } +itp-sgx-externalities = { path = "../../../core-primitives/substrate-sgx/externalities", default-features = false } +itp-stf-executor = { path = "../../../core-primitives/stf-executor", default-features = false } +itp-stf-state-handler = { path = "../../../core-primitives/stf-state-handler", default-features = false } +itp-storage = { path = "../../../core-primitives/storage", default-features = false } +itp-top-pool-author = { path = "../../../core-primitives/top-pool-author", default-features = false } +itp-types = { path = "../../../core-primitives/types", default-features = false } +itp-utils = { path = "../../../core-primitives/utils", default-features = false } + +# litentry primities +litentry-primitives = { path = "../../../litentry/primitives", default-features = false } + +bc-task-sender = { path = "../bc-task-sender", default-features = false } + +[features] +default = ["std"] +sgx = [ + "threadpool_sgx", + "hex-sgx", + "sgx_tstd", + "bc-task-sender/sgx", + "litentry-primitives/sgx", + "ita-stf/sgx", + "itp-enclave-metrics/sgx", + "itp-extrinsics-factory/sgx", + "itp-node-api/sgx", + "itp-sgx-crypto/sgx", + "itp-sgx-externalities/sgx", + "itp-stf-executor/sgx", + "itp-stf-state-handler/sgx", + "itp-storage/sgx", + "itp-top-pool-author/sgx", + "thiserror_sgx", + "futures_sgx", +] +std = [ + "threadpool", + "log/std", + "bc-task-sender/std", + "litentry-primitives/std", + "ita-sgx-runtime/std", + "ita-stf/std", + "itp-enclave-metrics/std", + "itp-extrinsics-factory/std", + "itp-node-api/std", + "itp-ocall-api/std", + "itp-sgx-crypto/std", + "itp-sgx-externalities/std", + "itp-stf-executor/std", + "itp-stf-state-handler/std", + "itp-storage/std", + "itp-top-pool-author/std", + "itp-types/std", + "itp-utils/std", + "futures", + "thiserror", +] diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs new file mode 100644 index 0000000000..ce76c7bb92 --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs @@ -0,0 +1,118 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +// re-export module to properly feature gate sgx and regular std environment +#[cfg(all(not(feature = "std"), feature = "sgx"))] +pub mod sgx_reexport_prelude { + pub use futures_sgx as futures; + pub use hex_sgx as hex; + pub use thiserror_sgx as thiserror; + pub use threadpool_sgx as threadpool; +} + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +pub use crate::sgx_reexport_prelude::*; + +use bc_task_sender::init_bit_across_task_sender_storage; +use litentry_primitives::AesRequest; +use log::*; +use std::{ + string::{String, ToString}, + sync::Arc, + vec::Vec, +}; +use threadpool::ThreadPool; + +use itp_ocall_api::{EnclaveAttestationOCallApi, EnclaveMetricsOCallApi, EnclaveOnChainOCallApi}; +use itp_sgx_crypto::{key_repository::AccessKey, ShieldingCryptoDecrypt, ShieldingCryptoEncrypt}; +use itp_sgx_externalities::SgxExternalitiesTrait; +use itp_stf_executor::traits::StfEnclaveSigning; +use itp_stf_state_handler::handle_state::HandleState; + +use ita_stf::TrustedCallSigned; + +#[derive(Debug, thiserror::Error, Clone)] +pub enum Error { + #[error("Request error: {0}")] + RequestError(String), + + #[error("Assertion error: {0}")] + AssertionError(String), + + #[error("Other error: {0}")] + OtherError(String), +} + +pub struct BitAcrossTaskContext< + ShieldingKeyRepository, + S: StfEnclaveSigning, + H: HandleState, + O: EnclaveOnChainOCallApi, +> where + ShieldingKeyRepository: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + 'static, +{ + pub shielding_key: Arc, + pub enclave_signer: Arc, + pub state_handler: Arc, + pub ocall_api: Arc, +} + +impl< + ShieldingKeyRepository, + S: StfEnclaveSigning, + H: HandleState, + O: EnclaveOnChainOCallApi, + > BitAcrossTaskContext +where + ShieldingKeyRepository: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + 'static, + H::StateT: SgxExternalitiesTrait, +{ + pub fn new( + shielding_key: Arc, + enclave_signer: Arc, + state_handler: Arc, + ocall_api: Arc, + ) -> Self { + Self { shielding_key, enclave_signer, state_handler, ocall_api } + } +} + +pub fn run_bit_across_handler_runner( + _context: Arc>, +) where + ShieldingKeyRepository: AccessKey + Send + Sync + 'static, + ::KeyType: + ShieldingCryptoEncrypt + ShieldingCryptoDecrypt + 'static, + S: StfEnclaveSigning + Send + Sync + 'static, + H: HandleState + Send + Sync + 'static, + H::StateT: SgxExternalitiesTrait, + O: EnclaveOnChainOCallApi + EnclaveMetricsOCallApi + EnclaveAttestationOCallApi + 'static, +{ + let bit_across_task_receiver = init_bit_across_task_sender_storage(); + let n_workers = 2; + let pool = ThreadPool::new(n_workers); + + while let Ok(mut req) = bit_across_task_receiver.recv() { + pool.execute(move || { + if let Err(e) = req.sender.send(handle_request(&mut req.request)) { + warn!("Unable to submit response back to the handler: {:?}", e); + } + }); + } + + pool.join(); + warn!("bit_across_task_receiver loop terminated"); +} + +pub fn handle_request(_request: &mut AesRequest) -> Result, String> +where +{ + Err("Not Implemented".to_string()) +} diff --git a/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml new file mode 100644 index 0000000000..a1a4c5024d --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml @@ -0,0 +1,37 @@ +[package] +name = "bc-task-sender" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# std dependencies +futures = { version = "0.3.8", optional = true } + +# sgx dependencies +futures_sgx = { package = "futures", git = "https://github.com/mesalock-linux/futures-rs-sgx", optional = true } +sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", features = ["net", "thread"], optional = true } + +# no_std dependencies +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +lazy_static = { version = "1.1.0", features = ["spin_no_std"] } +log = { version = "0.4", default-features = false } + +# litentry primities +litentry-primitives = { path = "../../../litentry/primitives", default-features = false } + +[features] +default = ["std"] +sgx = [ + "futures_sgx", + "sgx_tstd", + "futures_sgx", + "litentry-primitives/sgx", +] +std = [ + "futures", + "log/std", + "futures", + "litentry-primitives/std", +] diff --git a/bitacross-worker/bitacross/core/bc-task-sender/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-sender/src/lib.rs new file mode 100644 index 0000000000..6569bba2b2 --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-task-sender/src/lib.rs @@ -0,0 +1,110 @@ +#![feature(trait_alias)] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +// re-export module to properly feature gate sgx and regular std environment +#[cfg(all(not(feature = "std"), feature = "sgx"))] +pub mod sgx_reexport_prelude { + pub use futures_sgx as futures; +} + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +pub use crate::sgx_reexport_prelude::*; + +use codec::{Decode, Encode}; +use futures::channel::oneshot; +use lazy_static::lazy_static; +use litentry_primitives::AesRequest; +#[cfg(feature = "std")] +use std::sync::Mutex; +#[cfg(feature = "sgx")] +use std::sync::SgxMutex as Mutex; +use std::{ + format, + string::String, + sync::{ + mpsc::{channel, Receiver, Sender as MpscSender}, + Arc, + }, + vec::Vec, +}; + +#[derive(Debug)] +pub struct BitAcrossRequest { + pub sender: oneshot::Sender, String>>, + pub request: AesRequest, +} + +#[derive(Encode, Decode, Clone)] +pub struct BitAcrossResponse { + pub payload: Vec, +} + +pub type BitAcrossSender = MpscSender; + +// Global storage of the sender. Should not be accessed directly. +lazy_static! { + static ref GLOBAL_BIT_ACROSS_TASK_SENDER: Arc>> = + Arc::new(Mutex::new(Default::default())); +} + +pub struct BitAcrossRequestSender {} +impl BitAcrossRequestSender { + pub fn new() -> Self { + Self {} + } +} + +impl Default for BitAcrossRequestSender { + fn default() -> Self { + Self::new() + } +} + +impl BitAcrossRequestSender { + pub fn send(&self, request: BitAcrossRequest) -> Result<(), String> { + // Acquire lock on extrinsic sender + let mutex_guard = GLOBAL_BIT_ACROSS_TASK_SENDER.lock().unwrap(); + let bit_across_task_sender = mutex_guard.clone().unwrap(); + // Release mutex lock, so we don't block the lock longer than necessary. + drop(mutex_guard); + + // Send the request to the receiver loop. + bit_across_task_sender.send(request)?; + + Ok(()) + } +} + +/// Initialization of the extrinsic sender. Needs to be called before any sender access. +pub fn init_bit_across_task_sender_storage() -> Receiver { + let (sender, receiver) = channel(); + // It makes no sense to handle the unwrap, as this statement fails only if the lock has been poisoned + // I believe at that point it is an unrecoverable error + let mut bit_across_task_storage = GLOBAL_BIT_ACROSS_TASK_SENDER.lock().unwrap(); + *bit_across_task_storage = Some(BitAcrossTaskSender::new(sender)); + receiver +} + +/// Wrapping struct around the actual sender. Should not be accessed directly. (unnecessary) +#[derive(Clone, Debug)] +pub struct BitAcrossTaskSender { + sender: BitAcrossSender, +} + +impl BitAcrossTaskSender { + pub fn new(sender: BitAcrossSender) -> Self { + Self { sender } + } + + fn send(&self, request: BitAcrossRequest) -> Result<(), String> { + self.sender + .send(request) + .map_err(|e| format!("Failed to send message to BitAcross Handler: {:?}", e)) + } +} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs index 4bbe919a87..813d1ef979 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs @@ -20,10 +20,11 @@ #![cfg_attr(not(feature = "std"), no_std)] use crate::{ - error::Result, pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, pallet_vcmp::VCMPCallIndexes, + error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, + pallet_imp::IMPCallIndexes, pallet_proxy::ProxyCallIndexes, + pallet_sidechain::SidechainCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teerex::TeerexCallIndexes, pallet_utility::UtilityCallIndexes, + pallet_vcmp::VCMPCallIndexes, }; use codec::{Decode, Encode}; use sp_core::storage::StorageKey; @@ -33,6 +34,7 @@ pub use itp_api_client_types::{Metadata, MetadataError}; pub mod error; pub mod pallet_balances; +pub mod pallet_bitacross; pub mod pallet_imp; pub mod pallet_proxy; pub mod pallet_sidechain; @@ -54,6 +56,7 @@ pub trait NodeMetadataTrait: + UtilityCallIndexes + ProxyCallIndexes + BalancesCallIndexes + + BitAcrossCallIndexes { } impl< @@ -64,7 +67,8 @@ impl< + SystemSs58Prefix + UtilityCallIndexes + ProxyCallIndexes - + BalancesCallIndexes, + + BalancesCallIndexes + + BitAcrossCallIndexes, > NodeMetadataTrait for T { } diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index cdf24e4fcc..a1289c489c 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -16,10 +16,11 @@ */ use crate::{ - error::Result, pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, pallet_vcmp::VCMPCallIndexes, runtime_call::RuntimeCall, + error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, + pallet_imp::IMPCallIndexes, pallet_proxy::ProxyCallIndexes, + pallet_sidechain::SidechainCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teerex::TeerexCallIndexes, pallet_utility::UtilityCallIndexes, + pallet_vcmp::VCMPCallIndexes, runtime_call::RuntimeCall, }; use codec::{Decode, Encode}; @@ -86,6 +87,9 @@ pub struct NodeMetadataMock { transfer_allow_death: u8, runtime_spec_version: u32, runtime_transaction_version: u32, + + bitacross_module: u8, + bitacross_placeholder: u8, } impl NodeMetadataMock { @@ -142,6 +146,9 @@ impl NodeMetadataMock { transfer_allow_death: 0u8, runtime_spec_version: 25, runtime_transaction_version: 4, + + bitacross_module: 69u8, + bitacross_placeholder: 70u8, } } } @@ -315,3 +322,9 @@ impl BalancesCallIndexes for NodeMetadataMock { Ok([self.balances_module, self.transfer_allow_death]) } } + +impl BitAcrossCallIndexes for NodeMetadataMock { + fn placeholder_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.bitacross_module, self.bitacross_placeholder]) + } +} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs new file mode 100644 index 0000000000..a7e5a033ee --- /dev/null +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs @@ -0,0 +1,30 @@ +// Copyright 2020-2023 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +// TODO: maybe use macros to simplify this +use crate::{error::Result, NodeMetadata}; + +const BITACROSS: &str = "BitAcross"; + +pub trait BitAcrossCallIndexes { + fn placeholder_call_indexes(&self) -> Result<[u8; 2]>; +} + +impl BitAcrossCallIndexes for NodeMetadata { + fn placeholder_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(BITACROSS, "placeholder") + } +} diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 5488ba3106..c17f580554 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -139,6 +139,9 @@ litentry-macros = { path = "../../primitives/core/macros", default-features = fa litentry-primitives = { path = "../litentry/primitives", default-features = false, features = ["sgx"] } litentry-proc-macros = { path = "../../primitives/core/proc-macros", default-features = false } +# bitacross +bc-task-receiver = { path = "../bitacross/core/bc-task-receiver", default-features = false, features = ["sgx"] } + # substrate deps frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } frame-system = { optional = true, default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } diff --git a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs index d0aa22e265..5b598ae0e5 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs @@ -180,7 +180,7 @@ pub type IntegriteeParentchainIndirectCallsExecutor = IndirectCallsExecutor< EnclaveStfEnclaveSigner, EnclaveTopPoolAuthor, EnclaveNodeMetadataRepository, - integritee::ShieldFundsAndInvokeFilter, + integritee::BitAcrossIndirectCallsFilter, EventCreator, integritee::ParentchainEventHandler, EnclaveTrustedCallSigned, diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index f97b9e3679..3ba32756d4 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -15,6 +15,8 @@ */ +#![allow(clippy::unwrap_used)] + pub mod global_components; pub mod parentchain; use crate::{ @@ -50,6 +52,7 @@ use crate::{ Hash, }; use base58::ToBase58; +use bc_task_receiver::{run_bit_across_handler_runner, BitAcrossTaskContext}; use codec::Encode; use core::str::FromStr; use ita_stf::{Getter, TrustedCallSigned}; @@ -91,6 +94,7 @@ use log::*; use sgx_types::sgx_status_t; use sp_core::crypto::Pair; use std::{collections::HashMap, path::PathBuf, string::String, sync::Arc}; + pub(crate) fn init_enclave( mu_ra_url: String, untrusted_worker_url: String, @@ -226,6 +230,8 @@ pub(crate) fn init_enclave( Arc::new(IntelAttestationHandler::new(ocall_api, signing_key_repository)); GLOBAL_ATTESTATION_HANDLER_COMPONENT.initialize(attestation_handler); + std::thread::spawn(move || run_bit_across_handler().unwrap()); + Ok(()) } @@ -244,6 +250,31 @@ fn initialize_state_observer( Ok(Arc::new(EnclaveStateObserver::from_map(states_map))) } +fn run_bit_across_handler() -> Result<(), Error> { + let author_api = GLOBAL_TOP_POOL_AUTHOR_COMPONENT.get()?; + let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; + let state_observer = GLOBAL_STATE_OBSERVER_COMPONENT.get()?; + + let shielding_key_repository = GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT.get()?; + #[allow(clippy::unwrap_used)] + let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; + let stf_enclave_signer = Arc::new(EnclaveStfEnclaveSigner::new( + state_observer, + ocall_api.clone(), + shielding_key_repository.clone(), + author_api, + )); + + let stf_task_context = BitAcrossTaskContext::new( + shielding_key_repository, + stf_enclave_signer, + state_handler, + ocall_api, + ); + run_bit_across_handler_runner(Arc::new(stf_task_context)); + Ok(()) +} + pub(crate) fn init_enclave_sidechain_components( fail_mode: Option, fail_at: u64, diff --git a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs index 22776fbd39..236b437d27 100644 --- a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -150,7 +150,7 @@ pub fn submit_shielding_call_to_top_pool() { _, _, _, - integritee::ShieldFundsAndInvokeFilter, + integritee::BitAcrossIndirectCallsFilter, TestEventCreator, integritee::ParentchainEventHandler, TrustedCallSigned, diff --git a/bitacross-worker/sidechain/rpc-handler/Cargo.toml b/bitacross-worker/sidechain/rpc-handler/Cargo.toml index 58cf470bf7..8ac4d88b9e 100644 --- a/bitacross-worker/sidechain/rpc-handler/Cargo.toml +++ b/bitacross-worker/sidechain/rpc-handler/Cargo.toml @@ -33,6 +33,9 @@ codec = { package = "parity-scale-codec", version = "3.0.0", default-features = log = { version = "0.4", default-features = false } sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +# bitacross +bc-task-sender = { path = "../../bitacross/core/bc-task-sender", default-features = false } + [features] default = ["std"] std = [ @@ -46,6 +49,7 @@ std = [ "jsonrpc-core", "log/std", "rust-base58", + "bc-task-sender/std", ] sgx = [ "futures_sgx", @@ -54,4 +58,5 @@ sgx = [ "itp-top-pool-author/sgx", "jsonrpc-core_sgx", "rust-base58_sgx", + "bc-task-sender/sgx", ] diff --git a/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs b/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs index 5de9ed776a..021f565c17 100644 --- a/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs +++ b/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs @@ -25,7 +25,9 @@ use rust_base58::base58::FromBase58; #[cfg(feature = "sgx")] use base58::FromBase58; +use bc_task_sender::{BitAcrossRequest, BitAcrossRequestSender}; use codec::{Decode, Encode}; +use futures::channel::oneshot; use itp_rpc::RpcReturnValue; use itp_stf_primitives::types::AccountId; use itp_top_pool_author::traits::AuthorApi; @@ -55,6 +57,18 @@ where G: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, { let watch_author = top_pool_author.clone(); + + // Submit BitAcross Request + io_handler.add_method("bitacross_submitRequest", move |params: Params| { + debug!("worker_api_direct rpc was called: bitacross_submitRequest"); + async move { + let json_value = match request_bit_across_inner(params).await { + Ok(value) => value.to_hex(), + Err(error) => compute_hex_encoded_return_error(&error), + }; + Ok(json!(json_value)) + } + }); io_handler.add_sync_method("author_submitAndWatchRsaRequest", move |params: Params| { debug!("worker_api_direct rpc was called: author_submitAndWatchRsaRequest"); let json_value = match author_submit_extrinsic_inner( @@ -318,3 +332,28 @@ where response.map_err(|e| format!("{:?}", e)) } + +async fn request_bit_across_inner(params: Params) -> Result { + let payload = get_request_payload(params)?; + let request = AesRequest::from_hex(&payload) + .map_err(|e| format!("AesRequest construction error: {:?}", e))?; + + let bit_across_request_sender = BitAcrossRequestSender::new(); + let (sender, receiver) = oneshot::channel::, String>>(); + + bit_across_request_sender.send(BitAcrossRequest { sender, request })?; + + // we only expect one response, hence no loop + match receiver.await { + Ok(Ok(response)) => + Ok(RpcReturnValue { do_watch: false, value: response, status: DirectRequestStatus::Ok }), + Ok(Err(e)) => { + log::error!("Received error in jsonresponse: {:?} ", e); + Err(compute_hex_encoded_return_error(&e)) + }, + Err(_) => { + // This case will only happen if the sender has been dropped + Err(compute_hex_encoded_return_error("The sender has been dropped")) + }, + } +} diff --git a/pallets/bitacross-pallet/Cargo.toml b/pallets/bitacross-pallet/Cargo.toml new file mode 100644 index 0000000000..d52a5f6541 --- /dev/null +++ b/pallets/bitacross-pallet/Cargo.toml @@ -0,0 +1,68 @@ +[package] +authors = ['Trust Computing GmbH '] +edition = '2021' +homepage = 'https://litentry.com' +name = "pallet-bitacross" +repository = 'https://github.com/litentry/litentry-parachain' +version = '0.1.0' + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +# third-party dependencies +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } +pallet-teerex = { path = "../teerex", default-features = false } +scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } + +# primitives +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } + +# frame dependencies +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } + +# benchmarking +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false, optional = true } +test-utils = { path = "../test-utils", default-features = false, optional = true } + +# local +core-primitives = { path = "../../primitives/core", default-features = false } +teerex-primitives = { path = "../../primitives/teerex", default-features = false } + +[dev-dependencies] +pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } +pallet-group = { path = "../../pallets/group" } +pallet-teerex = { path = "../teerex" } +pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } +test-utils = { path = "../test-utils" } + +[features] +default = ["std"] +runtime-benchmarks = [ + "frame-benchmarking/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "frame-system/runtime-benchmarks", + "pallet-timestamp/runtime-benchmarks", + "test-utils", +] +skip-ias-check = [ + "pallet-teerex/skip-ias-check", +] +std = [ + "codec/std", + "scale-info/std", + "sp-std/std", + "sp-runtime/std", + "sp-io/std", + "sp-core/std", + "frame-support/std", + "frame-system/std", + "frame-benchmarking?/std", + "core-primitives/std", + "teerex-primitives/std", + "pallet-teerex/std", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/bitacross-pallet/src/lib.rs b/pallets/bitacross-pallet/src/lib.rs new file mode 100644 index 0000000000..b33abf6a4f --- /dev/null +++ b/pallets/bitacross-pallet/src/lib.rs @@ -0,0 +1,68 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +//! TODO: event/error handling +//! Currently the errors are synchronously emitted from this pallet itself, +//! meanwhile we have the `SomeError` **Event** which is callable from TEE +//! to represent any generic "error". +//! However, there are so many error cases in TEE that I'm not even sure +//! if it's a good idea to have a matching extrinsic for error propagation. +//! +//! The reasons that we don't use pallet_teerex::call_worker directly are: +//! - call teerex::call_worker inside IMP won't trigger the handler, because it's not called as +//! extrinsics so won't be scraped +//! - the origin is discarded in call_worker but we need it +//! - to simplify the F/E usage, we only need to encrypt the needed parameters (see e.g. +//! shield_funds) + +#![cfg_attr(not(feature = "std"), no_std)] +#![allow(unused_variables)] +#![allow(clippy::let_unit_value, deprecated)] + +pub use pallet::*; +#[frame_support::pallet] +pub mod pallet { + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + // some extrinsics should only be called by origins from TEE + type TEECallOrigin: EnsureOrigin; + // origin that is allowed to call extrinsics + type ExtrinsicWhitelistOrigin: EnsureOrigin; + } + + #[pallet::event] + pub enum Event {} + + #[pallet::error] + pub enum Error {} + + #[pallet::call] + impl Pallet { + /// add an account to the delegatees + #[pallet::call_index(0)] + #[pallet::weight(10000000)] + pub fn placeholder(origin: OriginFor, account: T::AccountId) -> DispatchResult { + Ok(()) + } + } +} diff --git a/runtime/rococo/Cargo.toml b/runtime/rococo/Cargo.toml index b896739e60..e8c0c0bd73 100644 --- a/runtime/rococo/Cargo.toml +++ b/runtime/rococo/Cargo.toml @@ -88,6 +88,7 @@ frame-system-benchmarking = { git = "https://github.com/paritytech/substrate", d # Rococo pallets pallet-account-fix = { path = "../../pallets/account-fix", default-features = false } pallet-asset-manager = { path = "../../pallets/xcm-asset-manager", default-features = false } +pallet-bitacross = { path = "../../pallets/bitacross-pallet", default-features = false } pallet-bridge = { path = "../../pallets/bridge", default-features = false } pallet-bridge-transfer = { path = "../../pallets/bridge-transfer", default-features = false } pallet-drop3 = { path = "../../pallets/drop3", default-features = false } @@ -277,6 +278,7 @@ std = [ "moonbeam-evm-tracer/std", "moonbeam-rpc-primitives-debug/std", "moonbeam-rpc-primitives-txpool/std", + "pallet-bitacross/std", ] tee-dev = [ "pallet-teerex/skip-ias-check", @@ -331,4 +333,5 @@ try-runtime = [ "pallet-xcm/try-runtime", "parachain-info/try-runtime", "pallet-account-fix/try-runtime", + "pallet-bitacross/try-runtime", ] diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 55b5992490..627a32d369 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -17,7 +17,7 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::identity_op)] // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. -#![recursion_limit = "256"] +#![recursion_limit = "512"] #[cfg(feature = "runtime-benchmarks")] #[macro_use] @@ -1039,6 +1039,13 @@ impl pallet_identity_management::Config for Runtime { type ExtrinsicWhitelistOrigin = IMPExtrinsicWhitelist; } +// NOTE: Use this for bitacross-pallet +impl pallet_bitacross::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type TEECallOrigin = EnsureEnclaveSigner; + type ExtrinsicWhitelistOrigin = IMPExtrinsicWhitelist; +} + impl pallet_group::Config for Runtime { type RuntimeEvent = RuntimeEvent; type GroupManagerOrigin = EnsureRootOrAllCouncil; @@ -1257,6 +1264,7 @@ construct_runtime! { VCManagement: pallet_vc_management = 66, IMPExtrinsicWhitelist: pallet_group:: = 67, VCMPExtrinsicWhitelist: pallet_group:: = 68, + BitAcross: pallet_bitacross = 70, // TEE Teerex: pallet_teerex = 90, From ed1ebe2d1ef633386727af8ef9262e251d769b12 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 5 Feb 2024 23:55:28 +0100 Subject: [PATCH 13/64] Bump tokio from 1.35.1 to 1.36.0 (#2465) Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.35.1 to 1.36.0. - [Release notes](https://github.com/tokio-rs/tokio/releases) - [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.1...tokio-1.36.0) --- updated-dependencies: - dependency-name: tokio dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 356 ++++++++++++++++++++++++------------------------ node/Cargo.toml | 2 +- 2 files changed, 179 insertions(+), 179 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2e217c5596..9a38f33515 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -570,7 +570,7 @@ dependencies = [ [[package]] name = "binary-merkle-tree" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -3191,7 +3191,7 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] name = "fork-tree" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", ] @@ -3312,7 +3312,7 @@ checksum = "6c2141d6d6c8512188a7891b4b01590a45f6dac67afb4f255c4124dbb86d4eaa" [[package]] name = "frame-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-support-procedural", @@ -3337,7 +3337,7 @@ dependencies = [ [[package]] name = "frame-benchmarking-cli" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "array-bytes 4.2.0", @@ -3384,7 +3384,7 @@ dependencies = [ [[package]] name = "frame-election-provider-solution-type" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -3395,7 +3395,7 @@ dependencies = [ [[package]] name = "frame-election-provider-support" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-election-provider-solution-type", "frame-support", @@ -3412,7 +3412,7 @@ dependencies = [ [[package]] name = "frame-executive" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -3441,7 +3441,7 @@ dependencies = [ [[package]] name = "frame-remote-externalities" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-recursion", "futures 0.3.30", @@ -3461,7 +3461,7 @@ dependencies = [ [[package]] name = "frame-support" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bitflags 1.3.2", "environmental", @@ -3494,7 +3494,7 @@ dependencies = [ [[package]] name = "frame-support-procedural" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "cfg-expr", @@ -3510,7 +3510,7 @@ dependencies = [ [[package]] name = "frame-support-procedural-tools" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support-procedural-tools-derive", "proc-macro-crate", @@ -3522,7 +3522,7 @@ dependencies = [ [[package]] name = "frame-support-procedural-tools-derive" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -3532,7 +3532,7 @@ dependencies = [ [[package]] name = "frame-system" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "log", @@ -3550,7 +3550,7 @@ dependencies = [ [[package]] name = "frame-system-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -3565,7 +3565,7 @@ dependencies = [ [[package]] name = "frame-system-rpc-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -3574,7 +3574,7 @@ dependencies = [ [[package]] name = "frame-try-runtime" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "parity-scale-codec", @@ -5854,7 +5854,7 @@ dependencies = [ [[package]] name = "mmr-gadget" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "log", @@ -5873,7 +5873,7 @@ dependencies = [ [[package]] name = "mmr-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "jsonrpsee", @@ -6791,7 +6791,7 @@ dependencies = [ [[package]] name = "pallet-aura" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6807,7 +6807,7 @@ dependencies = [ [[package]] name = "pallet-authority-discovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6823,7 +6823,7 @@ dependencies = [ [[package]] name = "pallet-authorship" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6837,7 +6837,7 @@ dependencies = [ [[package]] name = "pallet-babe" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -6861,7 +6861,7 @@ dependencies = [ [[package]] name = "pallet-bags-list" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -6881,7 +6881,7 @@ dependencies = [ [[package]] name = "pallet-balances" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -6896,7 +6896,7 @@ dependencies = [ [[package]] name = "pallet-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -6915,7 +6915,7 @@ dependencies = [ [[package]] name = "pallet-beefy-mmr" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "binary-merkle-tree", @@ -6961,7 +6961,7 @@ dependencies = [ [[package]] name = "pallet-bounties" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7017,7 +7017,7 @@ dependencies = [ [[package]] name = "pallet-child-bounties" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7055,7 +7055,7 @@ dependencies = [ [[package]] name = "pallet-collective" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7072,7 +7072,7 @@ dependencies = [ [[package]] name = "pallet-conviction-voting" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "assert_matches", "frame-benchmarking", @@ -7089,7 +7089,7 @@ dependencies = [ [[package]] name = "pallet-democracy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7123,7 +7123,7 @@ dependencies = [ [[package]] name = "pallet-election-provider-multi-phase" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7146,7 +7146,7 @@ dependencies = [ [[package]] name = "pallet-election-provider-support-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7159,7 +7159,7 @@ dependencies = [ [[package]] name = "pallet-elections-phragmen" version = "5.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7381,7 +7381,7 @@ dependencies = [ [[package]] name = "pallet-fast-unstake" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7399,7 +7399,7 @@ dependencies = [ [[package]] name = "pallet-grandpa" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7438,7 +7438,7 @@ dependencies = [ [[package]] name = "pallet-identity" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "enumflags2", "frame-benchmarking", @@ -7476,7 +7476,7 @@ dependencies = [ [[package]] name = "pallet-im-online" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7496,7 +7496,7 @@ dependencies = [ [[package]] name = "pallet-indices" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7513,7 +7513,7 @@ dependencies = [ [[package]] name = "pallet-membership" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7530,7 +7530,7 @@ dependencies = [ [[package]] name = "pallet-mmr" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7547,7 +7547,7 @@ dependencies = [ [[package]] name = "pallet-multisig" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7563,7 +7563,7 @@ dependencies = [ [[package]] name = "pallet-nis" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7579,7 +7579,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools" version = "1.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7596,7 +7596,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools-benchmarking" version = "1.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7616,7 +7616,7 @@ dependencies = [ [[package]] name = "pallet-nomination-pools-runtime-api" version = "1.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "pallet-nomination-pools", "parity-scale-codec", @@ -7627,7 +7627,7 @@ dependencies = [ [[package]] name = "pallet-offences" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7644,7 +7644,7 @@ dependencies = [ [[package]] name = "pallet-offences-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7708,7 +7708,7 @@ dependencies = [ [[package]] name = "pallet-preimage" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7725,7 +7725,7 @@ dependencies = [ [[package]] name = "pallet-proxy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7740,7 +7740,7 @@ dependencies = [ [[package]] name = "pallet-ranked-collective" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7758,7 +7758,7 @@ dependencies = [ [[package]] name = "pallet-recovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7773,7 +7773,7 @@ dependencies = [ [[package]] name = "pallet-referenda" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "assert_matches", "frame-benchmarking", @@ -7792,7 +7792,7 @@ dependencies = [ [[package]] name = "pallet-scheduler" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7809,7 +7809,7 @@ dependencies = [ [[package]] name = "pallet-session" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7830,7 +7830,7 @@ dependencies = [ [[package]] name = "pallet-session-benchmarking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7872,7 +7872,7 @@ dependencies = [ [[package]] name = "pallet-society" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -7886,7 +7886,7 @@ dependencies = [ [[package]] name = "pallet-staking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-election-provider-support", @@ -7909,7 +7909,7 @@ dependencies = [ [[package]] name = "pallet-staking-reward-curve" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -7920,7 +7920,7 @@ dependencies = [ [[package]] name = "pallet-staking-reward-fn" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sp-arithmetic", @@ -7929,7 +7929,7 @@ dependencies = [ [[package]] name = "pallet-staking-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -7938,7 +7938,7 @@ dependencies = [ [[package]] name = "pallet-state-trie-migration" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -7955,7 +7955,7 @@ dependencies = [ [[package]] name = "pallet-sudo" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -8052,7 +8052,7 @@ dependencies = [ [[package]] name = "pallet-timestamp" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8070,7 +8070,7 @@ dependencies = [ [[package]] name = "pallet-tips" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8089,7 +8089,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-support", "frame-system", @@ -8105,7 +8105,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "pallet-transaction-payment-rpc-runtime-api", @@ -8121,7 +8121,7 @@ dependencies = [ [[package]] name = "pallet-transaction-payment-rpc-runtime-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "pallet-transaction-payment", "parity-scale-codec", @@ -8133,7 +8133,7 @@ dependencies = [ [[package]] name = "pallet-treasury" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8150,7 +8150,7 @@ dependencies = [ [[package]] name = "pallet-utility" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8188,7 +8188,7 @@ dependencies = [ [[package]] name = "pallet-vesting" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -8203,7 +8203,7 @@ dependencies = [ [[package]] name = "pallet-whitelist" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-benchmarking", "frame-support", @@ -11035,7 +11035,7 @@ dependencies = [ [[package]] name = "sc-allocator" version = "4.1.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sp-core", @@ -11046,7 +11046,7 @@ dependencies = [ [[package]] name = "sc-authority-discovery" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11074,7 +11074,7 @@ dependencies = [ [[package]] name = "sc-basic-authorship" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "futures-timer", @@ -11097,7 +11097,7 @@ dependencies = [ [[package]] name = "sc-block-builder" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sc-client-api", @@ -11112,7 +11112,7 @@ dependencies = [ [[package]] name = "sc-chain-spec" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "memmap2", "sc-chain-spec-derive", @@ -11131,7 +11131,7 @@ dependencies = [ [[package]] name = "sc-chain-spec-derive" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -11142,7 +11142,7 @@ dependencies = [ [[package]] name = "sc-cli" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "chrono", @@ -11182,7 +11182,7 @@ dependencies = [ [[package]] name = "sc-client-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "fnv", "futures 0.3.30", @@ -11208,7 +11208,7 @@ dependencies = [ [[package]] name = "sc-client-db" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "kvdb", @@ -11234,7 +11234,7 @@ dependencies = [ [[package]] name = "sc-consensus" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11259,7 +11259,7 @@ dependencies = [ [[package]] name = "sc-consensus-aura" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11288,7 +11288,7 @@ dependencies = [ [[package]] name = "sc-consensus-babe" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "fork-tree", @@ -11324,7 +11324,7 @@ dependencies = [ [[package]] name = "sc-consensus-babe-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11346,7 +11346,7 @@ dependencies = [ [[package]] name = "sc-consensus-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11381,7 +11381,7 @@ dependencies = [ [[package]] name = "sc-consensus-beefy-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11400,7 +11400,7 @@ dependencies = [ [[package]] name = "sc-consensus-epochs" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "fork-tree", "parity-scale-codec", @@ -11413,7 +11413,7 @@ dependencies = [ [[package]] name = "sc-consensus-grandpa" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "array-bytes 4.2.0", @@ -11453,7 +11453,7 @@ dependencies = [ [[package]] name = "sc-consensus-grandpa-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "finality-grandpa", "futures 0.3.30", @@ -11473,7 +11473,7 @@ dependencies = [ [[package]] name = "sc-consensus-slots" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -11496,7 +11496,7 @@ dependencies = [ [[package]] name = "sc-executor" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lru 0.8.1", "parity-scale-codec", @@ -11520,7 +11520,7 @@ dependencies = [ [[package]] name = "sc-executor-common" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sc-allocator", "sp-maybe-compressed-blob", @@ -11533,7 +11533,7 @@ dependencies = [ [[package]] name = "sc-executor-wasmi" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "sc-allocator", @@ -11546,7 +11546,7 @@ dependencies = [ [[package]] name = "sc-executor-wasmtime" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "cfg-if", @@ -11564,7 +11564,7 @@ dependencies = [ [[package]] name = "sc-informant" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "futures 0.3.30", @@ -11580,7 +11580,7 @@ dependencies = [ [[package]] name = "sc-keystore" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11595,7 +11595,7 @@ dependencies = [ [[package]] name = "sc-network" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-channel", @@ -11640,7 +11640,7 @@ dependencies = [ [[package]] name = "sc-network-bitswap" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "cid", "futures 0.3.30", @@ -11660,7 +11660,7 @@ dependencies = [ [[package]] name = "sc-network-common" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11688,7 +11688,7 @@ dependencies = [ [[package]] name = "sc-network-gossip" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "futures 0.3.30", @@ -11707,7 +11707,7 @@ dependencies = [ [[package]] name = "sc-network-light" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11729,7 +11729,7 @@ dependencies = [ [[package]] name = "sc-network-sync" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "async-trait", @@ -11763,7 +11763,7 @@ dependencies = [ [[package]] name = "sc-network-transactions" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11783,7 +11783,7 @@ dependencies = [ [[package]] name = "sc-offchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "bytes", @@ -11814,7 +11814,7 @@ dependencies = [ [[package]] name = "sc-peerset" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "libp2p", @@ -11827,7 +11827,7 @@ dependencies = [ [[package]] name = "sc-proposer-metrics" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "substrate-prometheus-endpoint", @@ -11836,7 +11836,7 @@ dependencies = [ [[package]] name = "sc-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "jsonrpsee", @@ -11866,7 +11866,7 @@ dependencies = [ [[package]] name = "sc-rpc-api" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "parity-scale-codec", @@ -11885,7 +11885,7 @@ dependencies = [ [[package]] name = "sc-rpc-server" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "http", "jsonrpsee", @@ -11900,7 +11900,7 @@ dependencies = [ [[package]] name = "sc-rpc-spec-v2" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "futures 0.3.30", @@ -11926,7 +11926,7 @@ dependencies = [ [[package]] name = "sc-service" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "directories", @@ -11992,7 +11992,7 @@ dependencies = [ [[package]] name = "sc-state-db" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "log", "parity-scale-codec", @@ -12003,7 +12003,7 @@ dependencies = [ [[package]] name = "sc-storage-monitor" version = "0.1.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "clap", "fs4", @@ -12019,7 +12019,7 @@ dependencies = [ [[package]] name = "sc-sync-state-rpc" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "parity-scale-codec", @@ -12038,7 +12038,7 @@ dependencies = [ [[package]] name = "sc-sysinfo" version = "6.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "libc", @@ -12057,7 +12057,7 @@ dependencies = [ [[package]] name = "sc-telemetry" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "chrono", "futures 0.3.30", @@ -12076,7 +12076,7 @@ dependencies = [ [[package]] name = "sc-tracing" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "atty", @@ -12107,7 +12107,7 @@ dependencies = [ [[package]] name = "sc-tracing-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -12118,7 +12118,7 @@ dependencies = [ [[package]] name = "sc-transaction-pool" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12145,7 +12145,7 @@ dependencies = [ [[package]] name = "sc-transaction-pool-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12159,7 +12159,7 @@ dependencies = [ [[package]] name = "sc-utils" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-channel", "futures 0.3.30", @@ -12711,7 +12711,7 @@ dependencies = [ [[package]] name = "sp-api" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -12731,7 +12731,7 @@ dependencies = [ [[package]] name = "sp-api-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "blake2", @@ -12745,7 +12745,7 @@ dependencies = [ [[package]] name = "sp-application-crypto" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12758,7 +12758,7 @@ dependencies = [ [[package]] name = "sp-arithmetic" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "integer-sqrt", "num-traits", @@ -12772,7 +12772,7 @@ dependencies = [ [[package]] name = "sp-authority-discovery" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12785,7 +12785,7 @@ dependencies = [ [[package]] name = "sp-block-builder" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-api", @@ -12797,7 +12797,7 @@ dependencies = [ [[package]] name = "sp-blockchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "log", @@ -12815,7 +12815,7 @@ dependencies = [ [[package]] name = "sp-consensus" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures 0.3.30", @@ -12830,7 +12830,7 @@ dependencies = [ [[package]] name = "sp-consensus-aura" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "parity-scale-codec", @@ -12848,7 +12848,7 @@ dependencies = [ [[package]] name = "sp-consensus-babe" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "parity-scale-codec", @@ -12869,7 +12869,7 @@ dependencies = [ [[package]] name = "sp-consensus-beefy" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lazy_static", "parity-scale-codec", @@ -12888,7 +12888,7 @@ dependencies = [ [[package]] name = "sp-consensus-grandpa" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "finality-grandpa", "log", @@ -12906,7 +12906,7 @@ dependencies = [ [[package]] name = "sp-consensus-slots" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -12918,7 +12918,7 @@ dependencies = [ [[package]] name = "sp-core" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "array-bytes 4.2.0", "bitflags 1.3.2", @@ -12962,7 +12962,7 @@ dependencies = [ [[package]] name = "sp-core-hashing" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "blake2b_simd", "byteorder", @@ -12976,7 +12976,7 @@ dependencies = [ [[package]] name = "sp-core-hashing-proc-macro" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -12987,7 +12987,7 @@ dependencies = [ [[package]] name = "sp-database" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "kvdb", "parking_lot 0.12.1", @@ -12996,7 +12996,7 @@ dependencies = [ [[package]] name = "sp-debug-derive" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "proc-macro2", "quote", @@ -13006,7 +13006,7 @@ dependencies = [ [[package]] name = "sp-externalities" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "environmental", "parity-scale-codec", @@ -13017,7 +13017,7 @@ dependencies = [ [[package]] name = "sp-inherents" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "impl-trait-for-tuples", @@ -13032,7 +13032,7 @@ dependencies = [ [[package]] name = "sp-io" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bytes", "ed25519", @@ -13058,7 +13058,7 @@ dependencies = [ [[package]] name = "sp-keyring" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "lazy_static", "sp-core", @@ -13069,7 +13069,7 @@ dependencies = [ [[package]] name = "sp-keystore" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "futures 0.3.30", "parity-scale-codec", @@ -13083,7 +13083,7 @@ dependencies = [ [[package]] name = "sp-maybe-compressed-blob" version = "4.1.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "thiserror", "zstd 0.12.4", @@ -13092,7 +13092,7 @@ dependencies = [ [[package]] name = "sp-metadata-ir" version = "0.1.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-metadata", "parity-scale-codec", @@ -13103,7 +13103,7 @@ dependencies = [ [[package]] name = "sp-mmr-primitives" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ckb-merkle-mountain-range", "log", @@ -13121,7 +13121,7 @@ dependencies = [ [[package]] name = "sp-npos-elections" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13135,7 +13135,7 @@ dependencies = [ [[package]] name = "sp-offchain" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sp-api", "sp-core", @@ -13145,7 +13145,7 @@ dependencies = [ [[package]] name = "sp-panic-handler" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "backtrace", "lazy_static", @@ -13155,7 +13155,7 @@ dependencies = [ [[package]] name = "sp-rpc" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "rustc-hash", "serde", @@ -13165,7 +13165,7 @@ dependencies = [ [[package]] name = "sp-runtime" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "either", "hash256-std-hasher", @@ -13187,7 +13187,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "bytes", "impl-trait-for-tuples", @@ -13205,7 +13205,7 @@ dependencies = [ [[package]] name = "sp-runtime-interface-proc-macro" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "Inflector", "proc-macro-crate", @@ -13217,7 +13217,7 @@ dependencies = [ [[package]] name = "sp-session" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13231,7 +13231,7 @@ dependencies = [ [[package]] name = "sp-staking" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13244,7 +13244,7 @@ dependencies = [ [[package]] name = "sp-state-machine" version = "0.13.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hash-db 0.16.0", "log", @@ -13264,12 +13264,12 @@ dependencies = [ [[package]] name = "sp-std" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" [[package]] name = "sp-storage" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "impl-serde", "parity-scale-codec", @@ -13282,7 +13282,7 @@ dependencies = [ [[package]] name = "sp-timestamp" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "futures-timer", @@ -13297,7 +13297,7 @@ dependencies = [ [[package]] name = "sp-tracing" version = "6.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "sp-std", @@ -13309,7 +13309,7 @@ dependencies = [ [[package]] name = "sp-transaction-pool" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "sp-api", "sp-runtime", @@ -13318,7 +13318,7 @@ dependencies = [ [[package]] name = "sp-transaction-storage-proof" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "log", @@ -13334,7 +13334,7 @@ dependencies = [ [[package]] name = "sp-trie" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ahash 0.8.3", "hash-db 0.16.0", @@ -13357,7 +13357,7 @@ dependencies = [ [[package]] name = "sp-version" version = "5.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "impl-serde", "parity-scale-codec", @@ -13374,7 +13374,7 @@ dependencies = [ [[package]] name = "sp-version-proc-macro" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "proc-macro2", @@ -13385,7 +13385,7 @@ dependencies = [ [[package]] name = "sp-wasm-interface" version = "7.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "anyhow", "impl-trait-for-tuples", @@ -13399,7 +13399,7 @@ dependencies = [ [[package]] name = "sp-weights" version = "4.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", "scale-info", @@ -13629,7 +13629,7 @@ dependencies = [ [[package]] name = "substrate-build-script-utils" version = "3.0.0" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "platforms 2.0.0", ] @@ -13658,7 +13658,7 @@ dependencies = [ [[package]] name = "substrate-frame-rpc-system" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "frame-system-rpc-runtime-api", "futures 0.3.30", @@ -13677,7 +13677,7 @@ dependencies = [ [[package]] name = "substrate-prometheus-endpoint" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "hyper", "log", @@ -13689,7 +13689,7 @@ dependencies = [ [[package]] name = "substrate-rpc-client" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "jsonrpsee", @@ -13702,7 +13702,7 @@ dependencies = [ [[package]] name = "substrate-state-trie-migration-rpc" version = "4.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "jsonrpsee", "log", @@ -13721,7 +13721,7 @@ dependencies = [ [[package]] name = "substrate-wasm-builder" version = "5.0.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "ansi_term", "build-helper", @@ -14033,9 +14033,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.35.1" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c89b4efa943be685f629b149f53829423f8f5531ea21249408e8e2f8671ec104" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -14406,7 +14406,7 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" [[package]] name = "try-runtime-cli" version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" +source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "async-trait", "clap", diff --git a/node/Cargo.toml b/node/Cargo.toml index de0ebf6ed6..3b9fff4981 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -22,7 +22,7 @@ futures = { version = "0.3.30", features = ["compat"] } log = "0.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" -tokio = { version = "1.35.1", features = ["macros", "sync"] } +tokio = { version = "1.36.0", features = ["macros", "sync"] } # Substrate dependencies sc-basic-authorship = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } From 94bd3a7b67c7c20b99e52ffc1612375e39f73479 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Tue, 6 Feb 2024 09:22:10 +0100 Subject: [PATCH 14/64] P 419 deploy automation (#2418) * Small update to GHA and versioning * use DOCKER_TAG * set env * fix bugs in deploy sh * update the package version of sidechain-api * refactor deploy.sh and based on deploy.sh, create new prod_deploy.sh * refactor deploy.sh and based on deploy.sh, create new prod_deploy.sh * rm not used files * check and load env file for worker * minor improvement during debugging * move production deploy to private repo * move development deploy to private repo * This should only be a warn message. * fix: load local file --------- Co-authored-by: Kailai Wang Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> Co-authored-by: Zhouhui Tian <125243011+zhouhuitian@users.noreply.github.com> --- .../sidechain/consensus/slots/src/lib.rs | 2 +- .../core-primitives/settings/src/lib.rs | 4 +- .../enclave-runtime/src/top_pool_execution.rs | 4 +- tee-worker/local-setup/.env.dev | 2 +- tee-worker/local-setup/internal-one.json | 27 + tee-worker/local-setup/launch.py | 10 +- .../scripts/litentry/release/config.json.eg | 11 - tee-worker/scripts/litentry/release/deploy.sh | 555 ------------------ .../scripts/litentry/release/prepare.sh | 50 -- .../litentry/release/template/worker.service | 4 +- 10 files changed, 44 insertions(+), 625 deletions(-) create mode 100644 tee-worker/local-setup/internal-one.json delete mode 100644 tee-worker/scripts/litentry/release/config.json.eg delete mode 100755 tee-worker/scripts/litentry/release/deploy.sh delete mode 100755 tee-worker/scripts/litentry/release/prepare.sh diff --git a/bitacross-worker/sidechain/consensus/slots/src/lib.rs b/bitacross-worker/sidechain/consensus/slots/src/lib.rs index 9c22327580..14ed2b6d21 100644 --- a/bitacross-worker/sidechain/consensus/slots/src/lib.rs +++ b/bitacross-worker/sidechain/consensus/slots/src/lib.rs @@ -497,7 +497,7 @@ pub trait SimpleSlotWorker { }; if is_single_worker { - error!("Running as single worker, skipping timestamp within slot check") + warn!("Running as single worker, skipping timestamp within slot check") } else if !timestamp_within_slot(&slot_info, &proposing.block) { warn!( target: logging_target, diff --git a/tee-worker/core-primitives/settings/src/lib.rs b/tee-worker/core-primitives/settings/src/lib.rs index bc3ca98dcf..81a6bd440a 100644 --- a/tee-worker/core-primitives/settings/src/lib.rs +++ b/tee-worker/core-primitives/settings/src/lib.rs @@ -42,8 +42,8 @@ pub mod files { pub static SIDECHAIN_PURGE_LIMIT: u64 = 100; // keep the last.. sidechainblocks when purging // used by enclave - /// Path to the light-client db for the Integritee parentchain. - pub const LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "integritee_lcdb"; + /// Path to the light-client db for the Litentry parentchain. + pub const LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "litentry_lcdb"; /// Path to the light-client db for the Target A parentchain. pub const TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "target_a_lcdb"; diff --git a/tee-worker/enclave-runtime/src/top_pool_execution.rs b/tee-worker/enclave-runtime/src/top_pool_execution.rs index e80611707b..3f45673f81 100644 --- a/tee-worker/enclave-runtime/src/top_pool_execution.rs +++ b/tee-worker/enclave-runtime/src/top_pool_execution.rs @@ -99,8 +99,8 @@ pub unsafe extern "C" fn execute_trusted_calls() -> sgx_status_t { fn execute_top_pool_trusted_calls_internal() -> Result<()> { let start_time = Instant::now(); - debug!("----------------------------------------"); - debug!("Start sidechain block production cycle"); + info!("----------------------------------------"); + info!("Start sidechain block production cycle"); // We acquire lock explicitly (variable binding), since '_' will drop the lock after the statement. // See https://medium.com/codechain/rust-underscore-does-not-bind-fec6a18115a8 diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index 668e449b65..1e30335fe5 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -30,7 +30,7 @@ DISCORD_AUTH_TOKEN= ACHAINABLE_AUTH_KEY= ONEBLOCK_NOTION_KEY= NODEREAL_API_KEY= -GENIIDATA_API_KEY=142cf1b0-1ca7-11ee-bb5e-9d74c2e854ac +GENIIDATA_API_KEY= # The followings are default value. # Can be skipped; or overwrite within non-production mode. diff --git a/tee-worker/local-setup/internal-one.json b/tee-worker/local-setup/internal-one.json new file mode 100644 index 0000000000..5882223148 --- /dev/null +++ b/tee-worker/local-setup/internal-one.json @@ -0,0 +1,27 @@ +{ + "workers": [ + { + "source": "bin", + "flags": [ + "--clean-reset", + "--ws-external", + "-P", + "2000", + "-w", + "2001", + "-r", + "3443", + "-h", + "4545", + "-u", + "wss://tee-internal.litentry.io", + "-p", + "443", + "--parentchain-start-block", + "27" + ], + "subcommand_flags": [ + ] + } + ] +} diff --git a/tee-worker/local-setup/launch.py b/tee-worker/local-setup/launch.py index 3c15bd1e46..346c727469 100755 --- a/tee-worker/local-setup/launch.py +++ b/tee-worker/local-setup/launch.py @@ -12,6 +12,7 @@ import signal from subprocess import Popen, PIPE, STDOUT, run import os +import shutil import sys from time import sleep from typing import Union, IO @@ -156,10 +157,16 @@ def offset_port(offset): def setup_environment(offset, config, parachain_dir): + if not os.path.isfile("./local-setup/.env"): + shutil.copy("./local-setup/.env.dev", "./local-setup/.env") + load_dotenv("./local-setup/.env.dev") offset_port(offset) check_all_ports_and_reallocate() - generate_config_local_json(parachain_dir) + + if parachain_dir != "": + generate_config_local_json(parachain_dir) + generate_env_local() # TODO: only works for single worker for now @@ -219,6 +226,7 @@ def main(processes, config_path, parachain_type, log_config_path, offset, parach setup_environment(offset, config, parachain_dir) run(["../scripts/launch-local-binary.sh", "rococo"], check=True) elif parachain_type == "remote": + setup_environment(offset, config, "") print("Litentry parachain should be started remotely") else: sys.exit("Unsupported parachain_type") diff --git a/tee-worker/scripts/litentry/release/config.json.eg b/tee-worker/scripts/litentry/release/config.json.eg deleted file mode 100644 index acfdbc872a..0000000000 --- a/tee-worker/scripts/litentry/release/config.json.eg +++ /dev/null @@ -1,11 +0,0 @@ -{ - "twitter_official_url": "https://api.twitter.com", - "twitter_litentry_url": "", - "twitter_auth_token_v2": "abcdefghijklmnopqrstuvwxyz", - "discord_official_url": "https://discordapp.com", - "discord_litentry_url": "", - "discord_auth_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", - "achainable_url": "https://graph.tdf-labs.io/", - "achainable_auth_key": "88888888-4444-4444-4444-1234567890ab", - "credential_endpoint": "" -} \ No newline at end of file diff --git a/tee-worker/scripts/litentry/release/deploy.sh b/tee-worker/scripts/litentry/release/deploy.sh deleted file mode 100755 index 72c7060dfa..0000000000 --- a/tee-worker/scripts/litentry/release/deploy.sh +++ /dev/null @@ -1,555 +0,0 @@ -#!/bin/bash - -set -eo pipefail - -# This script is used to perform actions on the target host, including: -# - generate: generate the systemd service files from the template -# - restart: restart the parachain, or the worker, or both -# - upgrade-worker: uprade the worker0 to the rev in local repo -# -# TODO: -# the combinations of flags are not yet well verified/organised, especially the following: -# --only-worker -# --build -# --discard - -# ------------------------------ -# path setting -# ------------------------------ - -ROOTDIR=$(git rev-parse --show-toplevel) -BASEDIR=/opt/litentry -PARACHAIN_BASEDIR="$BASEDIR/parachain" -WORKER_BASEDIR="$BASEDIR/worker" -BACKUP_BASEDIR="$BASEDIR/backup" -LOG_BACKUP_BASEDIR="$BACKUP_BASEDIR/log" -WORKER_BACKUP_BASEDIR="$BACKUP_BASEDIR/worker" -RELAYCHAIN_ALICE_BASEDIR="$PARACHAIN_BASEDIR/relay-alice" -RELAYCHAIN_BOB_BASEDIR="$PARACHAIN_BASEDIR/relay-bob" -PARACHAIN_ALICE_BASEDIR="$PARACHAIN_BASEDIR/para-alice" - -# ------------------------------ -# default arg setting -# ------------------------------ - -BUILD=false -DISCARD=false -WORKER_CONFIG= -CHAIN=rococo -ONLY_WORKER=false -PARACHAIN_HOST=localhost -PARACHAIN_PORT=9944 -DOCKER_IMAGE=litentry/litentry-parachain:tee-prod -COPY_FROM_DOCKER=false -PRODUCTION=false -ACTION= - -# ------------------------------ -# Some global setting -# ------------------------------ - -WORKER_COUNT= -PARACHAIN_ID= -OLD_MRENCLAVE= -NEW_MRENCLAVE= -OLD_SHARD= -LATEST_FINALIZED_BLOCK= - -SGX_SDK=/opt/intel/sgxsdk -SGX_ENCLAVE_SIGNER=$SGX_SDK/bin/x64/sgx_sign - -# ------------------------------ -# main() -# ------------------------------ - -function main { - # 0/ check if $USER has sudo - if sudo -l -U $USER 2>/dev/null | grep -q 'may run the following'; then - source "$SGX_SDK/environment" - else - echo "$USER doesn't have sudo permission" - exit 1 - fi - - # 1/ create folders if missing - sudo mkdir -p "$BASEDIR" - sudo chown $USER:$GROUPS "$BASEDIR" - for d in "$LOG_BACKUP_BASEDIR" "$WORKER_BACKUP_BASEDIR" "$RELAYCHAIN_ALICE_BASEDIR" "$RELAYCHAIN_BOB_BASEDIR" \ - "$PARACHAIN_ALICE_BASEDIR" "$WORKER_BASEDIR"; do - mkdir -p "$d" - done - - # 2/ parse command lines - echo "Parsing command line ..." - while [ $# -gt 0 ]; do - case "$1" in - -h|--help) - display_help - exit 0 - ;; - -b|--build) - BUILD=true - shift - ;; - -d|--discard) - DISCARD=true - shift - ;; - -c|--config) - WORKER_CONFIG="$(realpath -s $2)" - shift 2 - ;; - -a|--only-worker) - ONLY_WORKER=true - shift - ;; - -x|--chain) - CHAIN="$2" - shift 2 - ;; - -p|--parachain-port) - PARACHAIN_PORT="$2" - shift 2 - ;; - -z|--parachain-host) - PARACHAIN_HOST="$2" - shift 2 - ;; - -v|--copy-from-docker) - COPY_FROM_DOCKER=true - DOCKER_IMAGE="$2" - shift 2 - ;; - --prod) - PRODUCTION=true - shift - ;; - generate|restart|upgrade-worker) - ACTION="$1" - shift - ;; - *) - echo "Error: unknown option or subcommand $1" - display_help - exit 1 - ;; - esac - done - - # 3/ sanity checks - if [ ! -f "$WORKER_CONFIG" ]; then - echo "Worker config not found: $WORKER_CONFIG" - exit 1 - fi - - WORKER_COUNT=$(cat "$WORKER_CONFIG" | jq '.workers | length') - echo "Worker count: $WORKER_COUNT" - - # TODO: check flags conflict, e.g. - # - having `--discard` together with `upgrade-worker` doesn't make sense - # - `upgrade-worker` should ignore the `--only-worker` flag - - # 4/ main business logic - case "$ACTION" in - generate) - backup_services - generate_services - exit - ;; - restart) - backup_logs - backup_workers - stop_services - prune - build - setup_working_dir - if [ "$ONLY_WORKER" = true ]; then - remove_clean_reset - fi - restart_services - exit - ;; - upgrade-worker) - # build the new worker, the code must be under $ROOTDIR/tee-worker already - build_worker - # update the schedule - set_scheduled_enclave - - # wait until sidechain stalls - wait_for_sidechain - backup_workers - stop_worker_services - get_old_mrenclave - # TODO: actually we only need the copy-up - setup_working_dir - migrate_shard - remove_clean_reset - restart_services - exit - ;; - *) - echo "Unknown action: $ACTION" - exit 1 ;; - esac -} - -# ------------------------------ -# helper functions -# ------------------------------ - -function print_divider { - echo "------------------------------------------------------------" -} - -function display_help { - echo "usage: ./deploy.sh [options]" - echo "" - echo "subcommands:" - echo " generate Generate the parachain and worker systemd files" - echo " restart Restart the services" - echo " upgrade-worker Upgrade the worker" - echo "" - echo "options:" - echo " -h, --help Display this help message and exit" - echo " -b, --build Build the parachain and worker binaries (default: false)" - echo " -d, --discard Clean the existing state for parachain and worker (default: false)" - echo " -c, --config Config file for the worker" - echo " -a, --only-worker Start only the worker (default: false)" - echo " -x, --chain Chain type for launching the parachain network (default: rococo)" - echo " -h, --parachain-host Parachain ws URL (default: localhost)" - echo " -p, --parachain-port Parachain ws port (default: 9944)" - echo " -v, --copy-from-docker Copy the parachain binary from a docker image (default: litentry/litentry-parachain:tee-prod)" - echo " --prod Use a prod configuration to build and run the worker (default: false)" - echo "" - echo "examples:" - echo " ./deploy.sh generate --config tmp.json" - echo " ./deploy.sh restart --config tmp.json --discard --build" - echo " ./deploy.sh restart --config tmp.json --only-worker" - echo " ./deploy.sh upgrade-worker --config tmp.json --only-worker" - echo "" - echo "notes:" - echo " - This script requires an OS that supports systemd." - echo " - It is mandatory to provide a JSON config file for the worker." - echo " - jq is required to be installed on the system " - echo "" - echo "For more information or assistance, please contact Litentry parachain team." -} - -# TODO: in fact, this function only backs up the parachain logs -# maybe we want to remove it as it's not so critical anyway -function backup_logs { - echo "Backing up logs ..." - now=$(date +"%Y%m%d-%H%M%S") - outdir="$LOG_BACKUP_BASEDIR/log-$now" - mkdir -p "$outdir" - cp "$PARACHAIN_BASEDIR"/*.log "$outdir" || true - echo "Logs backed up into $outdir" -} - -function backup_workers { - echo "Backing up workers ..." - now=$(date +"%Y%m%d-%H%M%S") - cd "$WORKER_BASEDIR" || exit - for i in $(ls -d * 2>/dev/null); do - outdir="$WORKER_BACKUP_BASEDIR/$i-$now" - cp -rf "$i" "$outdir" - echo "Worker backed up into $outdir" - done -} - -function backup_services { - echo "Backing up services ..." - now=$(date +"%Y%m%d-%H%M%S") - cd /etc/systemd/system || exit - outdir="$WORKER_BACKUP_BASEDIR/service-$now" - mkdir -p "$outdir" - for f in para-alice.service relay-alice.service relay-bob.service $(ls worker*.service 2>/dev/null); do - cp "$f" "$outdir" || true - done -} - -function prune { - if [ "$DISCARD" = true ]; then - echo "Pruning the existing state ..." - rm -rf "$PARACHAIN_BASEDIR"/* - rm -rf "$WORKER_BASEDIR"/* - fi -} - -function generate_services { - echo "Generating systemd service files ..." - cd "$ROOTDIR/tee-worker/scripts/litentry/release" - cp template/* . - sed -i "s/CHAIN/$CHAIN/g" *.service - sed -i "s/USER/$USER/g" *.service - for ((i = 0; i < WORKER_COUNT; i++)); do - cp worker.service worker$i.service - sed -i "s/NUMBER/$i/g" worker$i.service - # populate args - flags=$(cat "$WORKER_CONFIG" | jq -r ".workers[$i].flags[]") - subcommand_flags=$(cat "$WORKER_CONFIG" | jq -r ".workers[$i].subcommand_flags[]") - args= - for flag in $flags; do - args+=" $flag" - done - args+=" run" - for subcommand_flag in $subcommand_flags; do - args+=" $subcommand_flag" - done - sed -i "s;ARGS;$args;" worker$i.service - done - rm worker.service - sudo cp *.service -f /etc/systemd/system/ - rm *.service - sudo systemctl daemon-reload - echo "Done, please check files under /etc/systemd/system/" - echo "Restart the services to take effect" -} - -function build_worker { - echo "Building worker ..." - cd $ROOTDIR/tee-worker/ || exit - if [ "$PRODUCTION" = true ]; then - # we will get an error if SGX_COMMERCIAL_KEY is not set for prod - SGX_PRODUCTION=1 make - else - # use SW mode for dev - SGX_MODE=SW make - fi -} - -# TODO: take github rev into consideration -function build { - if [ "$BUILD" = true ]; then - echo "Building the parachain and worker binaries ..." - - # download polkadot - echo "Downloading polkadot binary ..." - url="https://github.com/paritytech/polkadot/releases/download/v0.9.42/polkadot" - polkadot_bin="$PARACHAIN_BASEDIR/polkadot" - wget -O "$polkadot_bin" -q "$url" - chmod a+x "$polkadot_bin" - if [ ! -s "$polkadot_bin" ]; then - echo "$polkadot_bin is 0 bytes, download URL: $url" && exit 1 - fi - if ! "$polkadot_bin" --version &> /dev/null; then - echo "Cannot execute $polkadot_bin, wrong executable?" && exit 1 - fi - - # pull or build parachain - if [ "$COPY_FROM_DOCKER" = true ]; then - echo "Pulling binary from $DOCKER_IMAGE ..." - docker pull "$DOCKER_IMAGE" - docker cp "$(docker create --rm $DOCKER_IMAGE):/usr/local/bin/litentry-collator" "$PARACHAIN_BASEDIR" - else - echo "Building parachain binary ..." - cd "$ROOTDIR" || exit - if [ "$PRODUCTION" = true ]; then - cargo build --locked --profile production - else - pwd - make build-node - fi - cp "$ROOTDIR/target/release/litentry-collator" "$PARACHAIN_BASEDIR" - fi - chmod a+x "$PARACHAIN_BASEDIR/litentry-collator" - fi -} - -function restart_services { - sudo systemctl daemon-reload - if [ "$ONLY_WORKER" = false ]; then - echo "Restarting parachain services ..." - - cd "$PARACHAIN_BASEDIR" || exit - ./polkadot build-spec --chain rococo-local --disable-default-bootnode --raw > rococo-local-chain-spec.json - ./litentry-collator export-genesis-state --chain $CHAIN-dev > genesis-state - ./litentry-collator export-genesis-wasm --chain $CHAIN-dev > genesis-wasm - - sudo systemctl restart relay-alice.service - sleep 5 - sudo systemctl restart relay-bob.service - sleep 5 - sudo systemctl restart para-alice.service - sleep 5 - register_parachain - fi - - echo "Restarting worker services ..." - for ((i = 0; i < WORKER_COUNT; i++)); do - sudo systemctl restart "worker$i.service" - sleep 5 - done - echo "Done" -} - -function stop_worker_services { - echo "Stopping worker services ..." - for ((i = 0; i < WORKER_COUNT; i++)); do - sudo systemctl stop "worker$i.service" - sleep 5 - done -} - -function stop_parachain_services { - echo "Stopping parachain services ..." - sudo systemctl stop para-alice.service relay-alice.service relay-bob.service -} - -function stop_services { - stop_worker_services - - # TODO: it means we can't stop parachain service alone - # this needs to be done directly via `systemctl` - if [ "$ONLY_WORKER" = false ]; then - stop_parachain_services - fi -} - -function register_parachain { - echo "Register parathread now ..." - cd "$ROOTDIR" || exit - export PARACHAIN_ID=$(grep DEFAULT_PARA_ID node/src/chain_specs/$CHAIN.rs | grep u32 | sed 's/.* = //;s/\;//') - cd "$ROOTDIR/ts-tests" || exit - if [[ -z "$NODE_ENV" ]]; then - echo "NODE_ENV=ci" > .env - else - echo "NODE_ENV=$NODE_ENV" > .env - fi - # The genesis state path file needs to be updated as it is hardcoded to be /tmp/parachain_dev - jq --arg genesis_state "$PARACHAIN_BASEDIR/genesis-state" --arg genesis_wasm "$PARACHAIN_BASEDIR/genesis-wasm" '.genesis_state_path = $genesis_state | .genesis_wasm_path = $genesis_wasm' config.ci.json > config.ci.json.1 - mv config.ci.json.1 config.ci.json - pnpm install - pnpm run register-parathread 2>&1 | tee "$PARACHAIN_BASEDIR/register-parathread.log" - print_divider - - echo "Upgrade parathread to parachain now ..." - # Wait for 90s to allow onboarding finish, after that we do the upgrade - sleep 90 - pnpm run upgrade-parathread 2>&1 | tee "$PARACHAIN_BASEDIR/upgrade-parathread.log" - print_divider - - echo "done. please check $PARACHAIN_BASEDIR for generated files if need" - print_divider - git restore config.ci.json -} - -function setup_working_dir { - echo "Setting up working dir ..." - cd "$ROOTDIR/tee-worker/bin" || exit - - if [ "$PRODUCTION" = false ]; then - for f in 'key.txt' 'spid.txt'; do - [ -f "$f" ] || touch "$f" - done - fi - - for ((i = 0; i < WORKER_COUNT; i++)); do - worker_dir="$WORKER_BASEDIR/w$i" - mkdir -p "$worker_dir" - for f in 'key.txt' 'spid.txt' 'enclave.signed.so' 'litentry-worker'; do - [ -f "$f" ] && cp -f "$f" "$worker_dir" - done - - cd "$worker_dir" - [ -f light_client_db.bin/db.bin.backup ] && cp -f light_client_db.bin/db.bin.backup light_client_db.bin/db.bin - - enclave_account=$(./litentry-worker signing-key | grep -oP '^Enclave account: \K.*$$') - - if [ "$PRODUCTION" = true ]; then - echo "Transferring balance to the enclave account $enclave_account ..." - cd $ROOTDIR/scripts/ts-utils/ || exit - pnpm install - pnpm exec ts-node transfer.ts $enclave_account - fi - done -} - -function get_old_mrenclave { - cd "$WORKER_BASEDIR/w0" || exit - OLD_SHARD=$(./litentry-worker mrenclave) - $SGX_ENCLAVE_SIGNER dump -enclave ./enclave.signed.so -dumpfile df.out - OLD_MRENCLAVE=$($ROOTDIR/tee-worker/extract_identity < df.out | awk '{print $2}') - rm df.out - echo "old shard: $OLD_SHARD" - echo "old mrenclave: $OLD_MRENCLAVE" -} - -function set_scheduled_enclave { - echo "Setting scheduled enclave ..." - cd $ROOTDIR/tee-worker || exit - NEW_MRENCLAVE=$(make mrenclave 2>&1 | grep MRENCLAVE | awk '{print $2}') - echo "new mrenclave: $NEW_MRENCLAVE" - - latest_sidechain_block - - echo "Setting up the new worker on chain ..." - cd $ROOTDIR/ts-tests/ || exit - pnpm install - pnpm run setup-enclave $NEW_MRENCLAVE $SCHEDULED_UPDATE_BLOCK -} - -function wait_for_sidechain { - echo "Waiting for sidechain to reach block $SCHEDULED_UPDATE_BLOCK ..." - found=false - for _ in $(seq 1 30); do - sleep 20 - block_number=$(grep -F 'Enclave produced sidechain blocks' $WORKER_BASEDIR/w0/worker.log | tail -n 1 | sed 's/.*\[//;s/]//') - echo "current sidechain block: $block_number" - if [ $((block_number+1)) -eq $SCHEDULED_UPDATE_BLOCK ]; then - echo "we should stall soon ..." - fi - if tail -n 50 $WORKER_BASEDIR/w0/worker.log | grep -q "Skipping sidechain block $SCHEDULED_UPDATE_BLOCK due to mismatch MRENCLAVE"; then - echo "we reach $SCHEDULED_UPDATE_BLOCK now" - found=true - break - fi - done - if [ $found = false ]; then - echo "not reached, timeout" - exit 1 - fi -} - -function migrate_shard { - echo "Migrating shards for workers ..." - for ((i = 0; i < WORKER_COUNT; i++)); do - cd "$WORKER_BASEDIR/w$i" || exit - echo "old MRENCLAVE: $OLD_MRENCLAVE" - echo "new MRENCLAVE: $NEW_MRENCLAVE" - ./litentry-worker migrate-shard --old-shard $OLD_MRENCLAVE --new-shard $NEW_MRENCLAVE - - cd shards || exit - rm -rf $OLD_SHARD - done - echo "Done" -} - -function remove_clean_reset { - echo "Removing --clean-reset flag for workers ..." - for ((i = 0; i < WORKER_COUNT; i++)); do - sudo sed -i 's/--clean-reset//' /etc/systemd/system/worker$i.service - done - echo "Done" -} - -# TODO: here we only read worker0 logs here -function latest_sidechain_block { - block_number=$(grep -F 'Enclave produced sidechain blocks' $WORKER_BASEDIR/w0/worker.log | tail -n 1 | sed 's/.*\[//;s/]//') - SCHEDULED_UPDATE_BLOCK=$((block_number + 30)) - echo "Current sidechain block: $block_number, scheduled update block: $SCHEDULED_UPDATE_BLOCK" -} - -# TODO: unused -function _latest_parentchain_block { - # JSON-RPC request payload - request='{"jsonrpc":"2.0","id":1,"method":"chain_getHeader","params":[]}' - - # Make the JSON-RPC request and retrieve the latest finalized block - response=$(curl -s -H "Content-Type: application/json" -d "$request" http://$PARACHAIN_HOST:$PARACHAIN_PORT) - hex_number=$(echo "$response" | grep -oP '(?<="number":")[^"]+') - LATEST_FINALIZED_BLOCK=$(printf "%d" "$hex_number") - echo "Current parachain block: $LATEST_FINALIZED_BLOCK" -} - -main "$@" diff --git a/tee-worker/scripts/litentry/release/prepare.sh b/tee-worker/scripts/litentry/release/prepare.sh deleted file mode 100755 index e9817e8d71..0000000000 --- a/tee-worker/scripts/litentry/release/prepare.sh +++ /dev/null @@ -1,50 +0,0 @@ -#!/bin/bash -set -euo pipefail - - -# This WORKER_DIR is the directory where worker will start from. -WORKER_DIR=/opt/worker/ - -# CONFIG_DIR provides all the necessary private secret files. -# They should only exist on the running machine. -CONFIG_DIR=/opt/configs/ -CONFIG=$CONFIG_DIR/config.json -ACCOUNT=$CONFIG_DIR/private_account.json -INTEL_KEY=$CONFIG_DIR/key_production.txt -INTEL_SPID=$CONFIG_DIR/spid_production.txt - -############################################################################## -# Don't edit anything from here -if [[ ! -e "$WORKER_DIR" ]]; then - mkdir -p $WORKER_DIR -fi - -for Item in $CONFIG $ACCOUNT $INTEL_KEY $INTEL_SPID; do - if [[ ! -e "$Item" ]]; then - echo "Error: $Item is not a valid path." - exit 1 - fi -done - -# Generate keys and copy around. -SRC_DIR=$(dirname "$0") -cd $SRC_DIR - -./litentry-worker signing-key | grep -oP '^Enclave account: \K.*$$' > enclave_account.txt -echo "Enclave account is prepared inside enclave_account.txt" - -./litentry-worker shielding-key - -for Item in 'enclave.signed.so' 'litentry-worker' 'aes_key_sealed.bin' 'ed25519_key_sealed.bin' 'enclave-shielding-pubkey.json' 'enclave-signing-pubkey.bin' 'rsa3072_key_sealed.bin' 'sidechain_db'; do - cp -r "${Item}" "${WORKER_DIR}" -done - -cp $CONFIG "${WORKER_DIR}/config.json" -cp $INTEL_KEY "${WORKER_DIR}/key_production.txt" -cp $INTEL_SPID "${WORKER_DIR}/spid_production.txt" - -# Comment out for the moment. Need to adapt together with PR-1587 ts-utils. -cp $ACCOUNT "${WORKER_DIR}/ts-utils/private_account.json" -cp "enclave_account.txt" "${WORKER_DIR}/ts-utils/enclave_account.txt" -cp "mrenclave.txt" "${WORKER_DIR}/ts-utils/mrenclave.txt" - diff --git a/tee-worker/scripts/litentry/release/template/worker.service b/tee-worker/scripts/litentry/release/template/worker.service index e218d60278..d2a6edb24d 100644 --- a/tee-worker/scripts/litentry/release/template/worker.service +++ b/tee-worker/scripts/litentry/release/template/worker.service @@ -6,9 +6,9 @@ Type=simple User=USER Environment='RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug' WorkingDirectory=/opt/litentry/worker/wNUMBER -ExecStart=/bin/bash -c 'cd /opt/litentry/worker/wNUMBER && source /opt/intel/sgxsdk/environment && ./litentry-worker ARGS' +ExecStart=/bin/bash -c 'cd /opt/litentry/worker/wNUMBER && source /opt/intel/sgxsdk/environment && source /opt/worker_configs/worker_env && ./litentry-worker ARGS' StandardOutput=append:/opt/litentry/worker/wNUMBER/worker.log StandardError=inherit [Install] -WantedBy=multi-user.target \ No newline at end of file +WantedBy=multi-user.target From 4d7fdcbafa78776cac309db128fe4a2a83927c69 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Tue, 6 Feb 2024 09:56:11 +0100 Subject: [PATCH 15/64] P 419 deploy automation (#2466) * Small update to GHA and versioning * use DOCKER_TAG * set env * fix bugs in deploy sh * update the package version of sidechain-api * refactor deploy.sh and based on deploy.sh, create new prod_deploy.sh * refactor deploy.sh and based on deploy.sh, create new prod_deploy.sh * rm not used files * check and load env file for worker * minor improvement during debugging * move production deploy to private repo * move development deploy to private repo * This should only be a warn message. * fix: load local file * rm accidentally committed file --------- Co-authored-by: Kailai Wang Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> Co-authored-by: Zhouhui Tian <125243011+zhouhuitian@users.noreply.github.com> --- tee-worker/local-setup/internal-one.json | 27 ------------------------ tee-worker/local-setup/launch.py | 2 +- 2 files changed, 1 insertion(+), 28 deletions(-) delete mode 100644 tee-worker/local-setup/internal-one.json diff --git a/tee-worker/local-setup/internal-one.json b/tee-worker/local-setup/internal-one.json deleted file mode 100644 index 5882223148..0000000000 --- a/tee-worker/local-setup/internal-one.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "workers": [ - { - "source": "bin", - "flags": [ - "--clean-reset", - "--ws-external", - "-P", - "2000", - "-w", - "2001", - "-r", - "3443", - "-h", - "4545", - "-u", - "wss://tee-internal.litentry.io", - "-p", - "443", - "--parentchain-start-block", - "27" - ], - "subcommand_flags": [ - ] - } - ] -} diff --git a/tee-worker/local-setup/launch.py b/tee-worker/local-setup/launch.py index 346c727469..d89694e33e 100755 --- a/tee-worker/local-setup/launch.py +++ b/tee-worker/local-setup/launch.py @@ -160,7 +160,7 @@ def setup_environment(offset, config, parachain_dir): if not os.path.isfile("./local-setup/.env"): shutil.copy("./local-setup/.env.dev", "./local-setup/.env") - load_dotenv("./local-setup/.env.dev") + load_dotenv("./local-setup/.env") offset_port(offset) check_all_ports_and_reallocate() From 01d88ed4a338f2002d8c356d93ca2cec63d9b6f1 Mon Sep 17 00:00:00 2001 From: Verin1005 <104152026+Verin1005@users.noreply.github.com> Date: Tue, 6 Feb 2024 20:55:00 +0800 Subject: [PATCH 16/64] Data providers tests for Achainable, Nodereal, Discord, Litentry, and Twitter (#2434) * revert data-provider-tests * fix CorePrimitivesIdentity imports * Restore the data-provider test. * remove vipp3 test file * debug vcPayloadJson * refactor credential definitions * format * add event listening * complete vip3 tests * fix vip3MembershipCardSliver * add to ci.yml * fix ci * extract and reuse function * test cli * format * remove client config * format * random substrate wallet * test client * fix cli issue * fix cli path * env example * debug local * format * fix cli path * test mock address * withdraw account test. * modefy into json file * delete ts file * format * add achainable json * improve logic * fix comments * complete achainable test * step description * test token-holding-amount-amp * add litentry spaceId discord * add twitter * f ix ndoereal tests * add matic * Json data Integration Testing * assert desc * add yml * add GHA for data-provider-tests * add shell * chmod shell * add agrument * fix shell * rename * add dc account * fix bab * fix yml * format * remove Mirror * fix typo * order of json * remove A20 from ci.yml * rebase A20 test --------- Co-authored-by: Kasper Ziemianek --- .github/workflows/ci.yml | 5 +- .../verify-correctness-of-vc-content-.yml | 73 ++++ ...rs-test.yml => lit-data-provider-test.yml} | 6 +- .../common/credential-json/achainable.json | 251 ++++++++++++++ .../common/credential-json/discord.json | 110 +++++++ .../common/credential-json/index.ts | 24 +- .../common/credential-json/litentry.json | 16 + .../common/credential-json/nodereal.json | 311 ++++++++++++++++++ .../common/credential-json/oneblock.json | 44 +++ .../common/credential-json/twitter.json | 16 + .../{vip3-credential-test.json => vip3.json} | 0 ...roviders.test.ts => data-provider.test.ts} | 44 ++- .../ts-tests/integration-tests/package.json | 4 +- 13 files changed, 865 insertions(+), 39 deletions(-) create mode 100644 .github/workflows/verify-correctness-of-vc-content-.yml rename tee-worker/docker/{lit-data-providers-test.yml => lit-data-provider-test.yml} (84%) create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/achainable.json create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/discord.json create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/litentry.json create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/nodereal.json create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/oneblock.json create mode 100644 tee-worker/ts-tests/integration-tests/common/credential-json/twitter.json rename tee-worker/ts-tests/integration-tests/common/credential-json/{vip3-credential-test.json => vip3.json} (100%) rename tee-worker/ts-tests/integration-tests/{data-providers.test.ts => data-provider.test.ts} (81%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ed0f342620..395331915f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -184,7 +184,7 @@ jobs: - name: bitacross-worker enclave-runtime fmt check working-directory: ./bitacross-worker/enclave-runtime run: | - cargo fmt --all -- --check + cargo fmt --all -- --check - name: Enable corepack and pnpm run: corepack enable && corepack enable pnpm @@ -311,7 +311,7 @@ jobs: - set-condition - sequentialise if: needs.set-condition.outputs.rebuild_bitacross == 'true' -# todo: we might want to change this image in the future + # todo: we might want to change this image in the future container: "litentry/litentry-tee-dev:latest" steps: - uses: actions/checkout@v4 @@ -705,7 +705,6 @@ jobs: - test_name: lit-parentchain-nonce - test_name: lit-ii-batch-test - test_name: lit-test-stress-script - - test_name: lit-data-providers-test steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/verify-correctness-of-vc-content-.yml b/.github/workflows/verify-correctness-of-vc-content-.yml new file mode 100644 index 0000000000..201b5f32a4 --- /dev/null +++ b/.github/workflows/verify-correctness-of-vc-content-.yml @@ -0,0 +1,73 @@ +name: Verify the correctness of VC content + +on: + workflow_dispatch: + inputs: + docker-tag: + description: "client tag(e.g. p1.2.0-9701-w0.0.1-101)" + required: true + default: "latest" +jobs: + test-data-provider: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set ENV + run: | + # extracting parachain version and worker version from release tag + echo "PARACHAIN_TAG=$(echo ${{inputs.docker-tag}} | sed 's/p/v/;s/\(.*\)-w.*/\1/')" >> $GITHUB_ENV + echo "WORKER_TAG=$(echo ${{inputs.docker-tag}} | sed 's/.*\(w.*\)/\1/;s/w/v/')" >> $GITHUB_ENV + + - name: Pull litentry image optionally + run: | + docker pull parity/polkadot + docker pull litentry/litentry-worker:$WORKER_TAG + docker pull litentry/litentry-cli:$WORKER_TAG + docker pull litentry/litentry-parachain:$PARACHAIN_TAG + + - name: Re-tag docker image + run: | + docker tag litentry/litentry-worker:$WORKER_TAG litentry/litentry-worker:latest + docker tag litentry/litentry-cli:$WORKER_TAG litentry/litentry-cli:latest + docker tag litentry/litentry-parachain:$PARACHAIN_TAG litentry/litentry-parachain:latest + + - run: docker images --all + + - name: Enable corepack and pnpm + run: corepack enable && corepack enable pnpm + + - name: Generate parachain artefacts + run: | + ./tee-worker/scripts/litentry/generate_parachain_artefacts.sh + + - name: Build litentry parachain docker images + run: | + cd tee-worker/docker + docker compose -f litentry-parachain.build.yml build + + - name: Run data-provider-test + run: | + cd tee-worker/docker + docker compose -f docker-compose.yml -f lit-data-provider-test.yml up --no-build --exit-code-from lit-data-provider-test lit-data-provider-test + + - name: Stop docker containers + run: | + cd tee-worker/docker + docker compose -f docker-compose.yml -f lit-data-provider-test.yml stop + + - name: Collect Docker Logs + continue-on-error: true + if: always() + uses: jwalton/gh-docker-logs@v2 + with: + tail: all + dest: logs + + - name: Upload logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: logs-lit-data-provider-test + path: logs + if-no-files-found: ignore diff --git a/tee-worker/docker/lit-data-providers-test.yml b/tee-worker/docker/lit-data-provider-test.yml similarity index 84% rename from tee-worker/docker/lit-data-providers-test.yml rename to tee-worker/docker/lit-data-provider-test.yml index be0ee66ed4..2af04c8784 100644 --- a/tee-worker/docker/lit-data-providers-test.yml +++ b/tee-worker/docker/lit-data-provider-test.yml @@ -1,7 +1,7 @@ services: - lit-data-providers-test: + lit-data-provider-test: image: litentry/litentry-cli:latest - container_name: litentry-data-providers-test + container_name: litentry-data-provider-test volumes: - ../ts-tests:/ts-tests - ../client-api:/client-api @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-data-providers 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-data-provider 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/achainable.json b/tee-worker/ts-tests/integration-tests/common/credential-json/achainable.json new file mode 100644 index 0000000000..cf1a986f3b --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/achainable.json @@ -0,0 +1,251 @@ +[ + { + "id": "bab-holder", + "name": "Trustworthy Binance user", + "description": "You are holding a Binance Account Bound(BAB) token", + "assertion": { + "id": "Achainable", + "payload": { + "Basic": { + "name": "BAB token holder", + "chain": "Bsc" + } + } + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0xdB01C6a38E780a2644b3B26961b14313b019714a", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "uniswap-v2-v3-user", + "name": "Uniswap V2/V3 User", + "description": "You are a trader or liquidity provider of Uniswap V2 or V3\nUniswap V2 Factory Contract: 0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f\nUniswap V3 Factory Contract: 0x1f98431c8ad98523631ae4a59f267346ea31f984", + "assertion": { + "id": "Achainable", + "payload": { + "Basic": { + "name": "Uniswap V2/V3 user", + "chain": "Ethereum" + } + } + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0x7031a420603a83EFBE85503e5A4BF562121A85ab", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "dot-holder", + "name": "DOT Holding Time", + "description": "The length of time a user continues to hold DOT token (threshold DOT > 5)", + "assertion": { + "id": "A7", + "payload": "5" + }, + "dataProvider": "achainable", + "network": "polkadot", + "mockDid": "litentry:substrate:0xbf6e312fd006908b0152da113ec73b9ffc9983f9b848698180b9b43c5ad44681", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "eth-holder", + "name": "ETH Holding Time", + "description": "The length of time a user continues to hold ETH token (threshold ETH > 0.01)", + "assertion": { + "id": "A11", + "payload": "0.01" + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0x6887246668a3b87F54DeB3b94Ba47a6f63F32985", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": false + }, + { + "id": "wbtc-holder", + "name": "WBTC Holding Time", + "description": "The length of time a user continues to hold WBTC token (threshold WBTC > 0.001)", + "assertion": { + "id": "A10", + "payload": 0.001 + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0x5680b3FcBB64FB161adbD347BC92e8DDEDA97008", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "lit-holder", + "name": "LIT Holding Time", + "description": "The length of time a user continues to hold LIT token (threshold LIT > 10)", + "assertion": { + "id": "A4", + "payload": "10" + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:substrate:0x6c7fdb8b8eaad1af9faaf918493606e1a3e8c20f9d852773ab5ebfbb93bd1948", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "ethereum-account-class-of-year", + "name": "Ethereum Account Class of Year", + "description": "The class of year that your Ethereum account was created (must have on-chain records)", + "assertion": { + "id": "achainable", + "payload": { + "ClassOfYear": { + "name": "Account created between {dates}", + "chain": "Ethereum" + } + } + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0x3D66aF7cBeb2d9c6b4497a2269F7e3fcd9996524", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "evm-transaction-count", + "name": "ETH Transaction Count", + "description": "Your total transaction amount on these EVM networks: Ethereum", + "assertion": { + "id": "A8", + "payload": ["Ethereum"] + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0xb144EeFd29B2E448419fd8dD6Cd6c7EB258D4715", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": false + }, + { + "id": "litentry-transaction-count", + "name": "LIT Transaction Count", + "description": "Your total transaction amount on these Substrate networks: Litentry, Litmus, Ethereum.", + "assertion": { + "id": "A8", + "payload": ["Litentry", "Litmus", "Ethereum"] + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:substrate:0xcacb83fc3d36caa4d03a80c0669aa19b38ffd1a9bb54d78b719fac1942816b40", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "polkadot-governance-participation", + "name": "Polkadot Governance Participation Proof", + "description": "You have ever participated in any Polkadot on-chain governance events. This credential is counting: Technical Committee Proposals, Democracy Proposals, Council Proposals, Proposal Seconds, Proposal Votes, Democracy Votes, Council Votes, Treasury Spend Proposals.", + "assertion": { + "id": "A14", + "payload": [] + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:substrate:0x5FeEE1E799015A22EbD216d43484FCB3BA88de20", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "contract-creator", + "name": "Ethereum Contract Creator", + "description": "You are a deployer of a smart contract on these networks: Ethereum", + "assertion": { + "id": "Achainable", + "payload": { + "Amount": { + "name": "Created over {amount} contracts", + "chain": "Ethereum", + "amount": "0" + } + } + }, + "dataProvider": "achainable", + "network": "ethereum", + "mockDid": "litentry:evm:0x11fBffc1F3dF23E7219e2B3036fe2A12C10cD3AD", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holder-dot", + "name": "DOT Holder", + "description": "The number of DOT tokens you hold > 0", + "assertion": { + "id": "Achainable", + "payload": { + "Amount": { + "name": "Balance over {amount}", + "chain": "Polkadot", + "amount": "0" + } + } + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:substrate:0x0d584a4cbbfd9a4878d816512894e65918e54fae13df39a6f520fc90caea2fb0", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "token-holder-eth", + "name": "ETH Holder", + "description": "The number of ETH tokens you hold > 0", + "assertion": { + "id": "Achainable", + "payload": { + "Amount": { + "name": "Balance over {amount}", + "chain": "Ethereum", + "amount": "0" + } + } + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:evm:0x66485bB62896Bd7bE54dE8E2050cc8746a50E0b2", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holder-lit", + "name": "LIT Holder", + "description": "The number of LIT tokens you hold > 0", + "assertion": { + "id": "Achainable", + "payload": { + "Amount": { + "name": "Balance over {amount}", + "chain": "Litentry", + "amount": "0" + } + } + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:substrate:0xc0103c4b56ce752d05b16a88260e3a9e2c44306602a74c5edd1cd4ff56356f7c", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-btcs", + "name": "BTCS Holding Time", + "description": "The number of BTCS tokens you hold > 0", + "assertion": { + "id": "Brc20AmountHolder", + "payload": [] + }, + "dataProvider": "achainable", + "network": "litentry", + "mockDid": "litentry:bitcoin:0x030b0998ea7d5052e3016bd8c83f920f452f6102c06e91a2880556b91db68bce90", + "mockWeb3Network": "BitcoinP2tr", + "expectedCredentialValue": null + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/discord.json b/tee-worker/ts-tests/integration-tests/common/credential-json/discord.json new file mode 100644 index 0000000000..3f3f28c2f4 --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/discord.json @@ -0,0 +1,110 @@ +[ + { + "id": "id-hubber", + "name": "Active Discord ID-Hubber", + "description": "You have commented in Litentry Discord #🪂id-hubber channel. Channel link: https://discord.com/channels/807161594245152800/1093886939746291882", + "assertion": { + "id": "A3", + "payload": ["807161594245152800", "1093886939746291882", "1088092822592307240"] + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:niceguy2309", + "mockWeb3Network": "", + "expectedCredentialValue": true + }, + { + "id": "join-litentry-discord", + "name": "Litentry Discord Member", + "description": "The user is a member of Litentry Discord.\nServer link: 'https://discord.gg/phBSa3eMX9'.\nGuild ID: 807161594245152800", + "assertion": { + "id": "A2", + "payload": "807161594245152800" + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:almuyaad_jr", + "mockWeb3Network": "", + "expectedCredentialValue": true + }, + { + "id": "litentry-and-sora-quiz-attendee", + "name": "Litentry & SORA Quiz Attendee", + "description": "Congratulations on your participation in our first quiz in collaboration with our partner, SORA. You have embarked on an exciting educational journey, exploring the world of DeFi & Web3 Identity, we truly appreciate your curiosity and dedication.", + "assertion": { + "id": "GenericDiscordRole", + "payload": { + "soraquiz": "attendee" + } + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:light_bearer01", + "mockWeb3Network": "", + "expectedCredentialValue": false + }, + { + "id": "litentry-and-ordinals-user", + "name": "Litentry & Ordinals User", + "description": "Litentry Participants in the Bitcoin Ecosystem", + "assertion": { + "id": "GenericDiscordRole", + "payload": { + "soraquiz": "master" + } + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:cynthian09021", + "mockWeb3Network": "", + "expectedCredentialValue": false + }, + { + "id": "score-contest-legend", + "name": "Contest Legend", + "description": "You got the Top Award of community contest", + "assertion": { + "id": "GenericDiscordRole", + "payload": { + "soraquiz": "legend" + } + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:zawmyosatservice0737", + "mockWeb3Network": "", + "expectedCredentialValue": false + }, + { + "id": "score-contest-popularity", + "name": "Contest Popularity", + "description": "You got the Popularity of community contest", + "assertion": { + "id": "GenericDiscordRole", + "payload": { + "soraquiz": "popularity" + } + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:eudizjr", + "mockWeb3Network": "", + "expectedCredentialValue": false + }, + { + "id": "score-contest-participant", + "name": "Contest Participant", + "description": "You got the Participant of community contest", + "assertion": { + "id": "GenericDiscordRole", + "payload": { + "soraquiz": "participant" + } + }, + "dataProvider": "discord", + "network": "discord", + "mockDid": "litentry:discord:neo_travolta", + "mockWeb3Network": "", + "expectedCredentialValue": false + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/index.ts b/tee-worker/ts-tests/integration-tests/common/credential-json/index.ts index 19f9c55267..6d7af24221 100644 --- a/tee-worker/ts-tests/integration-tests/common/credential-json/index.ts +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/index.ts @@ -1,16 +1,24 @@ import { CorePrimitivesAssertion, CorePrimitivesNetworkWeb3Network } from 'parachain-api'; import type { Codec } from '@polkadot/types-codec/types'; import type { U8aLike } from '@polkadot/util/types'; -type DataProvider = { - id: string; - name: string; - url: string; -}; type AssertionGenericPayload = string | Array | Record; -import vip3Json from './vip3-credential-test.json' assert { type: 'json' }; -export const vip3CredentialJson = vip3Json as unknown as CredentialDefinition[]; +import vip3Json from './vip3.json' assert { type: 'json' }; +import achainableJson from './achainable.json' assert { type: 'json' }; +import noderealJson from './nodereal.json' assert { type: 'json' }; +import discordJson from './discord.json' assert { type: 'json' }; +import litentryJson from './litentry.json' assert { type: 'json' }; +import twitterJson from './twitter.json' assert { type: 'json' }; +import oneblockJson from './oneblock.json' assert { type: 'json' }; +export const vip3 = vip3Json as unknown as CredentialDefinition[]; +export const achainable = achainableJson as unknown as CredentialDefinition[]; +export const nodereal = noderealJson as unknown as CredentialDefinition[]; +export const discord = discordJson as unknown as CredentialDefinition[]; +export const litentry = litentryJson as unknown as CredentialDefinition[]; +export const twitter = twitterJson as unknown as CredentialDefinition[]; +export const oneblock = oneblockJson as unknown as CredentialDefinition[]; +export const credentialsJson = [...vip3, ...achainable, ...nodereal, ...litentry, ...twitter, ...oneblock, ...discord]; export interface CredentialDefinition { id: string; @@ -20,7 +28,7 @@ export interface CredentialDefinition { id: CorePrimitivesAssertion['type']; payload: AssertionGenericPayload; }; - dataProvider: DataProvider; + dataProvider: string; network: CorePrimitivesNetworkWeb3Network['type']; mockDid: string; mockWeb3Network: string; diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/litentry.json b/tee-worker/ts-tests/integration-tests/common/credential-json/litentry.json new file mode 100644 index 0000000000..770dba85c5 --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/litentry.json @@ -0,0 +1,16 @@ +[ + { + "id": "evm-version-early-bird", + "name": "IDHub EVM Version Early Bird", + "description": "The user is an early bird user of the IdentityHub EVM version and has generated at least 1 credential during 2023 Aug 14th ~ Aug 21st.", + "assertion": { + "id": "A20", + "payload": [] + }, + "dataProvider": "litentryIndexer", + "network": "litentry", + "mockDid": "litentry:substrate:0xa20d2c66bd88271ce78b2d8b7367c025e108944fda26a5a4e95b5efcc3f26b45", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": false + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/nodereal.json b/tee-worker/ts-tests/integration-tests/common/credential-json/nodereal.json new file mode 100644 index 0000000000..298a0a7a2c --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/nodereal.json @@ -0,0 +1,311 @@ +[ + { + "id": "bnb-domain-holding-amount", + "name": ".bnb Holding Amount", + "description": "You are holding a certain amount of bnb domain names", + + "assertion": { + "id": "BnbDomainHolding", + "payload": [] + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xf813361F1FADC5c51EFdd0ba5b93e2760a5537EC", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "bnb-999-club-member", + "name": "000-999.bnb Domain Holding Amount", + "description": "You are holding a certain amount of 000-999.bnb domain names", + "assertion": { + "id": "BnbDigitDomainClub", + "payload": "Bnb999ClubMember" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xA6E3d8B20a5DC12c986AF63E496B8D585117aBBd", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "bnb-10k-club-member", + "name": "0000-9999.bnb Holding Amount", + "description": "You are holding a certain amount of 0000-9999.bnb domain names", + "assertion": { + "id": "BnbDigitDomainClub", + "payload": "Bnb10KClubMember" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xD673b52E4c560f5d2BD41fd92e7566Ef445DC5AB", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-amp", + "name": "AMP Holding Amount", + "description": "The number of AMP tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Amp" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x5029bC78f84Fc7F1568FbA1A7808e753691E6675", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-comp", + "name": "COMP Holding Amount", + "description": "The number of COMP tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Comp" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xA5044e67f0c35b31fe82F2Ded6606b0b91545e98", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-crv", + "name": "CRV Holding Amount", + "description": "The number of CRV tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Crv" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xd14A1dB9B2Bfe0cE9cC175C24d6B01a16aB84f42", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-cvx", + "name": "CVX Holding Amount", + "description": "The number of CVX tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Cvx" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xAA1582084c4f588eF9BE86F5eA1a919F86A3eE57", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-dydx", + "name": "DYDX Holding Amount", + "description": "The number of DYDX tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Dydx" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xe7DAE0cEd7a64d50136D466945257b600e718ACa", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-grt", + "name": "GRT Holding Amount", + "description": "The number of GRT tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Grt" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x46f80018211D5cBBc988e853A8683501FCA4ee9b", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-gtc", + "name": "GTC Holding Amount", + "description": "The number of GTC tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Gtc" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x310E035d176ccB589511eD16af7aE7BAc4fc7f83", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-gusd", + "name": "GUSD Holding Amount", + "description": "The number of GUSD tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Gusd" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x94CFF34005A073911C3179abE89F1677f0D37d42", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-link", + "name": "LINK Holding Amount", + "description": "The number of LINK tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Link" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xB4e9275827B5f049196f5337F69533937475A3de", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-lit", + "name": "LIT Holding Amount", + "description": "The number of LIT tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Lit" + }, + "dataProvider": "nodereal", + "network": "litentry", + "mockDid": "litentry:evm:0xcFD97648df7fB75A545c69106d2049aa3D540334", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-people", + "name": "PEOPLE Holding Amount", + "description": "The number of PEOPLE tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "People" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x60E4F4bF50204dEeeD8bF4C6216b41BA2e5e453a", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-ton", + "name": "TON Holding Amount", + "description": "The number of TON tokens you hold > 0", + "assertion": { + "id": "EvmAmountHolding", + "payload": "Ton" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x8BaE6C9EA994d18e6b05cE33aE3e54Fa6F7FcE82", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-trx", + "name": "TRX Holding Amount", + "description": "The number of TRX tokens you hold > 0", + "assertion": { + "id": "EvmAmountHolding", + "payload": "Trx" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x2d23D597b21F88c55fC6f1E2a84f42b06b7915dF", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-tusd", + "name": "TUSD Holding Amount", + "description": "The number of TUSD tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Tusd" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xDebc74120F822C0F0e6Eb69dB5F347F574d2D446", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-usdd", + "name": "USDD Holding Amount", + "description": "The number of USDD tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Usdd" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x31743a08D895d01a49dB98b8F9c8469D92f63745", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-usdt", + "name": "USDT Holding Amount", + "description": "The number of USDT tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Usdt" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x67aB29354a70732CDC97f372Be81d657ce8822cd", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-holding-amount-matic", + "name": "MATIC Holding Amount", + "description": "The number of MATIC tokens you hold > 0", + "assertion": { + "id": "TokenHoldingAmount", + "payload": "Matic" + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0x3B7BB88dB769923dC2eE1e9e6A83c00A74c407D2", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + }, + { + "id": "token-staking-amount-lit", + "name": "LIT Staking Amount", + "description": "You are staking a certain amount of LIT", + "assertion": { + "id": "LitStaking", + "payload": [] + }, + "dataProvider": "nodereal", + "network": "litentry", + "mockDid": "litentry:substrate:0xfc7a9dd32be14db4695555aa9a2abd240a8c2160f84ccb403a985701dd13fe50", + "mockWeb3Network": "litentry,polkadot", + "expectedCredentialValue": true + }, + { + "id": "weirdo-ghost-gang-holder", + "name": "WeirdoGhostGang Holder", + "description": "You are WeirdoGhostGang NFT holder", + "assertion": { + "id": "WeirdoGhostGangHolder", + "payload": [] + }, + "dataProvider": "nodereal", + "network": "ethereum", + "mockDid": "litentry:evm:0xCaA18Cd73E2756c680859F0A97E2C4846D50f71B", + "mockWeb3Network": "bsc,ethereum", + "expectedCredentialValue": true + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/oneblock.json b/tee-worker/ts-tests/integration-tests/common/credential-json/oneblock.json new file mode 100644 index 0000000000..a3272ce8bc --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/oneblock.json @@ -0,0 +1,44 @@ +[ + { + "id": "oneblock-course-participation", + "name": "OneBlock+ Substrate Blockchain Development Course Participation", + "description": "You were a participant to the course co-created by OneBlock+ and Parity: \"Introduction to Substrate Blockchain Development, Phase 12\".", + "assertion": { + "id": "Oneblock", + "payload": "CourseParticipation" + }, + "dataProvider": "oneblock", + "network": "oneblock", + "mockDid": "litentry:substrate:0xf26f4f77ccb38173185242cd1199696e5df5201666aae383de86eeb9e8251b3b", + "mockWeb3Network": "polkadot,kusama", + "expectedCredentialValue": false + }, + { + "id": "oneblock-course-completion", + "name": "OneBlock+ Substrate Blockchain Development Course Completion", + "description": "You have completed the course co-created by OneBlock+ and Parity: \"Introduction to Substrate Blockchain Development, Phase 12\". \n\n OneBlock+: “We hope you will keep your enthusiasm and continue to explore on the road ahead.”", + "assertion": { + "id": "Oneblock", + "payload": "CourseCompletion" + }, + "dataProvider": "oneblock", + "network": "oneblock", + "mockDid": "litentry:substrate:0xaa639b158b45acd0f4315be3a074aee110979f34123828a0c8ef74c24925d96f", + "mockWeb3Network": "polkadot,kusama", + "expectedCredentialValue": true + }, + { + "id": "oneblock-course-outstanding", + "name": "OneBlock+ Substrate Blockchain Development Course Outstanding Student", + "description": "You were awarded the title \"Outstanding Student\" in the course \"Introduction to Substrate Blockchain Development, Phase 12” co-created by OneBlock+ and Parity.", + "assertion": { + "id": "Oneblock", + "payload": "CourseOutstanding" + }, + "dataProvider": "oneblock", + "network": "oneblock", + "mockDid": "litentry:substrate:0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d", + "mockWeb3Network": "polkadot,kusama", + "expectedCredentialValue": true + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/twitter.json b/tee-worker/ts-tests/integration-tests/common/credential-json/twitter.json new file mode 100644 index 0000000000..c10bb75859 --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/common/credential-json/twitter.json @@ -0,0 +1,16 @@ +[ + { + "id": "twitter-follower-amount", + "name": "Twitter Follower Amount", + "description": "You have a certain amount of Twitter followers.", + "assertion": { + "id": "A6", + "payload": "A6" + }, + "dataProvider": "twitter", + "network": "twitter", + "mockDid": "litentry:twitter:litentry", + "mockWeb3Network": "", + "expectedCredentialValue": true + } +] diff --git a/tee-worker/ts-tests/integration-tests/common/credential-json/vip3-credential-test.json b/tee-worker/ts-tests/integration-tests/common/credential-json/vip3.json similarity index 100% rename from tee-worker/ts-tests/integration-tests/common/credential-json/vip3-credential-test.json rename to tee-worker/ts-tests/integration-tests/common/credential-json/vip3.json diff --git a/tee-worker/ts-tests/integration-tests/data-providers.test.ts b/tee-worker/ts-tests/integration-tests/data-provider.test.ts similarity index 81% rename from tee-worker/ts-tests/integration-tests/data-providers.test.ts rename to tee-worker/ts-tests/integration-tests/data-provider.test.ts index 78903f6a7f..f1a5f6f834 100644 --- a/tee-worker/ts-tests/integration-tests/data-providers.test.ts +++ b/tee-worker/ts-tests/integration-tests/data-provider.test.ts @@ -17,7 +17,7 @@ import { $ as zx } from 'zx'; import { subscribeToEventsWithExtHash } from './common/transactions'; import { KeyringPair } from '@polkadot/keyring/types'; import { u8aToHex } from '@polkadot/util'; -import { vip3CredentialJson, CredentialDefinition } from './common/credential-json'; +import { CredentialDefinition, credentialsJson } from './common/credential-json'; describe('Test Vc (direct invocation)', function () { let context: IntegrationTestContext = undefined as any; let teeShieldingKey: KeyObject = undefined as any; @@ -27,7 +27,6 @@ describe('Test Vc (direct invocation)', function () { const reqExtHash = '0x0000000000000000000000000000000000000000000000000000000000000000'; const keyringPairs: KeyringPair[] = []; let argvId = ''; - const credentialsJson: CredentialDefinition[] = [...vip3CredentialJson]; this.timeout(6000000); before(async () => { @@ -38,8 +37,8 @@ describe('Test Vc (direct invocation)', function () { // usage example: // `pnpm run test-data-providers:local --id=vip3-membership-card-gold` for single test // `pnpm run test-data-providers:local` for all tests - const argv = process.argv.indexOf('--id'); - argvId = process.argv[argv + 1]; + const idIndex = process.argv.indexOf('--id'); + argvId = process.argv[idIndex + 1]; const { protocol: workerProtocal, hostname: workerHostname, @@ -49,6 +48,7 @@ describe('Test Vc (direct invocation)', function () { async function linkIdentityViaCli(id: string) { const credentialDefinitions = credentialsJson.find((item) => item.id === id) as CredentialDefinition; + console.log(`linking identity-${credentialDefinitions.mockDid} via cli`); const keyringPair = randomSubstrateWallet(); keyringPairs.push(keyringPair); @@ -79,15 +79,16 @@ describe('Test Vc (direct invocation)', function () { async function requestVc(id: string, index: number) { const credentialDefinitions = credentialsJson.find((item) => item.id === id) as CredentialDefinition; - const assertion = { [credentialDefinitions.assertion.id]: credentialDefinitions.assertion.payload, }; + console.log('vc description: ', credentialDefinitions.description); + + console.log('assertion: ', assertion); let currentNonce = (await getSidechainNonce(context, teeShieldingKey, substrateIdentities[index])).toNumber(); const getNextNonce = () => currentNonce++; const nonce = getNextNonce(); - console.log(nonce, substrateIdentities[index].toHuman(), u8aToHex(keyringPairs[index].publicKey)); const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; const requestVcCall = await createSignedTrustedCallRequestVc( @@ -106,29 +107,26 @@ describe('Test Vc (direct invocation)', function () { const vcResults = context.api.createType('RequestVCResult', res.value) as unknown as RequestVCResult; const decryptVcPayload = decryptWithAes(aesKey, vcResults.vc_payload, 'utf-8').replace('0x', ''); const vcPayloadJson = JSON.parse(decryptVcPayload); + console.log('vcPayload: ', vcPayloadJson); - assert.equal(vcPayloadJson.credentialSubject.values[0], credentialDefinitions.expectedCredentialValue); + assert.equal( + vcPayloadJson.credentialSubject.values[0], + credentialDefinitions.expectedCredentialValue, + "credential value doesn't match, please check the credential json expectedCredentialValue" + ); } if (argvId && credentialsJson.find((item) => item.id === argvId)) { - const credentialDefinitions = credentialsJson.find((item) => item.id === argvId) as CredentialDefinition; - - step( - `linking identity::${credentialDefinitions.mockDid} via cli and request vc::${credentialDefinitions.mockDid}`, - async function () { - await linkIdentityViaCli(argvId); - await requestVc(argvId, 0); - } - ); + step(`link identity && request vc with specific credentials for ${argvId}`, async function () { + await linkIdentityViaCli(argvId); + await requestVc(argvId, 0); + }); } else { credentialsJson.forEach(({ id }, index) => { - step( - `linking identity::${credentialsJson[index].mockDid} via cli and request vc::${credentialsJson[index].id}`, - async function () { - await linkIdentityViaCli(id); - await requestVc(id, index); - } - ); + step(`link identity && request vc with all credentials for ${id}`, async function () { + await linkIdentityViaCli(id); + await requestVc(id, index); + }); }); } }); diff --git a/tee-worker/ts-tests/integration-tests/package.json b/tee-worker/ts-tests/integration-tests/package.json index 1e5f3dd9a7..991658cd0f 100644 --- a/tee-worker/ts-tests/integration-tests/package.json +++ b/tee-worker/ts-tests/integration-tests/package.json @@ -19,8 +19,8 @@ "test-ii-vc:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_vc.test.ts'", "test-ii-batch:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_batch.test.ts'", "test-ii-batch:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_batch.test.ts'", - "test-data-providers:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-providers.test.ts'", - "test-data-providers:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-providers.test.ts'" + "test-data-provider:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-provider.test.ts'", + "test-data-provider:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-provider.test.ts'" }, "dependencies": { "@noble/ed25519": "^1.7.3", From 9e876e52c9b9dc746d198635d7183182f366dedc Mon Sep 17 00:00:00 2001 From: "will.li" <120463031+higherordertech@users.noreply.github.com> Date: Wed, 7 Feb 2024 19:03:12 +1100 Subject: [PATCH 17/64] feat: P-431 add karat dao user vc (#2467) Co-authored-by: higherordertech Co-authored-by: BillyWooo --- primitives/core/src/assertion.rs | 8 +- primitives/core/src/lib.rs | 3 + primitives/core/src/platform_user.rs | 35 ++++ .../commands/litentry/request_vc.rs | 16 +- .../commands/litentry/request_vc_direct.rs | 8 +- .../interfaces/vc/definitions.ts | 5 + .../core/assertion-build-v2/src/lib.rs | 1 + .../src/platform_user/mod.rs | 180 ++++++++++++++++ tee-worker/litentry/core/common/src/lib.rs | 1 + .../core/common/src/platform_user/mod.rs | 35 ++++ .../litentry/core/credentials-v2/src/lib.rs | 1 + .../credentials-v2/src/platform_user/mod.rs | 61 ++++++ .../core/data-providers/src/karat_dao.rs | 194 ++++++++++++++++++ .../litentry/core/data-providers/src/lib.rs | 28 +++ .../core/mock-server/src/karat_dao.rs | 42 ++++ .../litentry/core/mock-server/src/lib.rs | 2 + tee-worker/litentry/core/service/src/lib.rs | 1 + .../src/platform_user/karat_dao_user.rs | 50 +++++ .../core/service/src/platform_user/mod.rs | 40 ++++ .../receiver/src/handler/assertion.rs | 7 + .../lc-vc-task-receiver/src/vc_handling.rs | 7 + tee-worker/litentry/primitives/src/lib.rs | 4 +- tee-worker/service/src/prometheus_metrics.rs | 1 + 23 files changed, 722 insertions(+), 8 deletions(-) create mode 100644 primitives/core/src/platform_user.rs create mode 100644 tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs create mode 100644 tee-worker/litentry/core/common/src/platform_user/mod.rs create mode 100644 tee-worker/litentry/core/credentials-v2/src/platform_user/mod.rs create mode 100644 tee-worker/litentry/core/data-providers/src/karat_dao.rs create mode 100644 tee-worker/litentry/core/mock-server/src/karat_dao.rs create mode 100644 tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs create mode 100644 tee-worker/litentry/core/service/src/platform_user/mod.rs diff --git a/primitives/core/src/assertion.rs b/primitives/core/src/assertion.rs index 99de17a200..e1efc63f20 100644 --- a/primitives/core/src/assertion.rs +++ b/primitives/core/src/assertion.rs @@ -19,8 +19,8 @@ use crate::{ all_web3networks, AccountId, BnbDigitDomainType, BoundedWeb3Network, EVMTokenType, - GenericDiscordRoleType, OneBlockCourseType, VIP3MembershipCardLevel, Web3Network, - Web3TokenType, + GenericDiscordRoleType, OneBlockCourseType, PlatformUserType, VIP3MembershipCardLevel, + Web3Network, Web3TokenType, }; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -261,6 +261,9 @@ pub enum Assertion { #[codec(index = 24)] TokenHoldingAmount(Web3TokenType), + + #[codec(index = 25)] + PlatformUser(PlatformUserType), } impl Assertion { @@ -309,6 +312,7 @@ impl Assertion { // no web3 network is allowed Self::A2(..) | Self::A3(..) | Self::A6 | Self::GenericDiscordRole(..) => vec![], Self::TokenHoldingAmount(t_type) => t_type.get_supported_networks(), + Self::PlatformUser(p_type) => p_type.get_supported_networks(), } } } diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index 25e3402370..e9f94c5f07 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -62,6 +62,9 @@ pub use evm_amount_holding::*; mod web3_token; pub use web3_token::*; +mod platform_user; +pub use platform_user::*; + /// Common types of parachains. mod types { use sp_runtime::{ diff --git a/primitives/core/src/platform_user.rs b/primitives/core/src/platform_user.rs new file mode 100644 index 0000000000..c7977a0e50 --- /dev/null +++ b/primitives/core/src/platform_user.rs @@ -0,0 +1,35 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_std::vec::Vec; + +use crate::{all_evm_web3networks, Web3Network}; + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub enum PlatformUserType { + #[codec(index = 0)] + KaratDaoUser, +} + +impl PlatformUserType { + pub fn get_supported_networks(&self) -> Vec { + match self { + Self::KaratDaoUser => all_evm_web3networks(), + } + } +} diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs index 17158a882a..7497931eb7 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs @@ -29,8 +29,9 @@ use litentry_primitives::{ AchainableAmounts, AchainableBasic, AchainableBetweenPercents, AchainableClassOfYear, AchainableDate, AchainableDateInterval, AchainableDatePercent, AchainableParams, AchainableToken, Assertion, BoundedWeb3Network, ContestType, EVMTokenType, - GenericDiscordRoleType, Identity, OneBlockCourseType, ParameterString, RequestAesKey, - SoraQuizType, VIP3MembershipCardLevel, Web3Network, Web3TokenType, REQUEST_AES_KEY_LEN, + GenericDiscordRoleType, Identity, OneBlockCourseType, ParameterString, PlatformUserType, + RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, Web3Network, Web3TokenType, + REQUEST_AES_KEY_LEN, }; use sp_core::Pair; @@ -108,6 +109,8 @@ pub enum Command { BRC20AmountHolder, #[clap(subcommand)] TokenHoldingAmount(TokenHoldingAmountCommand), + #[clap(subcommand)] + PlatformUser(PlatformUserCommand), } #[derive(Args, Debug)] @@ -220,6 +223,11 @@ pub enum TokenHoldingAmountCommand { Trx, } +#[derive(Subcommand, Debug)] +pub enum PlatformUserCommand { + KaratDaoUser, +} + // I haven't found a good way to use common args for subcommands #[derive(Args, Debug)] pub struct AmountHoldingArg { @@ -544,6 +552,10 @@ impl RequestVcCommand { TokenHoldingAmountCommand::Ton => Assertion::TokenHoldingAmount(Web3TokenType::Ton), TokenHoldingAmountCommand::Trx => Assertion::TokenHoldingAmount(Web3TokenType::Trx), }, + Command::PlatformUser(arg) => match arg { + PlatformUserCommand::KaratDaoUser => + Assertion::PlatformUser(PlatformUserType::KaratDaoUser), + }, }; let key = Self::random_aes_key(); diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs index 15e5153610..75255f4d29 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs @@ -30,8 +30,8 @@ use litentry_primitives::{ AchainableAmounts, AchainableBasic, AchainableBetweenPercents, AchainableClassOfYear, AchainableDate, AchainableDateInterval, AchainableDatePercent, AchainableParams, AchainableToken, Assertion, ContestType, EVMTokenType, GenericDiscordRoleType, Identity, - OneBlockCourseType, RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, Web3Network, - Web3TokenType, REQUEST_AES_KEY_LEN, + OneBlockCourseType, PlatformUserType, RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, + Web3Network, Web3TokenType, REQUEST_AES_KEY_LEN, }; use sp_core::Pair; @@ -293,6 +293,10 @@ impl RequestVcDirectCommand { TokenHoldingAmountCommand::Ton => Assertion::TokenHoldingAmount(Web3TokenType::Ton), TokenHoldingAmountCommand::Trx => Assertion::TokenHoldingAmount(Web3TokenType::Trx), }, + Command::PlatformUser(arg) => match arg { + PlatformUserCommand::KaratDaoUser => + Assertion::PlatformUser(PlatformUserType::KaratDaoUser), + }, }; let key: [u8; 32] = Self::random_aes_key(); diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts index 701fdf5377..f5889c3300 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts @@ -32,6 +32,7 @@ export default { BRC20AmountHolder: "Null", CyptoSummary: "Null", TokenHoldingAmount: "Web3TokenType", + PlatformUser: "PlatformUserType", }, }, AssertionSupportedNetwork: { @@ -173,5 +174,9 @@ export default { "Trx", ], }, + // PlatformUserType + PlatformUserType: { + _enum: ["KaratDaoUser"], + }, }, }; diff --git a/tee-worker/litentry/core/assertion-build-v2/src/lib.rs b/tee-worker/litentry/core/assertion-build-v2/src/lib.rs index 4d4e798eac..69ea1134cd 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/lib.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/lib.rs @@ -41,4 +41,5 @@ use lc_assertion_build::{transpose_identity, Result}; use lc_service::DataProviderConfig; use log::*; +pub mod platform_user; pub mod token_holding_amount; diff --git a/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs new file mode 100644 index 0000000000..5ba68f7f9b --- /dev/null +++ b/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs @@ -0,0 +1,180 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use std::collections::HashSet; + +use lc_credentials_v2::{platform_user::PlatformUserAssertionUpdate, Credential}; +use lc_service::platform_user::is_user; +use lc_stf_task_sender::AssertionBuildRequest; +use litentry_primitives::{PlatformUserType, Web3Network}; +use log::debug; + +use crate::*; + +pub fn build( + req: &AssertionBuildRequest, + platform_user_type: PlatformUserType, + data_provider_config: &DataProviderConfig, +) -> Result { + debug!("platform user: {:?}", platform_user_type); + + let identities: Vec<(Web3Network, Vec)> = transpose_identity(&req.identities); + let addresses: Vec = identities + .into_iter() + .flat_map(|(_, addresses)| addresses.into_iter()) + .collect::>() + .into_iter() + .collect(); + + let result = + is_user(platform_user_type.clone(), addresses, data_provider_config).map_err(|e| { + Error::RequestVCFailed( + Assertion::PlatformUser(platform_user_type.clone()), + ErrorDetail::DataProviderError(ErrorString::truncate_from( + format!("{e:?}").as_bytes().to_vec(), + )), + ) + }); + + match result { + Ok(value) => match Credential::new(&req.who, &req.shard) { + Ok(mut credential_unsigned) => { + credential_unsigned.update_platform_user_assertion(platform_user_type, value); + Ok(credential_unsigned) + }, + Err(e) => { + error!("Generate unsigned credential failed {:?}", e); + Err(Error::RequestVCFailed( + Assertion::PlatformUser(platform_user_type), + e.into_error_detail(), + )) + }, + }, + Err(e) => Err(e), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use itp_stf_primitives::types::ShardIdentifier; + use itp_types::AccountId; + use lc_common::platform_user::PlatformName; + use lc_credentials_v2::assertion_logic::{AssertionLogic, Op}; + use lc_mock_server::run; + use litentry_hex_utils::decode_hex; + use litentry_primitives::{Identity, IdentityNetworkTuple}; + + fn init() -> DataProviderConfig { + let _ = env_logger::builder().is_test(true).try_init(); + let url = run(0).unwrap() + "/karat_dao/"; + + let mut config = DataProviderConfig::new(); + config.set_karat_dao_api_url(url); + config + } + + fn crate_assertion_build_request( + platform_user_type: PlatformUserType, + identities: Vec, + ) -> AssertionBuildRequest { + AssertionBuildRequest { + shard: ShardIdentifier::default(), + signer: AccountId::from([0; 32]), + who: AccountId::from([0; 32]).into(), + assertion: Assertion::PlatformUser(platform_user_type), + identities, + top_hash: Default::default(), + parachain_block_number: 0u32, + sidechain_block_number: 0u32, + maybe_key: None, + should_create_id_graph: false, + req_ext_hash: Default::default(), + } + } + + fn build_and_assert_result( + identities: Vec, + platform_user_type: PlatformUserType, + assertion_value: bool, + data_provider_config: &DataProviderConfig, + ) { + let req = crate_assertion_build_request(PlatformUserType::KaratDaoUser, identities); + + match build(&req, platform_user_type.clone(), &data_provider_config) { + Ok(credential) => { + log::info!("build karat dao user done"); + assert_eq!( + *(credential.credential_subject.assertions.first().unwrap()), + AssertionLogic::And { + items: vec![Box::new(AssertionLogic::Item { + src: "$platform".into(), + op: Op::Equal, + dst: platform_user_type.get_platform_name().into() + })] + } + ); + assert_eq!( + *(credential.credential_subject.values.first().unwrap()), + assertion_value + ); + }, + Err(e) => { + panic!("build karat dao user failed with error {:?}", e); + }, + } + } + + #[test] + fn build_karat_dao_user_works() { + let data_provider_config = init(); + let mut address = + decode_hex("0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".as_bytes().to_vec()) + .unwrap() + .as_slice() + .try_into() + .unwrap(); + let mut identities: Vec = + vec![(Identity::Evm(address), vec![Web3Network::Ethereum])]; + + build_and_assert_result( + identities, + PlatformUserType::KaratDaoUser, + true, + &data_provider_config, + ); + + address = decode_hex("0x75438d34c9125839c8b08d21b7f3167281659e7c".as_bytes().to_vec()) + .unwrap() + .as_slice() + .try_into() + .unwrap(); + identities = vec![(Identity::Evm(address), vec![Web3Network::Bsc, Web3Network::Ethereum])]; + + build_and_assert_result( + identities, + PlatformUserType::KaratDaoUser, + false, + &data_provider_config, + ); + } +} diff --git a/tee-worker/litentry/core/common/src/lib.rs b/tee-worker/litentry/core/common/src/lib.rs index bb84990a23..b8654a586f 100644 --- a/tee-worker/litentry/core/common/src/lib.rs +++ b/tee-worker/litentry/core/common/src/lib.rs @@ -24,6 +24,7 @@ extern crate sgx_tstd as std; use litentry_primitives::Web3Network; +pub mod platform_user; pub mod web3_token; pub fn web3_network_to_chain(network: &Web3Network) -> &'static str { diff --git a/tee-worker/litentry/core/common/src/platform_user/mod.rs b/tee-worker/litentry/core/common/src/platform_user/mod.rs new file mode 100644 index 0000000000..8cb8b9c21a --- /dev/null +++ b/tee-worker/litentry/core/common/src/platform_user/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use litentry_primitives::PlatformUserType; + +pub trait PlatformName { + fn get_platform_name(&self) -> &'static str; +} + +impl PlatformName for PlatformUserType { + fn get_platform_name(&self) -> &'static str { + match self { + Self::KaratDaoUser => "KaratDao", + } + } +} diff --git a/tee-worker/litentry/core/credentials-v2/src/lib.rs b/tee-worker/litentry/core/credentials-v2/src/lib.rs index 4bdd90bb13..0921662eb3 100644 --- a/tee-worker/litentry/core/credentials-v2/src/lib.rs +++ b/tee-worker/litentry/core/credentials-v2/src/lib.rs @@ -36,4 +36,5 @@ compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the sam // TODO migration to v2 in the future pub use lc_credentials::{assertion_logic, Credential}; +pub mod platform_user; pub mod token_holding_amount; diff --git a/tee-worker/litentry/core/credentials-v2/src/platform_user/mod.rs b/tee-worker/litentry/core/credentials-v2/src/platform_user/mod.rs new file mode 100644 index 0000000000..82c1ea7a76 --- /dev/null +++ b/tee-worker/litentry/core/credentials-v2/src/platform_user/mod.rs @@ -0,0 +1,61 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use lc_common::platform_user::PlatformName; +use litentry_primitives::PlatformUserType; + +// TODO migration to v2 in the future +use lc_credentials::{ + assertion_logic::{AssertionLogic, Op}, + Credential, +}; + +const TYPE: &str = "Platform user"; +const DESCRIPTION: &str = "You are a user of a certain platform"; + +pub trait PlatformUserAssertionUpdate { + fn update_platform_user_assertion( + &mut self, + platform_user_type: PlatformUserType, + is_user: bool, + ); +} + +impl PlatformUserAssertionUpdate for Credential { + fn update_platform_user_assertion( + &mut self, + platform_user_type: PlatformUserType, + is_user: bool, + ) { + self.add_subject_info(DESCRIPTION, TYPE); + + let mut assertion = AssertionLogic::new_and(); + assertion = assertion.add_item(AssertionLogic::new_item( + "$platform", + Op::Equal, + platform_user_type.get_platform_name(), + )); + + self.credential_subject.assertions.push(assertion); + self.credential_subject.values.push(is_user); + } +} diff --git a/tee-worker/litentry/core/data-providers/src/karat_dao.rs b/tee-worker/litentry/core/data-providers/src/karat_dao.rs new file mode 100644 index 0000000000..2a211b22a1 --- /dev/null +++ b/tee-worker/litentry/core/data-providers/src/karat_dao.rs @@ -0,0 +1,194 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +use crate::sgx_reexport_prelude::*; + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use crate::{build_client, DataProviderConfig, Error, HttpError}; +use http::header::CONNECTION; +use http_req::response::Headers; +use itc_rest_client::{ + http_client::{DefaultSend, HttpClient}, + rest_client::RestClient, + RestGet, RestPath, +}; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::{ + format, str, + string::{String, ToString}, + thread, time, vec, + vec::Vec, +}; + +pub struct KaratDaoClient { + api_retry_delay: u64, + api_retry_times: u16, + client: RestClient>, +} + +#[derive(Debug)] +pub struct KaraDaoRequest { + path: String, + query: Option>, +} + +impl KaratDaoClient { + pub fn new(data_provider_config: &DataProviderConfig) -> Self { + let api_retry_delay = data_provider_config.karat_dao_api_retry_delay; + let api_retry_times = data_provider_config.karat_dao_api_retry_times; + let api_url = data_provider_config.karat_dao_api_url.clone(); + + let mut headers = Headers::new(); + headers.insert(CONNECTION.as_str(), "close"); + let client = build_client(api_url.as_str(), headers); + + KaratDaoClient { api_retry_delay, api_retry_times, client } + } + + fn retry(&mut self, action: A) -> Result + where + A: Fn(&mut KaratDaoClient) -> Result, + { + let mut retries = 0; + // retry delay 1 second + let base_delay = time::Duration::from_millis(self.api_retry_delay); + // maximum 5 retry times + let maximum_retries = self.api_retry_times; + + loop { + if retries > 0 { + debug!("Fail to call karat dao api, begin retry: {}", retries); + } + + if retries > maximum_retries { + return Err(Error::RequestError(format!( + "Fail to call call karat dao api within {} retries", + maximum_retries + ))) + } + + match action(self) { + Ok(response) => return Ok(response), + Err(err) => { + let req_err: Error = + Error::RequestError(format!("karat dao api error: {}", err)); + match err { + HttpError::HttpError(code, _) => + if code == 429 { + // Too Many Requests + // exponential back off + thread::sleep(base_delay * 2u32.pow(retries as u32)); + retries += 1; + } else { + return Err(req_err) + }, + _ => return Err(req_err), + } + }, + } + } + } + + fn get(&mut self, params: KaraDaoRequest) -> Result + where + T: serde::de::DeserializeOwned + RestPath, + { + if let Some(query) = params.query { + let transformed_query: Vec<(&str, &str)> = + query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + self.retry(|c| c.client.get_with::(params.path.clone(), &transformed_query)) + } else { + self.retry(|c| c.client.get::(params.path.clone())) + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserVerificationResponse { + pub result: UserVerificationResult, +} + +impl RestPath for UserVerificationResponse { + fn get_path(path: String) -> Result { + Ok(path) + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct UserVerificationResult { + pub is_valid: bool, +} + +pub trait KaraDaoApi { + fn user_verification(&mut self, address: String) -> Result; +} + +impl KaraDaoApi for KaratDaoClient { + fn user_verification(&mut self, address: String) -> Result { + let query: Vec<(String, String)> = vec![("address".to_string(), address)]; + + let params = KaraDaoRequest { path: "user/verification".into(), query: Some(query) }; + + debug!("user_verification, params: {:?}", params); + + match self.get::(params) { + Ok(resp) => { + debug!("user_verification, response: {:?}", resp); + Ok(resp) + }, + Err(e) => { + debug!("user_verification, error: {:?}", e); + Err(Error::RequestError(format!("{:?}", e))) + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lc_mock_server::run; + + fn init() -> DataProviderConfig { + let _ = env_logger::builder().is_test(true).try_init(); + let url = run(0).unwrap() + "/karat_dao/"; + + let mut config = DataProviderConfig::new(); + config.set_karat_dao_api_url(url); + config + } + + #[test] + fn does_user_verification_works() { + let config = init(); + let mut client = KaratDaoClient::new(&config); + let mut response = client + .user_verification("0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".into()) + .unwrap(); + assert_eq!(response.result.is_valid, true); + + response = client + .user_verification("0x9401518f4ebba857baa879d9f76e1cc8b31ed197".into()) + .unwrap(); + assert_eq!(response.result.is_valid, false); + } +} diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index c0fac547ce..48fb2c9e86 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -69,6 +69,7 @@ pub mod achainable_names; pub mod discord_litentry; pub mod discord_official; pub mod geniidata; +pub mod karat_dao; pub mod nodereal; pub mod nodereal_jsonrpc; pub mod twitter_official; @@ -190,6 +191,9 @@ pub struct DataProviderConfig { pub geniidata_url: String, pub geniidata_api_key: String, pub litentry_archive_url: String, + pub karat_dao_api_retry_delay: u64, + pub karat_dao_api_retry_times: u16, + pub karat_dao_api_url: String, } impl Default for DataProviderConfig { @@ -231,6 +235,9 @@ impl DataProviderConfig { geniidata_url: "https://api.geniidata.com/api/1/brc20/balance?".to_string(), geniidata_api_key: "".to_string(), litentry_archive_url: "https://archive-test.litentry.io".to_string(), + karat_dao_api_retry_delay: 5000, + karat_dao_api_retry_times: 2, + karat_dao_api_url: "https://api.karatdao.com/".to_string(), }; // we allow to override following config properties for non prod dev @@ -292,6 +299,15 @@ impl DataProviderConfig { if let Ok(v) = env::var("LITENTRY_ARCHIVE_URL") { config.set_litentry_archive_url(v); } + if let Ok(v) = env::var("KARAT_DAO_API_RETRY_DELAY") { + config.set_karat_dao_api_retry_delay(v.parse::().unwrap()); + } + if let Ok(v) = env::var("KARAT_DAO_API_RETRY_TIME") { + config.set_karat_dao_api_retry_times(v.parse::().unwrap()); + } + if let Ok(v) = env::var("KARAT_DAO_API_URL") { + config.set_karat_dao_api_url(v); + } }); // set secrets from env variables if let Ok(v) = env::var("TWITTER_AUTH_TOKEN_V2") { @@ -414,6 +430,18 @@ impl DataProviderConfig { debug!("set_litentry_archive_url: {:?}", v); self.litentry_archive_url = v; } + pub fn set_karat_dao_api_retry_delay(&mut self, v: u64) { + debug!("set_karat_dao_api_retry_delay: {:?}", v); + self.karat_dao_api_retry_delay = v; + } + pub fn set_karat_dao_api_retry_times(&mut self, v: u16) { + debug!("set_karat_dao_api_retry_times: {:?}", v); + self.karat_dao_api_retry_times = v; + } + pub fn set_karat_dao_api_url(&mut self, v: String) { + debug!("set_karat_dao_api_url: {:?}", v); + self.karat_dao_api_url = v; + } } #[derive(Debug, thiserror::Error, Clone)] diff --git a/tee-worker/litentry/core/mock-server/src/karat_dao.rs b/tee-worker/litentry/core/mock-server/src/karat_dao.rs new file mode 100644 index 0000000000..3178d750d8 --- /dev/null +++ b/tee-worker/litentry/core/mock-server/src/karat_dao.rs @@ -0,0 +1,42 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . +#![allow(opaque_hidden_inferred_bound)] + +use std::collections::HashMap; + +use lc_data_providers::karat_dao::{UserVerificationResponse, UserVerificationResult}; + +use warp::{http::Response, Filter}; + +pub(crate) fn query() -> impl Filter + Clone { + warp::get() + .and(warp::path!("karat_dao" / "user" / "verification")) + .and(warp::query::>()) + .map(move |p: HashMap| { + let default = String::default(); + let address = p.get("address").unwrap_or(&default); + + if address == "0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee" { + let body = + UserVerificationResponse { result: UserVerificationResult { is_valid: true } }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } else { + let body = + UserVerificationResponse { result: UserVerificationResult { is_valid: false } }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } + }) +} diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index f60f4a303d..b48fcb0224 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -24,6 +24,7 @@ use warp::Filter; pub mod achainable; pub mod discord_litentry; pub mod discord_official; +pub mod karat_dao; pub mod litentry_archive; pub mod nodereal_jsonrpc; pub mod twitter_litentry; @@ -63,6 +64,7 @@ pub fn run(port: u16) -> Result { .or(discord_litentry::check_join()) .or(discord_litentry::has_role()) .or(nodereal_jsonrpc::query()) + .or(karat_dao::query()) .or(achainable::query()) .or(litentry_archive::query_user_joined_evm_campaign()) .boxed(), diff --git a/tee-worker/litentry/core/service/src/lib.rs b/tee-worker/litentry/core/service/src/lib.rs index e1773ead74..281a5595e1 100644 --- a/tee-worker/litentry/core/service/src/lib.rs +++ b/tee-worker/litentry/core/service/src/lib.rs @@ -34,4 +34,5 @@ use litentry_primitives::{ErrorDetail as Error, IntoErrorDetail, Web3Network, We pub use lc_data_providers::DataProviderConfig; +pub mod platform_user; pub mod web3_token; diff --git a/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs b/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs new file mode 100644 index 0000000000..584d7806d3 --- /dev/null +++ b/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs @@ -0,0 +1,50 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use core::result::Result; + +use lc_data_providers::{ + karat_dao::{KaraDaoApi, KaratDaoClient}, + DataProviderConfig, +}; + +use crate::*; + +pub fn is_user( + addresses: Vec, + data_provider_config: &DataProviderConfig, +) -> Result { + let mut is_user = false; + let mut client = KaratDaoClient::new(data_provider_config); + for address in addresses { + match client.user_verification(address) { + Ok(response) => { + is_user = response.result.is_valid; + if is_user { + break + } + }, + Err(err) => return Err(err.into_error_detail()), + } + } + Ok(is_user) +} diff --git a/tee-worker/litentry/core/service/src/platform_user/mod.rs b/tee-worker/litentry/core/service/src/platform_user/mod.rs new file mode 100644 index 0000000000..5eac7fdd53 --- /dev/null +++ b/tee-worker/litentry/core/service/src/platform_user/mod.rs @@ -0,0 +1,40 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use core::result::Result; + +use lc_data_providers::DataProviderConfig; +use litentry_primitives::PlatformUserType; + +use crate::*; + +mod karat_dao_user; + +pub fn is_user( + platform_user_type: PlatformUserType, + addresses: Vec, + data_provider_config: &DataProviderConfig, +) -> Result { + match platform_user_type { + PlatformUserType::KaratDaoUser => karat_dao_user::is_user(addresses, data_provider_config), + } +} diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index 9ea4dd0743..3c44d2bf4e 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -200,6 +200,13 @@ where token_type, &self.context.data_provider_config, ), + + Assertion::PlatformUser(platform_user_type) => + lc_assertion_build_v2::platform_user::build( + &self.req, + platform_user_type, + &self.context.data_provider_config, + ), }?; // post-process the credential diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs index e3d05c2b22..4e3f8480f2 100644 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs +++ b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs @@ -176,6 +176,13 @@ where token_type, &self.context.data_provider_config, ), + + Assertion::PlatformUser(platform_user_type) => + lc_assertion_build_v2::platform_user::build( + &self.req, + platform_user_type, + &self.context.data_provider_config, + ), }?; // post-process the credential diff --git a/tee-worker/litentry/primitives/src/lib.rs b/tee-worker/litentry/primitives/src/lib.rs index ef643d7c19..af8137c463 100644 --- a/tee-worker/litentry/primitives/src/lib.rs +++ b/tee-worker/litentry/primitives/src/lib.rs @@ -53,8 +53,8 @@ pub use parentchain_primitives::{ BoundedWeb3Network, ContestType, EVMTokenType, ErrorDetail, ErrorString, GenericDiscordRoleType, Hash as ParentchainHash, Header as ParentchainHeader, IMPError, Index as ParentchainIndex, IntoErrorDetail, OneBlockCourseType, ParameterString, - SchemaContentString, SchemaIdString, Signature as ParentchainSignature, SoraQuizType, - VCMPError, VIP3MembershipCardLevel, Web3Network, Web3TokenType, MINUTES, + PlatformUserType, SchemaContentString, SchemaIdString, Signature as ParentchainSignature, + SoraQuizType, VCMPError, VIP3MembershipCardLevel, Web3Network, Web3TokenType, MINUTES, }; use scale_info::TypeInfo; use sp_core::{ecdsa, ed25519, sr25519, ByteArray}; diff --git a/tee-worker/service/src/prometheus_metrics.rs b/tee-worker/service/src/prometheus_metrics.rs index c998261561..18930aaa2c 100644 --- a/tee-worker/service/src/prometheus_metrics.rs +++ b/tee-worker/service/src/prometheus_metrics.rs @@ -292,6 +292,7 @@ fn handle_stf_call_request(req: RequestType, time: f64) { Assertion::BRC20AmountHolder => "BRC20AmountHolder", Assertion::CryptoSummary => "CryptoSummary", Assertion::TokenHoldingAmount(_) => "TokenHoldingAmount", + Assertion::PlatformUser(_) => "PlatformUser", }, }; inc_stf_calls(category, label); From be7b23da0cce78e66c7e7ec270cf6666a33be9fd Mon Sep 17 00:00:00 2001 From: Verin1005 <104152026+Verin1005@users.noreply.github.com> Date: Thu, 8 Feb 2024 17:46:36 +0800 Subject: [PATCH 18/64] p-496 Add vip3 service mock (#2470) * add vip3 service mock * modify description * format * fix yml env * small update --------- Co-authored-by: Kailai Wang --- tee-worker/Cargo.lock | 18 +++++++ tee-worker/docker/docker-compose.yml | 1 + .../docker/multiworker-docker-compose.yml | 3 ++ .../litentry/core/mock-server/src/lib.rs | 2 + .../litentry/core/mock-server/src/vip3.rs | 47 +++++++++++++++++++ tee-worker/local-setup/.env.dev | 2 +- .../common/utils/assertion.ts | 3 +- .../common/utils/vc-helper.ts | 4 +- 8 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 tee-worker/litentry/core/mock-server/src/vip3.rs diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index fca0a616d6..b3a43d3533 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -8723,6 +8723,23 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-bitacross" +version = "0.1.0" +dependencies = [ + "core-primitives", + "frame-benchmarking", + "frame-support", + "frame-system", + "pallet-teerex", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-runtime", + "sp-std 5.0.0", + "teerex-primitives", +] + [[package]] name = "pallet-bounties" version = "4.0.0-dev" @@ -11206,6 +11223,7 @@ dependencies = [ "pallet-aura", "pallet-authorship", "pallet-balances", + "pallet-bitacross", "pallet-bounties", "pallet-bridge", "pallet-bridge-transfer", diff --git a/tee-worker/docker/docker-compose.yml b/tee-worker/docker/docker-compose.yml index 44d8743e33..c9436092d0 100644 --- a/tee-worker/docker/docker-compose.yml +++ b/tee-worker/docker/docker-compose.yml @@ -136,6 +136,7 @@ services: - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID - LITENTRY_ARCHIVE_URL=http://localhost:19527 + - VIP3_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: diff --git a/tee-worker/docker/multiworker-docker-compose.yml b/tee-worker/docker/multiworker-docker-compose.yml index 5e92664999..ffc6c7d27c 100644 --- a/tee-worker/docker/multiworker-docker-compose.yml +++ b/tee-worker/docker/multiworker-docker-compose.yml @@ -137,6 +137,7 @@ services: - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID - LITENTRY_ARCHIVE_URL=http://localhost:19527 + - VIP3_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: @@ -188,6 +189,7 @@ services: - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID - LITENTRY_ARCHIVE_URL=http://localhost:19527 + - VIP3_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: @@ -239,6 +241,7 @@ services: - CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID - CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID - LITENTRY_ARCHIVE_URL=http://localhost:19527 + - VIP3_URL=http://localhost:19527 networks: - litentry-test-network healthcheck: diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index b48fcb0224..7e640eb1b1 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -29,6 +29,7 @@ pub mod litentry_archive; pub mod nodereal_jsonrpc; pub mod twitter_litentry; pub mod twitter_official; +pub mod vip3; // It should only works on UNIX. async fn shutdown_signal() { @@ -67,6 +68,7 @@ pub fn run(port: u16) -> Result { .or(karat_dao::query()) .or(achainable::query()) .or(litentry_archive::query_user_joined_evm_campaign()) + .or(vip3::query_user_sbt_level()) .boxed(), ) .bind_with_graceful_shutdown(([127, 0, 0, 1], port), shutdown_signal()); diff --git a/tee-worker/litentry/core/mock-server/src/vip3.rs b/tee-worker/litentry/core/mock-server/src/vip3.rs new file mode 100644 index 0000000000..0557ee56c0 --- /dev/null +++ b/tee-worker/litentry/core/mock-server/src/vip3.rs @@ -0,0 +1,47 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![allow(opaque_hidden_inferred_bound)] + +use lc_data_providers::vip3::{LevelEntity, VIP3SBTInfoResponse}; +use std::collections::HashMap; +use warp::{http::Response, Filter}; + +pub(crate) fn query_user_sbt_level( +) -> impl Filter + Clone { + warp::get() + .and(warp::path!("api" / "v1" / "sbt" / "info")) + .and(warp::query::>()) + .map(move |p: HashMap| { + let default = String::default(); + let account = p.get("addr").unwrap_or(&default); + if account == "0xff93b45308fd417df303d6515ab04d9e89a750ca" { + let body = VIP3SBTInfoResponse { + code: 0, + msg: "success".to_string(), + data: LevelEntity { level: 1 }, + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } else { + let body = VIP3SBTInfoResponse { + code: 0, + msg: "success".to_string(), + data: LevelEntity { level: 0 }, + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } + }) +} diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index 1e30335fe5..b07e56548d 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -48,6 +48,6 @@ NODEREAL_API_CHAIN_NETWORK_URL=https://{chain}-mainnet.nodereal.io/ CONTEST_LEGEND_DISCORD_ROLE_ID=CONTEST_LEGEND_DISCORD_ROLE_ID CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID -VIP3_URL=https://dappapi.vip3.io/ +VIP3_URL=http://localhost:19527 GENIIDATA_URL=https://api.geniidata.com/api/1/brc20/balance? LITENTRY_ARCHIVE_URL=http://localhost:19527 \ No newline at end of file diff --git a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts index fee60cc199..54e4c91906 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts @@ -272,7 +272,8 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri // step 4 // extrac proof and vc without proof json const vcPayloadJson = JSON.parse(decryptVcPayload); - + console.log('credential: ', vcPayloadJson); + console.log('assertions: ', vcPayloadJson.credentialSubject.assertions); const { proof, ...vcWithoutProof } = vcPayloadJson; // step 5 diff --git a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts index 9ad26dda1d..c5df98b20d 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts @@ -132,9 +132,9 @@ export const defaultAssertions = [ }, // VIP3 { - description: 'VIP3 Gold Card Holder', + description: 'VIP3 Silver Card Holder', assertion: { - VIP3MembershipCard: 'Gold', + VIP3MembershipCard: 'Silver', }, }, ]; From e97481e046c44b91ea7ac3338a8e28ab6cd9b76d Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Fri, 9 Feb 2024 12:21:40 +0100 Subject: [PATCH 19/64] Adjust tee-worker counterpart of pallet-teebag (#2469) * add AccountId to Enclave * refactor teebag * compiles with teebag * more update * remove unneeded tests * use dev mode in genesis * fix tests * fix impl * fix compile errors * comment out test * try to fix ts-test * update based on reviews * optimise * add more debug * ts-code verbessern --- .github/workflows/ci.yml | 3 - bitacross-worker/Cargo.lock | 28 +++ bitacross-worker/enclave-runtime/Cargo.lock | 95 +++++++-- node/src/chain_specs/rococo.rs | 6 +- pallets/teebag/src/lib.rs | 190 +++++++----------- pallets/teebag/src/mock.rs | 3 +- pallets/teebag/src/sgx_verify/collateral.rs | 1 - pallets/teebag/src/sgx_verify/mod.rs | 1 - pallets/teebag/src/tests.rs | 70 ++++--- pallets/teebag/src/types.rs | 35 ++-- runtime/rococo/src/lib.rs | 3 +- scripts/ts-utils/setup-enclave.ts | 28 +-- tee-worker/Cargo.lock | 23 ++- .../litentry/scheduled_enclave.rs | 27 ++- .../src/indirect_calls/mod.rs | 2 +- .../src/integritee/mod.rs | 33 ++- tee-worker/app-libs/sgx-runtime/src/lib.rs | 3 +- tee-worker/app-libs/stf/src/trusted_call.rs | 14 +- tee-worker/cli/lit_parentchain_nonce.sh | 2 +- .../cli/src/base_cli/commands/listen.rs | 73 +++---- tee-worker/cli/src/base_cli/mod.rs | 42 ++-- .../interfaces/sidechain/definitions.ts | 1 - .../core-primitives/enclave-api/Cargo.toml | 2 +- .../enclave-api/src/enclave_base.rs | 4 +- .../enclave-api/src/remote_attestation.rs | 4 +- .../node-api/api-client-extensions/src/lib.rs | 9 +- .../src/pallet_teebag.rs | 134 ++++++++++++ .../node-api/metadata/src/lib.rs | 17 +- .../node-api/metadata/src/metadata_mocks.rs | 125 ++++-------- .../node-api/metadata/src/pallet_teebag.rs | 71 +++++++ .../sgx-runtime-primitives/Cargo.toml | 5 - .../sgx-runtime-primitives/src/types.rs | 20 -- tee-worker/core-primitives/test/Cargo.toml | 4 +- .../test/src/mock/onchain_mock.rs | 43 ++-- tee-worker/core-primitives/types/src/lib.rs | 28 +-- .../indirect-calls-executor/src/executor.rs | 72 +------ .../indirect-calls-executor/src/mock.rs | 6 +- tee-worker/core/rpc-client/Cargo.toml | 1 - .../core/rpc-client/src/direct_client.rs | 6 +- tee-worker/core/rpc-client/src/mock.rs | 2 +- tee-worker/enclave-runtime/Cargo.lock | 175 ++++++++++++---- tee-worker/enclave-runtime/src/attestation.rs | 58 ++++-- .../src/rpc/worker_api_direct.rs | 19 +- .../src/test/fixtures/components.rs | 10 +- .../enclave-runtime/src/test/tests_main.rs | 3 +- .../src/test/top_pool_tests.rs | 31 ++- .../litentry/core/teebag-storage/Cargo.toml | 18 ++ .../litentry/core/teebag-storage/src/lib.rs | 48 +++++ tee-worker/litentry/primitives/Cargo.toml | 4 +- .../litentry/primitives/src/aes_request.rs | 2 +- tee-worker/litentry/primitives/src/lib.rs | 5 +- tee-worker/service/Cargo.toml | 2 - tee-worker/service/src/main_impl.rs | 140 +++++-------- tee-worker/service/src/sidechain_setup.rs | 4 +- tee-worker/service/src/sync_state.rs | 24 ++- tee-worker/service/src/tests/mock.rs | 47 ++--- .../src/tests/mocks/enclave_api_mock.rs | 3 +- tee-worker/service/src/worker.rs | 7 +- .../sidechain/consensus/aura/src/lib.rs | 19 +- .../aura/src/test/block_importer_tests.rs | 8 +- .../consensus/aura/src/test/fixtures/mod.rs | 2 +- .../sidechain/consensus/aura/src/verifier.rs | 3 +- .../src/block_import_confirmation_handler.rs | 4 +- .../sidechain/consensus/slots/src/lib.rs | 3 +- .../sidechain/consensus/slots/src/mocks.rs | 7 +- tee-worker/sidechain/peer-fetch/Cargo.toml | 1 + .../peer-fetch/src/untrusted_peer_fetch.rs | 8 +- .../sidechain/validateer-fetch/Cargo.toml | 7 +- .../validateer-fetch/src/validateer.rs | 56 ++---- .../common/utils/assertion.ts | 28 +-- .../integration-tests/common/utils/context.ts | 21 +- .../vc.issuer.attest.example.ts | 9 +- .../ts-tests/stress/src/litentry-api.ts | 19 +- .../ts-tests/worker/resuming_worker.test.ts | 2 +- ts-tests/common/setup/setup-enclave.ts | 10 +- ...nclave-check.ts => teebag-set-dev-mode.ts} | 16 +- 76 files changed, 1122 insertions(+), 937 deletions(-) create mode 100644 tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs create mode 100644 tee-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs create mode 100644 tee-worker/litentry/core/teebag-storage/Cargo.toml create mode 100644 tee-worker/litentry/core/teebag-storage/src/lib.rs rename ts-tests/common/setup/{skip-schedule-enclave-check.ts => teebag-set-dev-mode.ts} (68%) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 395331915f..9eb17c948f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -692,10 +692,7 @@ jobs: fail-fast: false matrix: include: - - test_name: demo-shielding-unshielding-multiworker - - test_name: demo-direct-call # Litentry - - test_name: lit-set-heartbeat-timeout - test_name: lit-ii-vc-test - test_name: lit-ii-identity-test - test_name: lit-di-substrate-identity-test diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 0402c9e279..6695ba9514 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -9035,6 +9035,32 @@ dependencies = [ "sp-std 5.0.0", ] +[[package]] +name = "pallet-teebag" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "chrono 0.4.26", + "der 0.6.1", + "frame-support", + "frame-system", + "hex 0.4.3", + "log 0.4.20", + "pallet-balances", + "pallet-timestamp", + "parity-scale-codec", + "ring 0.16.20", + "rustls-webpki", + "scale-info", + "serde 1.0.193", + "serde_json 1.0.103", + "sp-core", + "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "sp-runtime", + "sp-std 5.0.0", + "x509-cert", +] + [[package]] name = "pallet-teeracle" version = "0.1.0" @@ -10907,6 +10933,7 @@ dependencies = [ "pallet-session", "pallet-sidechain", "pallet-sudo", + "pallet-teebag", "pallet-teeracle", "pallet-teerex", "pallet-timestamp", @@ -11035,6 +11062,7 @@ dependencies = [ "pallet-group", "pallet-membership", "pallet-multisig", + "pallet-teebag", "pallet-teerex", "pallet-transaction-payment", "pallet-treasury", diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index ae5936ad0b..81ba58674d 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -32,7 +32,7 @@ dependencies = [ "derive_more", "either", "frame-metadata", - "hex", + "hex 0.4.3", "log", "parity-scale-codec", "scale-bits", @@ -248,6 +248,49 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bc-task-receiver" +version = "0.1.0" +dependencies = [ + "bc-task-sender", + "frame-support", + "futures 0.3.8", + "hex 0.4.0", + "ita-sgx-runtime", + "ita-stf", + "itp-enclave-metrics", + "itp-extrinsics-factory", + "itp-node-api", + "itp-ocall-api", + "itp-sgx-crypto", + "itp-sgx-externalities", + "itp-stf-executor", + "itp-stf-state-handler", + "itp-storage", + "itp-top-pool-author", + "itp-types", + "itp-utils", + "litentry-primitives", + "log", + "parity-scale-codec", + "sgx_tstd", + "sp-core", + "thiserror", + "threadpool", +] + +[[package]] +name = "bc-task-sender" +version = "0.1.0" +dependencies = [ + "futures 0.3.8", + "lazy_static", + "litentry-primitives", + "log", + "parity-scale-codec", + "sgx_tstd", +] + [[package]] name = "bech32" version = "0.10.0-beta" @@ -803,7 +846,7 @@ checksum = "7c24f403d068ad0b359e577a77f92392118be3f3c927538f2bb544a5ecd828c6" dependencies = [ "curve25519-dalek 3.2.0", "hashbrown 0.12.3", - "hex", + "hex 0.4.3", "rand_core 0.6.4", "sha2 0.9.9", "zeroize", @@ -838,12 +881,13 @@ name = "enclave-runtime" version = "0.0.1" dependencies = [ "array-bytes 6.1.0", + "bc-task-receiver", "cid", "derive_more", "env_logger", "frame-support", "frame-system", - "hex", + "hex 0.4.3", "ipfs-unixfs", "ita-parentchain-interface", "ita-sgx-runtime", @@ -1169,7 +1213,7 @@ name = "fp-account" version = "1.0.0-dev" source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" dependencies = [ - "hex", + "hex 0.4.3", "libsecp256k1", "log", "parity-scale-codec", @@ -1185,7 +1229,7 @@ name = "fp-account" version = "1.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "hex", + "hex 0.4.3", "libsecp256k1", "log", "parity-scale-codec", @@ -1629,6 +1673,14 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hex" +version = "0.4.0" +source = "git+https://github.com/mesalock-linux/rust-hex-sgx?tag=sgx_1.1.3#ee3266cd29b9f9c2eb69af9487f55c4f09c38f2b" +dependencies = [ + "sgx_tstd", +] + [[package]] name = "hex" version = "0.4.3" @@ -1850,7 +1902,7 @@ version = "0.9.0" dependencies = [ "frame-support", "frame-system", - "hex", + "hex 0.4.3", "hex-literal", "ita-sgx-runtime", "itp-hashing", @@ -2120,7 +2172,7 @@ dependencies = [ "base64 0.13.0 (git+https://github.com/mesalock-linux/rust-base64-sgx?rev=sgx_1.1.3)", "bit-vec", "chrono 0.4.11", - "hex", + "hex 0.4.3", "httparse", "itertools 0.10.5", "itp-ocall-api", @@ -2336,7 +2388,7 @@ dependencies = [ name = "itp-stf-executor" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "itc-parentchain-test", "itp-enclave-metrics", "itp-node-api", @@ -2448,7 +2500,7 @@ dependencies = [ name = "itp-test" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "itp-node-api", "itp-node-api-metadata-provider", "itp-ocall-api", @@ -2547,7 +2599,7 @@ dependencies = [ name = "itp-utils" version = "0.9.0" dependencies = [ - "hex", + "hex 0.4.3", "litentry-hex-utils", "parity-scale-codec", ] @@ -2658,7 +2710,7 @@ name = "its-consensus-slots" version = "0.9.0" dependencies = [ "derive_more", - "hex", + "hex 0.4.3", "itp-settings", "itp-sgx-externalities", "itp-stf-state-handler", @@ -2694,6 +2746,7 @@ dependencies = [ name = "its-rpc-handler" version = "0.9.0" dependencies = [ + "bc-task-sender", "futures 0.3.8", "itp-rpc", "itp-stf-primitives", @@ -2891,7 +2944,7 @@ dependencies = [ name = "litentry-hex-utils" version = "0.9.12" dependencies = [ - "hex", + "hex 0.4.3", ] [[package]] @@ -2904,7 +2957,7 @@ version = "0.1.0" dependencies = [ "bitcoin", "core-primitives", - "hex", + "hex 0.4.3", "itp-sgx-crypto", "itp-utils", "litentry-hex-utils", @@ -3202,7 +3255,7 @@ dependencies = [ "fp-evm 3.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "frame-support", "frame-system", - "hex", + "hex 0.4.3", "impl-trait-for-tuples", "log", "parity-scale-codec", @@ -3224,7 +3277,7 @@ dependencies = [ "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", "frame-support", "frame-system", - "hex", + "hex 0.4.3", "hex-literal", "impl-trait-for-tuples", "log", @@ -4742,7 +4795,7 @@ dependencies = [ "async-trait", "derive_more", "frame-metadata", - "hex", + "hex 0.4.3", "log", "maybe-async", "parity-scale-codec", @@ -4879,6 +4932,14 @@ dependencies = [ "sgx_tstd", ] +[[package]] +name = "threadpool" +version = "1.8.0" +source = "git+https://github.com/mesalock-linux/rust-threadpool-sgx?tag=sgx_1.1.3#098d98a85b7e2b02e2bb451a3dec0b027017ff4c" +dependencies = [ + "sgx_tstd", +] + [[package]] name = "tiny-keccak" version = "2.0.2" @@ -5043,7 +5104,7 @@ checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" dependencies = [ "byteorder 1.4.3", "crunchy", - "hex", + "hex 0.4.3", "static_assertions", ] diff --git a/node/src/chain_specs/rococo.rs b/node/src/chain_specs/rococo.rs index 0e6742ce9d..973640a429 100644 --- a/node/src/chain_specs/rococo.rs +++ b/node/src/chain_specs/rococo.rs @@ -19,8 +19,8 @@ use cumulus_primitives_core::ParaId; use rococo_parachain_runtime::{ AccountId, AuraId, Balance, BalancesConfig, CouncilMembershipConfig, GenesisConfig, ParachainInfoConfig, ParachainStakingConfig, PolkadotXcmConfig, SessionConfig, SudoConfig, - SystemConfig, TechnicalCommitteeMembershipConfig, TeebagConfig, TeerexConfig, - VCManagementConfig, UNIT, WASM_BINARY, + SystemConfig, TechnicalCommitteeMembershipConfig, TeebagConfig, TeebagOperationalMode, + TeerexConfig, VCManagementConfig, UNIT, WASM_BINARY, }; use sc_service::ChainType; use sc_telemetry::TelemetryEndpoints; @@ -253,7 +253,7 @@ fn generate_genesis( teebag: TeebagConfig { allow_sgx_debug_mode: true, admin: Some(root_key), - mode: Default::default(), + mode: TeebagOperationalMode::Development, }, } } diff --git a/pallets/teebag/src/lib.rs b/pallets/teebag/src/lib.rs index 577ee3f9a0..ecdc96deaa 100644 --- a/pallets/teebag/src/lib.rs +++ b/pallets/teebag/src/lib.rs @@ -30,9 +30,10 @@ use sp_runtime::traits::{CheckedSub, SaturatedConversion}; use sp_std::{prelude::*, str}; mod sgx_verify; -use sgx_verify::{ - deserialize_enclave_identity, deserialize_tcb_info, extract_certs, verify_certificate_chain, - verify_dcap_quote, verify_ias_report, SgxReport, +pub use sgx_verify::{ + deserialize_enclave_identity, deserialize_tcb_info, extract_certs, + extract_tcb_info_from_raw_dcap_quote, verify_certificate_chain, verify_dcap_quote, + verify_ias_report, SgxReport, }; pub use pallet::*; @@ -64,6 +65,10 @@ pub mod pallet { type MomentsPerDay: Get; /// The origin who can set the admin account type SetAdminOrigin: EnsureOrigin; + /// Maximum number of enclave identifiers allowed to be registered for a specific + /// `worker_type` + #[pallet::constant] + type MaxEnclaveIdentifier: Get; } // TODO: maybe add more sidechain lifecycle events @@ -76,10 +81,6 @@ pub mod pallet { AdminSet { new_admin: Option, }, - MaxEnclaveCountSet { - worker_type: WorkerType, - new_count: u64, - }, EnclaveAdded { who: T::AccountId, worker_type: WorkerType, @@ -130,6 +131,12 @@ pub mod pallet { InvalidSgxMode, /// The enclave doesn't exist. EnclaveNotExist, + /// The enclave identifier doesn't exist. + EnclaveIdentifierNotExist, + /// The enclave identifier already exists. + EnclaveIdentifierAlreadyExist, + /// when we try to re-register an existing enclave with a differnet worker type + WorkerTypeNotAllowed, /// The shard doesn't match the enclave. WrongMrenclaveForShard, /// The worker url is too long. @@ -143,16 +150,10 @@ pub mod pallet { ScheduledEnclaveNotExist, /// Enclave not in the scheduled list, therefore unexpected. EnclaveNotInSchedule, - /// The provided collateral data is invalid + /// The provided collateral data is invalid. CollateralInvalid, - /// The number of `extra_topics` passed to `publish_hash` exceeds the limit. - TooManyTopics, - /// The length of the `data` passed to `publish_hash` exceeds the limit. - DataTooLong, - /// max_enclave_count overflows - MaxEnclaveCountOverflow, - /// max_enclave_count underflows (than 0 or currently registered enclave count) - MaxEnclaveCountUnderflow, + /// MaxEnclaveIdentifier overflow. + MaxEnclaveIdentifierOverflow, /// A proposed block is unexpected. ReceivedUnexpectedSidechainBlock, /// The value for the next finalization candidate is invalid. @@ -167,25 +168,22 @@ pub mod pallet { #[pallet::getter(fn mode)] pub type Mode = StorageValue<_, OperationalMode, ValueQuery>; - #[pallet::type_value] - pub fn DefaultMaxEnclaveCount() -> u64 { - 3 - } - + // records the enclave identifier list for each worker_type + // T::AccountId is used as identifier as it's unique for each enclave #[pallet::storage] - #[pallet::getter(fn max_enclave_count)] - pub type MaxEnclaveCount = - StorageMap<_, Blake2_128Concat, WorkerType, u64, ValueQuery, DefaultMaxEnclaveCount>; - - #[pallet::storage] - #[pallet::getter(fn enclave_count)] - pub type EnclaveCount = StorageMap<_, Blake2_128Concat, WorkerType, u64, ValueQuery>; + #[pallet::getter(fn enclave_identifier)] + pub type EnclaveIdentifier = StorageMap< + _, + Blake2_128Concat, + WorkerType, + BoundedVec, + ValueQuery, + >; // registry that holds all registered enclaves, using T::AccountId as the key // having `worker_type` and `mrenclave` in each `Enclave` instance might seem a bit redundant, // but it increases flexibility where we **could** allow the same type of worker to have - // different mrenclaves - e.g. when more than one version of an enclave is permitted in TEE-node - // cluster. + // distinct mrenclaves: e.g. when multiple versions of enclave are permitted in TEE cluster. // // It simplifies the lookup a bit too, otherwise we might need several storages. #[pallet::storage] @@ -225,11 +223,6 @@ pub mod pallet { pub type ScheduledEnclave = StorageMap<_, Blake2_128Concat, (WorkerType, SidechainBlockNumber), MrEnclave>; - #[pallet::storage] - #[pallet::getter(fn worker_for_shard)] - pub type WorkerForShard = - StorageMap<_, Blake2_128Concat, ShardIdentifier, T::AccountId, OptionQuery>; - #[pallet::storage] #[pallet::getter(fn latest_sidechain_block_confirmation)] pub type LatestSidechainBlockConfirmation = @@ -304,24 +297,6 @@ pub mod pallet { #[pallet::call_index(2)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] - pub fn set_max_enclave_count( - origin: OriginFor, - worker_type: WorkerType, - new_count: u64, - ) -> DispatchResultWithPostInfo { - Self::ensure_admin_or_root(origin)?; - ensure!( - new_count > Self::max_enclave_count(worker_type), - Error::::MaxEnclaveCountUnderflow - ); - - MaxEnclaveCount::::insert(worker_type, new_count); - Self::deposit_event(Event::MaxEnclaveCountSet { worker_type, new_count }); - Ok(Pays::No.into()) - } - - #[pallet::call_index(3)] - #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn force_add_enclave( origin: OriginFor, who: T::AccountId, @@ -332,7 +307,7 @@ pub mod pallet { Ok(Pays::No.into()) } - #[pallet::call_index(4)] + #[pallet::call_index(3)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn force_remove_enclave( origin: OriginFor, @@ -343,7 +318,7 @@ pub mod pallet { Ok(Pays::No.into()) } - #[pallet::call_index(5)] + #[pallet::call_index(4)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn force_remove_enclave_by_mrenclave( origin: OriginFor, @@ -368,7 +343,7 @@ pub mod pallet { Ok(Pays::No.into()) } - #[pallet::call_index(6)] + #[pallet::call_index(5)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn force_remove_enclave_by_worker_type( origin: OriginFor, @@ -394,7 +369,7 @@ pub mod pallet { Ok(Pays::No.into()) } - #[pallet::call_index(7)] + #[pallet::call_index(6)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn set_scheduled_enclave( origin: OriginFor, @@ -412,7 +387,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(8)] + #[pallet::call_index(7)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn remove_scheduled_enclave( origin: OriginFor, @@ -432,7 +407,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(9)] + #[pallet::call_index(8)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::Yes))] pub fn register_enclave( origin: OriginFor, @@ -447,13 +422,12 @@ pub mod pallet { ensure!(worker_url.len() <= MAX_URL_LEN, Error::::EnclaveUrlTooLong); - let mut enclave = Enclave::new( - worker_type, - worker_url, - shielding_pubkey, - vc_pubkey, - attestation_type, - ); + let mut enclave = Enclave::new(worker_type) + .with_url(worker_url) + .with_shielding_pubkey(shielding_pubkey) + .with_vc_pubkey(vc_pubkey) + .with_attestation_type(attestation_type); + match attestation_type { AttestationType::Ignore => { ensure!( @@ -493,12 +467,11 @@ pub mod pallet { }, OperationalMode::Development => (), }; - enclave.register_timestamp = Self::now().saturated_into(); Self::add_enclave(&sender, &enclave)?; Ok(().into()) } - #[pallet::call_index(10)] + #[pallet::call_index(9)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::Yes))] pub fn unregister_enclave(origin: OriginFor) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; @@ -506,7 +479,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(11)] + #[pallet::call_index(10)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn register_quoting_enclave( origin: OriginFor, @@ -522,7 +495,7 @@ pub mod pallet { Ok(().into()) } - #[pallet::call_index(12)] + #[pallet::call_index(11)] #[pallet::weight((195_000_000, DispatchClass::Normal, Pays::No))] pub fn register_tcb_info( origin: OriginFor, @@ -596,13 +569,15 @@ pub mod pallet { sender_enclave.last_seen_timestamp = Self::now().saturated_into(); // Simple logic for now: only accept blocks from first registered enclave. - let primary_enclave = Self::primary_enclave(sender_enclave.worker_type) - .ok_or(Error::::EnclaveNotExist)?; - - if sender_enclave.register_timestamp > primary_enclave.register_timestamp { + let primary_enclave_identifier = + EnclaveIdentifier::::get(sender_enclave.worker_type) + .get(0) + .cloned() + .ok_or(Error::::EnclaveIdentifierNotExist)?; + if sender != primary_enclave_identifier { log::debug!( - "Ignore block confirmation from registered enclave with timestamp {}", - sender_enclave.register_timestamp + "Ignore block confirmation from non primary enclave identifier: {:?}, primary: {:?}", + sender, primary_enclave_identifier ); return Ok(().into()) } @@ -640,40 +615,20 @@ impl Pallet { Ok(().into()) } - fn increment_count(worker_type: WorkerType) -> Result<(), DispatchErrorWithPostInfo> { - let count = Self::enclave_count(worker_type); - ensure!(count < Self::max_enclave_count(worker_type), Error::::MaxEnclaveCountOverflow); - - EnclaveCount::::insert( - worker_type, - count.checked_add(1u64).ok_or(Error::::MaxEnclaveCountOverflow)?, - ); - - Ok(()) + fn add_enclave_identifier(worker_type: WorkerType, who: &T::AccountId) -> Result<(), Error> { + EnclaveIdentifier::::try_mutate(worker_type, |v| { + ensure!(!v.contains(who), Error::::EnclaveIdentifierAlreadyExist); + v.try_push(who.clone()).map_err(|_| Error::::MaxEnclaveIdentifierOverflow) + }) } - fn decrement_count(worker_type: WorkerType) -> Result<(), DispatchErrorWithPostInfo> { - let count = Self::enclave_count(worker_type); - EnclaveCount::::insert( - worker_type, - count.checked_sub(1u64).ok_or(Error::::MaxEnclaveCountUnderflow)?, - ); - - Ok(()) - } - - pub fn add_enclave(sender: &T::AccountId, enclave: &Enclave) -> DispatchResultWithPostInfo { + pub fn add_enclave(sender: &T::AccountId, enclave: &Enclave) -> Result<(), Error> { match EnclaveRegistry::::get(sender) { - Some(old_enclave) => { - if old_enclave.worker_type != enclave.worker_type { - // a tricky situation - we are re-registering the enclave with a different - // worker type - Self::decrement_count(old_enclave.worker_type)?; - Self::increment_count(enclave.worker_type)?; - } - // else - do nothing - }, - None => Self::increment_count(enclave.worker_type)?, + Some(old_enclave) => ensure!( + old_enclave.worker_type == enclave.worker_type, + Error::::WorkerTypeNotAllowed + ), + None => Self::add_enclave_identifier(enclave.worker_type, sender)?, }; EnclaveRegistry::::insert(sender, enclave); Self::deposit_event(Event::::EnclaveAdded { @@ -681,29 +636,25 @@ impl Pallet { worker_type: enclave.worker_type, url: enclave.url.clone(), }); - Ok(().into()) + Ok(()) } fn remove_enclave(sender: &T::AccountId) -> DispatchResultWithPostInfo { let enclave = EnclaveRegistry::::get(sender).ok_or(Error::::EnclaveNotExist)?; - let count = EnclaveCount::::get(enclave.worker_type); - EnclaveCount::::insert( - enclave.worker_type, - count.checked_sub(1u64).ok_or(Error::::MaxEnclaveCountOverflow)?, - ); + EnclaveIdentifier::::try_mutate(enclave.worker_type, |v| { + ensure!(v.contains(sender), Error::::EnclaveIdentifierNotExist); + v.retain(|e| e != sender); + Ok::<(), DispatchErrorWithPostInfo>(()) + })?; EnclaveRegistry::::remove(sender); Self::deposit_event(Event::::EnclaveRemoved { who: sender.clone() }); Ok(().into()) } - pub fn primary_enclave(worker_type: WorkerType) -> Option { - let mut enclaves = EnclaveRegistry::::iter_values() - .filter(|e| e.worker_type == worker_type) - .collect::>(); - enclaves.sort_by(|a, b| Ord::cmp(&a.register_timestamp, &b.register_timestamp)); - enclaves.get(0).cloned() + pub fn enclave_count(worker_type: WorkerType) -> u32 { + EnclaveIdentifier::::get(worker_type).iter().count() as u32 } fn verify_ias( @@ -810,7 +761,6 @@ impl Pallet { confirmation: SidechainBlockConfirmation, ) { LatestSidechainBlockConfirmation::::insert(shard, confirmation); - WorkerForShard::::insert(shard, sender.clone()); Self::deposit_event(Event::SidechainBlockFinalized { who: sender, sidechain_block_number: confirmation.block_number, diff --git a/pallets/teebag/src/mock.rs b/pallets/teebag/src/mock.rs index bcaac7e06f..945948cdda 100644 --- a/pallets/teebag/src/mock.rs +++ b/pallets/teebag/src/mock.rs @@ -24,7 +24,7 @@ use frame_support::{ }; use frame_system as system; use frame_system::EnsureRoot; -use sp_core::H256; +use sp_core::{ConstU32, H256}; use sp_keyring::AccountKeyring; use sp_runtime::{ generic, @@ -136,6 +136,7 @@ impl pallet_teebag::Config for Test { type RuntimeEvent = RuntimeEvent; type MomentsPerDay = MomentsPerDay; type SetAdminOrigin = EnsureRoot; + type MaxEnclaveIdentifier = ConstU32<1>; } // This function basically just builds a genesis storage key/value store according to diff --git a/pallets/teebag/src/sgx_verify/collateral.rs b/pallets/teebag/src/sgx_verify/collateral.rs index 76f9ad5e0c..53f3c55af4 100644 --- a/pallets/teebag/src/sgx_verify/collateral.rs +++ b/pallets/teebag/src/sgx_verify/collateral.rs @@ -15,7 +15,6 @@ */ -#![cfg_attr(not(feature = "std"), no_std)] pub extern crate alloc; use crate::{Fmspc, MrSigner, Pcesvn, QeTcb, QuotingEnclave, TcbInfoOnChain, TcbVersionStatus}; diff --git a/pallets/teebag/src/sgx_verify/mod.rs b/pallets/teebag/src/sgx_verify/mod.rs index 32b0ff6af8..9572fd879a 100644 --- a/pallets/teebag/src/sgx_verify/mod.rs +++ b/pallets/teebag/src/sgx_verify/mod.rs @@ -28,7 +28,6 @@ //! //! * https://download.01.org/intel-sgx/linux-1.5/docs/Intel_SGX_Developer_Guide.pdf -#![cfg_attr(not(feature = "std"), no_std)] pub extern crate alloc; use self::{ diff --git a/pallets/teebag/src/tests.rs b/pallets/teebag/src/tests.rs index 937cf27c8f..4930c9d2fd 100644 --- a/pallets/teebag/src/tests.rs +++ b/pallets/teebag/src/tests.rs @@ -19,7 +19,7 @@ #![allow(dead_code, unused_imports)] use crate::{ mock::*, AttestationType, DcapProvider, Enclave, EnclaveRegistry, Error, Event as TeebagEvent, - MaxEnclaveCount, ScheduledEnclave, SgxBuildMode, WorkerType, H256, + ScheduledEnclave, SgxBuildMode, WorkerType, H256, }; use frame_support::{assert_noop, assert_ok}; use hex_literal::hex; @@ -29,8 +29,12 @@ use test_utils::{get_signer, ias::consts::*}; const VALID_TIMESTAMP: Moment = 1671606747000; +fn alice() -> AccountId32 { + AccountKeyring::Alice.to_account_id() +} + fn default_enclave() -> Enclave { - Enclave::default() + Enclave::new(WorkerType::Identity) .with_attestation_type(AttestationType::Ignore) .with_url(URL.to_vec()) .with_last_seen_timestamp(pallet_timestamp::Pallet::::now()) @@ -84,7 +88,7 @@ fn register_enclave_dev_works_with_no_scheduled_enclave() { new_test_ext(true).execute_with(|| { // it works with no entry in scheduled_enclave assert_ok!(Teebag::register_enclave( - RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + RuntimeOrigin::signed(alice()), Default::default(), TEST4_MRENCLAVE.to_vec(), URL.to_vec(), @@ -97,10 +101,7 @@ fn register_enclave_dev_works_with_no_scheduled_enclave() { assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); - assert_eq!( - EnclaveRegistry::::get(AccountKeyring::Alice.to_account_id()).unwrap(), - enclave - ); + assert_eq!(EnclaveRegistry::::get(alice()).unwrap(), enclave); }) } @@ -123,7 +124,6 @@ fn register_enclave_dev_works_with_sgx_build_mode_debug() { let enclave = default_enclave() .with_mrenclave(TEST4_MRENCLAVE) .with_last_seen_timestamp(TEST4_TIMESTAMP) - .with_register_timestamp(TEST4_TIMESTAMP) .with_sgx_build_mode(SgxBuildMode::Debug) .with_attestation_type(AttestationType::Ias); @@ -216,7 +216,7 @@ fn register_dcap_enclave_works() { fn register_enclave_prod_works_with_sgx_build_mode_debug() { new_test_ext(false).execute_with(|| { assert_ok!(Teebag::set_scheduled_enclave( - RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + RuntimeOrigin::signed(alice()), WorkerType::Identity, 0, TEST4_MRENCLAVE @@ -237,7 +237,6 @@ fn register_enclave_prod_works_with_sgx_build_mode_debug() { let enclave = default_enclave() .with_mrenclave(TEST4_MRENCLAVE) .with_last_seen_timestamp(TEST4_TIMESTAMP) - .with_register_timestamp(TEST4_TIMESTAMP) .with_sgx_build_mode(SgxBuildMode::Debug) .with_attestation_type(AttestationType::Ias); @@ -251,7 +250,7 @@ fn register_enclave_prod_works_with_sgx_build_mode_debug() { fn register_enclave_prod_works_with_sgx_build_mode_production() { new_test_ext(false).execute_with(|| { assert_ok!(Teebag::set_scheduled_enclave( - RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + RuntimeOrigin::signed(alice()), WorkerType::Identity, 0, TEST8_MRENCLAVE @@ -272,7 +271,6 @@ fn register_enclave_prod_works_with_sgx_build_mode_production() { let enclave = default_enclave() .with_mrenclave(TEST8_MRENCLAVE) .with_last_seen_timestamp(TEST8_TIMESTAMP) - .with_register_timestamp(TEST8_TIMESTAMP) .with_sgx_build_mode(SgxBuildMode::Production) .with_attestation_type(AttestationType::Ias); @@ -287,7 +285,7 @@ fn register_enclave_prod_fails_with_wrong_attestation_type() { new_test_ext(false).execute_with(|| { assert_noop!( Teebag::register_enclave( - RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), + RuntimeOrigin::signed(alice()), Default::default(), TEST4_MRENCLAVE.to_vec(), URL.to_vec(), @@ -323,7 +321,6 @@ fn register_enclave_prod_fails_with_no_scheduled_enclave() { #[test] fn register_enclave_prod_fails_with_max_limit_reached() { new_test_ext(false).execute_with(|| { - MaxEnclaveCount::::insert(WorkerType::BitAcross, 1u64); ScheduledEnclave::::insert((WorkerType::BitAcross, 0), TEST5_MRENCLAVE); ScheduledEnclave::::insert((WorkerType::BitAcross, 1), TEST6_MRENCLAVE); ScheduledEnclave::::insert((WorkerType::Identity, 0), TEST5_MRENCLAVE); @@ -353,11 +350,27 @@ fn register_enclave_prod_fails_with_max_limit_reached() { None, AttestationType::Ias, ), - Error::::MaxEnclaveCountOverflow + Error::::MaxEnclaveIdentifierOverflow ); - // re-register them as WorkerType::Identity should work though + // re-register them as WorkerType::Identity is not allowed Timestamp::set_timestamp(TEST5_TIMESTAMP); + assert_noop!( + Teebag::register_enclave( + RuntimeOrigin::signed(signer5.clone()), + WorkerType::Identity, + TEST5_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + ), + Error::::WorkerTypeNotAllowed + ); + + // remove and re-register it should work + assert_ok!(Teebag::force_remove_enclave(RuntimeOrigin::signed(alice()), signer5.clone(),)); + assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer5), WorkerType::Identity, @@ -369,20 +382,21 @@ fn register_enclave_prod_fails_with_max_limit_reached() { )); Timestamp::set_timestamp(TEST6_TIMESTAMP); - assert_ok!(Teebag::register_enclave( - RuntimeOrigin::signed(signer6), - WorkerType::Identity, - TEST6_CERT.to_vec(), - URL.to_vec(), - None, - None, - AttestationType::Ias, - )); + assert_noop!( + Teebag::register_enclave( + RuntimeOrigin::signed(signer6), + WorkerType::Identity, + TEST6_CERT.to_vec(), + URL.to_vec(), + None, + None, + AttestationType::Ias, + ), + Error::::MaxEnclaveIdentifierOverflow + ); - assert_eq!(Teebag::enclave_count(WorkerType::Identity), 2); + assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); - assert_eq!(Teebag::max_enclave_count(WorkerType::Identity), 3); - assert_eq!(Teebag::max_enclave_count(WorkerType::BitAcross), 1); }) } diff --git a/pallets/teebag/src/types.rs b/pallets/teebag/src/types.rs index a8f1bc6b50..c54421b546 100644 --- a/pallets/teebag/src/types.rs +++ b/pallets/teebag/src/types.rs @@ -96,12 +96,11 @@ pub struct SidechainBlockConfirmation { pub block_header_hash: H256, } -#[derive(Encode, Decode, Default, Clone, PartialEq, Eq, RuntimeDebug, TypeInfo)] +#[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub struct Enclave { pub worker_type: WorkerType, pub mrenclave: MrEnclave, pub last_seen_timestamp: u64, // unix epoch in milliseconds when it's last seen - pub register_timestamp: u64, // unix epoch in milliseconds when it's registered pub url: Vec, // utf8 encoded url pub shielding_pubkey: Option>, // JSON serialised enclave shielding pub key pub vc_pubkey: Option>, @@ -110,21 +109,8 @@ pub struct Enclave { } impl Enclave { - pub fn new( - worker_type: WorkerType, - url: Vec, - shielding_pubkey: Option>, - vc_pubkey: Option>, - attestation_type: AttestationType, - ) -> Self { - Enclave { - worker_type, - url, - shielding_pubkey, - vc_pubkey, - attestation_type, - ..Default::default() - } + pub fn new(worker_type: WorkerType) -> Self { + Enclave { worker_type, ..Default::default() } } pub fn with_mrenclave(mut self, mrenclave: MrEnclave) -> Self { @@ -137,6 +123,16 @@ impl Enclave { self } + pub fn with_shielding_pubkey(mut self, shielding_pubkey: Option>) -> Self { + self.shielding_pubkey = shielding_pubkey; + self + } + + pub fn with_vc_pubkey(mut self, vc_pubkey: Option>) -> Self { + self.vc_pubkey = vc_pubkey; + self + } + pub fn with_last_seen_timestamp(mut self, t: u64) -> Self { self.last_seen_timestamp = t; self @@ -151,11 +147,6 @@ impl Enclave { self.sgx_build_mode = sgx_build_mode; self } - - pub fn with_register_timestamp(mut self, t: u64) -> Self { - self.register_timestamp = t; - self - } } // use the name `RsaRequest` to differentiate from `AesRequest` (see aes_request.rs in diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 627a32d369..999043c0e9 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -41,7 +41,7 @@ use runtime_common::EnsureEnclaveSigner; // for TEE pub use pallet_balances::Call as BalancesCall; pub use pallet_sidechain; -pub use pallet_teebag; +pub use pallet_teebag::{self, OperationalMode as TeebagOperationalMode}; pub use pallet_teeracle; pub use pallet_teerex; @@ -1029,6 +1029,7 @@ impl pallet_teebag::Config for Runtime { type RuntimeEvent = RuntimeEvent; type MomentsPerDay = MomentsPerDay; type SetAdminOrigin = EnsureRootOrHalfCouncil; + type MaxEnclaveIdentifier = ConstU32<3>; } impl pallet_identity_management::Config for Runtime { diff --git a/scripts/ts-utils/setup-enclave.ts b/scripts/ts-utils/setup-enclave.ts index f4e989c358..829ce280a7 100644 --- a/scripts/ts-utils/setup-enclave.ts +++ b/scripts/ts-utils/setup-enclave.ts @@ -31,11 +31,11 @@ async function transfer(api: any, Alice: any) { }); }); } -async function setTeerexAdmin(api: any, Alice: any) { +async function teebagSetAdmin(api: any, Alice: any) { return new Promise(async (resolve, reject) => { // Note: The hardcoded address is that of Alice on dev chain await api.tx.sudo - .sudo(api.tx.teerex.setAdmin("esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5")) + .sudo(api.tx.teebag.setAdmin("esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5")) .signAndSend(Alice, ({ status, events, dispatchError }) => { if (status.isInBlock || status.isFinalized) { if (dispatchError) { @@ -47,23 +47,23 @@ async function setTeerexAdmin(api: any, Alice: any) { const { docs, name, section } = decoded; console.log(colors.red(`${section}.${name}: ${docs.join(" ")}`)); - reject("updateScheduledEnclave failed"); + reject("teebag.setAdmin failed"); } else { // Other, CannotLookup, BadOrigin, no extra info console.log(dispatchError.toString()); - reject("updateScheduledEnclave failed"); + reject("teebag.setAdmin failed"); } } else { - console.log(colors.green("updateScheduledEnclave completed")); - resolve("updateScheduledEnclave done"); + console.log(colors.green("teebag.setAdmin completed")); + resolve("teebag.setAdmin done"); } } }); }); } -async function updateScheduledEnclave(api: any, Alice: any, block: any) { +async function teebagSetScheduledEnclave(api: any, Alice: any, block: any) { return new Promise(async (resolve, reject) => { - await api.tx.teerex.updateScheduledEnclave(block, hexToU8a(`0x${mrenclave}`)) + await api.tx.teebag.setScheduledEnclave("Identity", block, hexToU8a(`0x${mrenclave}`)) .signAndSend(Alice, ({ status, events, dispatchError }) => { if (status.isInBlock || status.isFinalized) { if (dispatchError) { @@ -75,15 +75,15 @@ async function updateScheduledEnclave(api: any, Alice: any, block: any) { const { docs, name, section } = decoded; console.log(colors.red(`${section}.${name}: ${docs.join(" ")}`)); - reject("updateScheduledEnclave failed"); + reject("teebag.setScheduledEnclave failed"); } else { // Other, CannotLookup, BadOrigin, no extra info console.log(dispatchError.toString()); - reject("updateScheduledEnclave failed"); + reject("teebag.setScheduledEnclave failed"); } } else { - console.log(colors.green("updateScheduledEnclave completed")); - resolve("updateScheduledEnclave done"); + console.log(colors.green("teebag.setScheduledEnclave completed")); + resolve("teebag.setScheduledEnclave done"); } } }); @@ -100,8 +100,8 @@ async function main() { const Alice = keyring.addFromUri("//Alice", { name: "Alice default" }); await transfer(defaultAPI, Alice); - await setTeerexAdmin(defaultAPI, Alice); - await updateScheduledEnclave(defaultAPI, Alice, block); + await teebagSetAdmin(defaultAPI, Alice); + await teebagSetScheduledEnclave(defaultAPI, Alice, block); console.log(colors.green("done")); process.exit(); diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index b3a43d3533..fdd91b00d3 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -4829,7 +4829,6 @@ dependencies = [ "serde_json 1.0.103", "sgx_crypto_helper", "sp-core", - "teerex-primitives", "thiserror 1.0.44", "url 2.4.0", "ws", @@ -4998,6 +4997,7 @@ dependencies = [ "itp-storage", "itp-types", "log 0.4.20", + "pallet-teebag", "parity-scale-codec", "serde_json 1.0.103", "sgx_crypto_helper", @@ -5005,7 +5005,6 @@ dependencies = [ "sgx_urts", "sp-core", "sp-runtime", - "teerex-primitives", "thiserror 1.0.44", ] @@ -5206,7 +5205,6 @@ name = "itp-sgx-runtime-primitives" version = "0.9.0" dependencies = [ "frame-system", - "litentry-primitives", "pallet-balances", "sp-core", "sp-runtime", @@ -5354,10 +5352,10 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-teerex-storage", "itp-time-utils", "itp-types", "jsonrpc-core 18.0.0 (git+https://github.com/scs/jsonrpc?branch=no_std_v18)", + "lc-teebag-storage", "litentry-primitives", "log 0.4.20", "parity-scale-codec", @@ -5618,6 +5616,7 @@ dependencies = [ "itc-rpc-client", "itp-node-api", "itp-test", + "itp-types", "its-primitives", "its-rpc-handler", "its-storage", @@ -5734,12 +5733,11 @@ name = "its-validateer-fetch" version = "0.9.0" dependencies = [ "derive_more", - "frame-support", "itc-parentchain-test", "itp-ocall-api", - "itp-teerex-storage", "itp-test", "itp-types", + "lc-teebag-storage", "parity-scale-codec", "sp-core", "sp-runtime", @@ -6405,6 +6403,15 @@ dependencies = [ "url 2.4.0", ] +[[package]] +name = "lc-teebag-storage" +version = "0.1.0" +dependencies = [ + "itp-storage", + "itp-types", + "sp-std 5.0.0", +] + [[package]] name = "lc-vc-task-receiver" version = "0.1.0" @@ -7151,6 +7158,7 @@ dependencies = [ "litentry-hex-utils", "log 0.4.20", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "pallet-teebag", "parity-scale-codec", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (git+https://github.com/mesalock-linux/rand-sgx?tag=sgx_1.1.3)", @@ -7165,7 +7173,6 @@ dependencies = [ "sp-std 5.0.0", "strum 0.26.1", "strum_macros 0.26.1", - "teerex-primitives", ] [[package]] @@ -7235,7 +7242,6 @@ dependencies = [ "serde 1.0.193", "serde_derive 1.0.193", "serde_json 1.0.103", - "sgx-verify", "sgx_crypto_helper", "sgx_types", "sp-consensus-grandpa", @@ -7243,7 +7249,6 @@ dependencies = [ "sp-keyring", "sp-runtime", "substrate-api-client", - "teerex-primitives", "thiserror 1.0.44", "tokio", "warp", diff --git a/tee-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs b/tee-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs index f2ae695c9e..5e6069b1a9 100644 --- a/tee-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs +++ b/tee-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs @@ -21,29 +21,35 @@ use itc_parentchain_indirect_calls_executor::{ IndirectDispatch, }; use itp_stf_primitives::traits::IndirectExecutor; -use itp_types::{MrEnclave, SidechainBlockNumber}; +use itp_types::{MrEnclave, SidechainBlockNumber, WorkerType}; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; -use log::debug; +use log::*; #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct UpdateScheduledEnclaveArgs { +pub struct SetScheduledEnclaveArgs { + worker_type: WorkerType, sbn: codec::Compact, mrenclave: MrEnclave, } impl> - IndirectDispatch for UpdateScheduledEnclaveArgs + IndirectDispatch for SetScheduledEnclaveArgs { type Args = (); fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { - debug!("execute indirect call: UpdateScheduledEnclave, sidechain_block_number: {:?}, mrenclave: {:?}", self.sbn, self.mrenclave); - GLOBAL_SCHEDULED_ENCLAVE.update(self.sbn.into(), self.mrenclave)?; + debug!("execute indirect call: SetScheduledEnclave, worker_type: {:?}, sidechain_block_number: {:?}, mrenclave: {:?}", self.worker_type, self.sbn, self.mrenclave); + if self.worker_type == WorkerType::Identity { + GLOBAL_SCHEDULED_ENCLAVE.update(self.sbn.into(), self.mrenclave)?; + } else { + warn!("Ignore SetScheduledEnclave due to wrong worker_type"); + } Ok(()) } } #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub struct RemoveScheduledEnclaveArgs { + worker_type: WorkerType, sbn: codec::Compact, } @@ -53,10 +59,15 @@ impl> type Args = (); fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { debug!( - "execute indirect call: RemoveScheduledEnclave, sidechain_block_number: {:?}", + "execute indirect call: RemoveScheduledEnclave, worker_type: {:?}, sidechain_block_number: {:?}", + self.worker_type, self.sbn ); - GLOBAL_SCHEDULED_ENCLAVE.remove(self.sbn.into())?; + if self.worker_type == WorkerType::Identity { + GLOBAL_SCHEDULED_ENCLAVE.remove(self.sbn.into())?; + } else { + warn!("Ignore RemoveScheduledEnclave due to wrong worker_type"); + } Ok(()) } } diff --git a/tee-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs b/tee-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs index 7b65f61b30..d14caeeb20 100644 --- a/tee-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs +++ b/tee-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs @@ -26,7 +26,7 @@ pub use litentry::{ deactivate_identity::DeactivateIdentityArgs, link_identity::LinkIdentityArgs, request_vc::RequestVCArgs, - scheduled_enclave::{RemoveScheduledEnclaveArgs, UpdateScheduledEnclaveArgs}, + scheduled_enclave::{RemoveScheduledEnclaveArgs, SetScheduledEnclaveArgs}, }; pub use shield_funds::ShieldFundsArgs; pub use transfer_to_alice_shields_funds::{TransferToAliceShieldsFundsArgs, ALICE_ACCOUNT_ID}; diff --git a/tee-worker/app-libs/parentchain-interface/src/integritee/mod.rs b/tee-worker/app-libs/parentchain-interface/src/integritee/mod.rs index 5dc036b76a..05803f81ac 100644 --- a/tee-worker/app-libs/parentchain-interface/src/integritee/mod.rs +++ b/tee-worker/app-libs/parentchain-interface/src/integritee/mod.rs @@ -23,7 +23,7 @@ use crate::{ decode_and_log_error, indirect_calls::{ ActivateIdentityArgs, DeactivateIdentityArgs, InvokeArgs, LinkIdentityArgs, - RemoveScheduledEnclaveArgs, RequestVCArgs, ShieldFundsArgs, UpdateScheduledEnclaveArgs, + RemoveScheduledEnclaveArgs, RequestVCArgs, SetScheduledEnclaveArgs, ShieldFundsArgs, }, integritee::extrinsic_parser::ParseExtrinsic, }; @@ -63,7 +63,7 @@ pub enum IndirectCall { #[codec(index = 5)] RequestVC(RequestVCArgs, Option>, H256), #[codec(index = 6)] - UpdateScheduledEnclave(UpdateScheduledEnclaveArgs), + SetScheduledEnclave(SetScheduledEnclaveArgs), #[codec(index = 7)] RemoveScheduledEnclave(RemoveScheduledEnclaveArgs), #[codec(index = 8)] @@ -88,7 +88,7 @@ impl> activate_identity.dispatch(executor, (address.clone(), *hash)), IndirectCall::RequestVC(request_vc, address, hash) => request_vc.dispatch(executor, (address.clone(), *hash)), - IndirectCall::UpdateScheduledEnclave(update_enclave_args) => + IndirectCall::SetScheduledEnclave(update_enclave_args) => update_enclave_args.dispatch(executor, ()), IndirectCall::RemoveScheduledEnclave(remove_enclave_args) => remove_enclave_args.dispatch(executor, ()), @@ -143,11 +143,7 @@ where "[ShieldFundsAndInvokeFilter] attempting to execute indirect call with index {:?}", index ); - if index == metadata.shield_funds_call_indexes().ok()? { - log::debug!("executing shield funds call"); - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { + if index == metadata.post_opaque_task_call_indexes().ok()? { log::debug!("executing invoke call"); let args = decode_and_log_error::(call_args)?; Some(IndirectCall::Invoke(args)) @@ -168,10 +164,10 @@ where let args = decode_and_log_error::(call_args)?; let hashed_extrinsic = xt.hashed_extrinsic; Some(IndirectCall::RequestVC(args, address, hashed_extrinsic)) - } else if index == metadata.update_scheduled_enclave().ok()? { - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::UpdateScheduledEnclave(args)) - } else if index == metadata.remove_scheduled_enclave().ok()? { + } else if index == metadata.set_scheduled_enclave_call_indexes().ok()? { + let args = decode_and_log_error::(call_args)?; + Some(IndirectCall::SetScheduledEnclave(args)) + } else if index == metadata.remove_scheduled_enclave_call_indexes().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::RemoveScheduledEnclave(args)) } else if index == metadata.batch_all_call_indexes().ok()? { @@ -193,10 +189,7 @@ fn parse_batch_all( log::debug!("Received BatchAll including {} calls", call_count.len()); for _i in 0..call_count.len() { let index: CallIndex = Decode::decode(call_args).ok()?; - if index == metadata.shield_funds_call_indexes().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { + if index == metadata.post_opaque_task_call_indexes().ok()? { let args = decode_and_log_error::(call_args)?; calls.push(IndirectCall::Invoke(args)) } else if index == metadata.link_identity_call_indexes().ok()? { @@ -215,10 +208,10 @@ fn parse_batch_all( let args = decode_and_log_error::(call_args)?; let hashed_extrinsic = hash; calls.push(IndirectCall::RequestVC(args, address.clone(), hashed_extrinsic)) - } else if index == metadata.update_scheduled_enclave().ok()? { - let args = decode_and_log_error::(call_args)?; - calls.push(IndirectCall::UpdateScheduledEnclave(args)) - } else if index == metadata.remove_scheduled_enclave().ok()? { + } else if index == metadata.set_scheduled_enclave_call_indexes().ok()? { + let args = decode_and_log_error::(call_args)?; + calls.push(IndirectCall::SetScheduledEnclave(args)) + } else if index == metadata.remove_scheduled_enclave_call_indexes().ok()? { let args = decode_and_log_error::(call_args)?; calls.push(IndirectCall::RemoveScheduledEnclave(args)) } diff --git a/tee-worker/app-libs/sgx-runtime/src/lib.rs b/tee-worker/app-libs/sgx-runtime/src/lib.rs index 0c2c5f715a..8130ab5e44 100644 --- a/tee-worker/app-libs/sgx-runtime/src/lib.rs +++ b/tee-worker/app-libs/sgx-runtime/src/lib.rs @@ -57,8 +57,7 @@ use sp_version::RuntimeVersion; pub use itp_sgx_runtime_primitives::{ constants::SLOT_DURATION, types::{ - AccountData, AccountId, Address, Balance, BlockNumber, ConvertAccountId, Hash, Header, - Index, SgxParentchainTypeConverter, Signature, + AccountData, AccountId, Address, Balance, BlockNumber, Hash, Header, Index, Signature, }, }; diff --git a/tee-worker/app-libs/stf/src/trusted_call.rs b/tee-worker/app-libs/stf/src/trusted_call.rs index d18f18dd86..725e8ee2fa 100644 --- a/tee-worker/app-libs/stf/src/trusted_call.rs +++ b/tee-worker/app-libs/stf/src/trusted_call.rs @@ -43,7 +43,7 @@ pub use ita_sgx_runtime::{Balance, IDGraph, Index, Runtime, System}; use itp_node_api::metadata::{provider::AccessNodeMetadata, NodeMetadataTrait}; use itp_node_api_metadata::{ pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_teerex::TeerexCallIndexes, pallet_vcmp::VCMPCallIndexes, + pallet_proxy::ProxyCallIndexes, pallet_vcmp::VCMPCallIndexes, }; use itp_stf_interface::{ExecuteCall, SHARD_VAULT_KEY}; pub use itp_stf_primitives::{ @@ -377,7 +377,6 @@ where node_metadata_repo: Arc, ) -> Result { let sender = self.call.sender_identity().clone(); - let call_hash = blake2_256(&self.call.encode()); let account_id: AccountId = sender.to_account_id().ok_or(Self::Error::InvalidAccount)?; let system_nonce = System::account_nonce(&account_id); ensure!(self.nonce == system_nonce, Self::Error::InvalidNonce(self.nonce, system_nonce)); @@ -524,16 +523,7 @@ where debug!("balance_shield({}, {})", account_id_to_string(&who), value); shield_funds(who, value)?; - // Send proof of execution on chain. - calls.push(ParentchainCall::Litentry(OpaqueCall::from_tuple(&( - node_metadata_repo - .get_from_metadata(|m| m.publish_hash_call_indexes()) - .map_err(|_| StfError::InvalidMetadata)? - .map_err(|_| StfError::InvalidMetadata)?, - call_hash, - Vec::::new(), - b"shielded some funds!".to_vec(), - )))); + // Litentry: we don't have publish_hash call in teebag Ok(TrustedCallResult::Empty) }, #[cfg(feature = "evm")] diff --git a/tee-worker/cli/lit_parentchain_nonce.sh b/tee-worker/cli/lit_parentchain_nonce.sh index 34c6419892..a89eaef9db 100755 --- a/tee-worker/cli/lit_parentchain_nonce.sh +++ b/tee-worker/cli/lit_parentchain_nonce.sh @@ -62,7 +62,7 @@ ${CLIENT} trusted --mrenclave $MRENCLAVE --direct send-erroneous-parentchain-cal echo "" sleep 20 -# wait for 10 `ProcessedParentchainBlock` events, which should take around 2 min (1 worker) +# wait for 10 `ParentchainBlockProcessed` events, which should take around 2 min (1 worker) # if the incoming parentchain extrinsic is blocked (due to the wrong nonce), there won't be # such many events. set -e diff --git a/tee-worker/cli/src/base_cli/commands/listen.rs b/tee-worker/cli/src/base_cli/commands/listen.rs index 27a9b15811..734ffb9af8 100644 --- a/tee-worker/cli/src/base_cli/commands/listen.rs +++ b/tee-worker/cli/src/base_cli/commands/listen.rs @@ -73,70 +73,55 @@ impl ListenCommand { }, } }, - RuntimeEvent::Teerex(ee) => { - println!(">>>>>>>>>> integritee teerex event: {:?}", ee); + RuntimeEvent::Teebag(ee) => { + println!(">>>>>>>>>> litentry teebag event: {:?}", ee); count += 1; match &ee { - my_node_runtime::pallet_teerex::Event::AddedEnclave( - accountid, + my_node_runtime::pallet_teebag::Event::EnclaveAdded { + who, + worker_type, url, - ) => { + } => { println!( - "AddedEnclave: {:?} at url {}", - accountid, + "EnclaveAdded: {:?} [{:?}] at url {}", + who, + worker_type, String::from_utf8(url.to_vec()) .unwrap_or_else(|_| "error".to_string()) ); }, - my_node_runtime::pallet_teerex::Event::RemovedEnclave( - accountid, - ) => { - println!("RemovedEnclave: {:?}", accountid); + my_node_runtime::pallet_teebag::Event::EnclaveRemoved { + who, + } => { + println!("EnclaveRemoved: {:?}", who); }, - my_node_runtime::pallet_teerex::Event::Forwarded(shard) => { + my_node_runtime::pallet_teebag::Event::OpaqueTaskPosted { shard } => { println!( - "Forwarded request for shard {}", + "OpaqueTaskPosted for shard {}", shard.encode().to_base58() ); }, - my_node_runtime::pallet_teerex::Event::ProcessedParentchainBlock( - accountid, - block_hash, - merkle_root, + my_node_runtime::pallet_teebag::Event::ParentchainBlockProcessed { + who, block_number, - ) => { + block_hash, + task_merkle_root, + } => { println!( - "ProcessedParentchainBlock from {} with hash {:?}, number {} and merkle root {:?}", - accountid, block_hash, merkle_root, block_number + "ParentchainBlockProcessed from {} with hash {:?}, number {} and merkle root {:?}", + who, block_hash, block_number, task_merkle_root ); }, - my_node_runtime::pallet_teerex::Event::ShieldFunds( - incognito_account, - ) => { - println!("ShieldFunds for {:?}", incognito_account); - }, - my_node_runtime::pallet_teerex::Event::UnshieldedFunds( - public_account, - ) => { - println!("UnshieldFunds for {:?}", public_account); - }, - _ => debug!("ignoring unsupported teerex event: {:?}", ee), - } - }, - RuntimeEvent::Sidechain(ee) => { - println!(">>>>>>>>>> integritee sidechain event: {:?}", ee); - count += 1; - match &ee { - my_node_runtime::pallet_sidechain::Event::ProposedSidechainBlock( - accountid, - block_hash, - ) => { + my_node_runtime::pallet_teebag::Event::SidechainBlockFinalized { + who, + sidechain_block_number, + } => { println!( - "ProposedSidechainBlock from {} with hash {:?}", - accountid, block_hash + "SidechainBlockFinalized from {} with number {:?}", + who, sidechain_block_number ); }, - _ => debug!("ignoring unsupported sidechain event: {:?}", ee), + _ => debug!("ignoring unsupported teebag event: {:?}", ee), } }, _ => debug!("ignoring unsupported module event: {:?}", evr.event), diff --git a/tee-worker/cli/src/base_cli/mod.rs b/tee-worker/cli/src/base_cli/mod.rs index 7270dd8dbb..a42888e4bf 100644 --- a/tee-worker/cli/src/base_cli/mod.rs +++ b/tee-worker/cli/src/base_cli/mod.rs @@ -35,7 +35,8 @@ use base58::ToBase58; use chrono::{DateTime, Utc}; use clap::Subcommand; use itc_rpc_client::direct_client::DirectApi; -use itp_node_api::api_client::PalletTeerexApi; +use itp_node_api::api_client::PalletTeebagApi; +use itp_types::WorkerType; use sp_core::crypto::Ss58Codec; use sp_keystore::Keystore; use std::{ @@ -178,29 +179,22 @@ fn print_sgx_metadata_raw(cli: &Cli) -> CliResult { fn list_workers(cli: &Cli) -> CliResult { let api = get_chain_api(cli); - let wcount = api.enclave_count(None).unwrap(); - println!("number of workers registered: {}", wcount); - - let mut mr_enclaves = Vec::with_capacity(wcount as usize); - - for w in 1..=wcount { - let enclave = api.enclave(w, None).unwrap(); - if enclave.is_none() { - println!("error reading enclave data"); - continue - }; - let enclave = enclave.unwrap(); - let timestamp = - DateTime::::from(UNIX_EPOCH + Duration::from_millis(enclave.timestamp)); - let mr_enclave = enclave.mr_enclave.to_base58(); - println!("Enclave {}", w); - println!(" AccountId: {}", enclave.pubkey.to_ss58check()); - println!(" MRENCLAVE: {}", mr_enclave); - println!(" RA timestamp: {}", timestamp); - println!(" URL: {}", enclave.url); - - mr_enclaves.push(mr_enclave); - } + let enclaves = api.all_enclaves(WorkerType::Identity, None).unwrap(); + println!("number of enclaves registered: {}", enclaves.len()); + + let mr_enclaves = enclaves + .iter() + .map(|enclave| { + println!("Enclave"); + println!(" MRENCLAVE: {}", enclave.mrenclave.to_base58()); + let timestamp = DateTime::::from( + UNIX_EPOCH + Duration::from_millis(enclave.last_seen_timestamp), + ); + println!(" Last seen: {}", timestamp); + println!(" URL: {}", String::from_utf8_lossy(enclave.url.as_slice())); + enclave.mrenclave.to_base58() + }) + .collect(); Ok(CliResultOk::MrEnclaveBase58 { mr_enclaves }) } diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/sidechain/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/sidechain/definitions.ts index c111ab837b..86364ad00c 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/sidechain/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/sidechain/definitions.ts @@ -79,7 +79,6 @@ export default { NoEligibleIdentity: "Null", }, }, - // teerex ShardIdentifier: "H256", }, }; diff --git a/tee-worker/core-primitives/enclave-api/Cargo.toml b/tee-worker/core-primitives/enclave-api/Cargo.toml index c9dfaa9dff..b0541b1136 100644 --- a/tee-worker/core-primitives/enclave-api/Cargo.toml +++ b/tee-worker/core-primitives/enclave-api/Cargo.toml @@ -26,7 +26,7 @@ itp-storage = { path = "../storage" } itp-types = { path = "../types" } # litentry -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } +pallet-teebag = { path = "../../../pallets/teebag", default-features = false } [features] default = [] diff --git a/tee-worker/core-primitives/enclave-api/src/enclave_base.rs b/tee-worker/core-primitives/enclave-api/src/enclave_base.rs index 4e79a6f902..b0f9dcb047 100644 --- a/tee-worker/core-primitives/enclave-api/src/enclave_base.rs +++ b/tee-worker/core-primitives/enclave-api/src/enclave_base.rs @@ -21,9 +21,9 @@ use codec::Decode; use core::fmt::Debug; use itc_parentchain::primitives::{ParentchainId, ParentchainInitParams}; use itp_types::ShardIdentifier; +use pallet_teebag::EnclaveFingerprint; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sp_core::ed25519; -use teerex_primitives::EnclaveFingerprint; /// Trait for base/common Enclave API functions pub trait EnclaveBase: Send + Sync + 'static { @@ -97,10 +97,10 @@ mod impl_ffi { }; use itp_types::ShardIdentifier; use log::*; + use pallet_teebag::EnclaveFingerprint; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sgx_types::*; use sp_core::ed25519; - use teerex_primitives::EnclaveFingerprint; impl EnclaveBase for Enclave { fn init( diff --git a/tee-worker/core-primitives/enclave-api/src/remote_attestation.rs b/tee-worker/core-primitives/enclave-api/src/remote_attestation.rs index 9aa32cb631..30a765da32 100644 --- a/tee-worker/core-primitives/enclave-api/src/remote_attestation.rs +++ b/tee-worker/core-primitives/enclave-api/src/remote_attestation.rs @@ -18,8 +18,8 @@ use crate::EnclaveResult; use itp_types::ShardIdentifier; +use pallet_teebag::Fmspc; use sgx_types::*; -use teerex_primitives::Fmspc; /// Struct that unites all relevant data reported by the QVE pub struct QveReport { @@ -128,8 +128,8 @@ mod impl_ffi { use itp_settings::worker::EXTRINSIC_MAX_SIZE; use itp_types::ShardIdentifier; use log::*; + use pallet_teebag::Fmspc; use sgx_types::*; - use teerex_primitives::Fmspc; const OS_SYSTEM_PATH: &str = "/usr/lib/x86_64-linux-gnu/"; const C_STRING_ENDING: &str = "\0"; diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index 2829b53c1c..e6e7c3757e 100644 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -21,12 +21,17 @@ pub use substrate_api_client::{api::Error as ApiClientError, rpc::TungsteniteRpc pub mod account; pub mod chain; -pub mod pallet_teeracle; +pub mod pallet_teebag; + +// TODO: part of P-487 - these will be removed anyway so I haven't spent time adjust them + +// pub mod pallet_teeracle; pub mod pallet_teerex; pub use account::*; pub use chain::*; -pub use pallet_teeracle::*; +pub use pallet_teebag::*; +// pub use pallet_teeracle::*; pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs new file mode 100644 index 0000000000..e243091ca5 --- /dev/null +++ b/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs @@ -0,0 +1,134 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::ApiResult; +use itp_api_client_types::{traits::GetStorage, Api, Config, Request}; +use itp_types::{AccountId, Enclave, ShardIdentifier, WorkerType}; + +pub const TEEBAG: &str = "Teebag"; + +/// ApiClient extension that enables communication with the `teebag` pallet. +pub trait PalletTeebagApi { + type Hash; + + fn enclave( + &self, + account: &AccountId, + at_block: Option, + ) -> ApiResult>; + fn enclave_count( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult; + fn primary_enclave_identifier_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult>; + fn primary_enclave_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult>; + fn all_enclaves( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult>; +} + +impl PalletTeebagApi for Api +where + RuntimeConfig: Config, + Client: Request, +{ + type Hash = RuntimeConfig::Hash; + + fn enclave( + &self, + account: &AccountId, + at_block: Option, + ) -> ApiResult> { + self.get_storage_map(TEEBAG, "EnclaveRegistry", account, at_block) + } + + fn enclave_count( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult { + // Vec<> and BoundedVec<> have the same encoding, thus they are used interchangeably + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + Ok(identifiers.len() as u64) + } + + // please note we don't use dedicated on-chain storage for this (like the upstream `WorkerForShard`) + // so this API will always return the "first" registered and qualified enclave. + // Wheter it meets our needs needs to be further evaluated + fn primary_enclave_identifier_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + let mut maybe_account: Option = None; + for account in identifiers { + match self.enclave(&account, at_block)? { + Some(e) => + if e.mrenclave == shard.as_ref() { + maybe_account = Some(account.clone()); + break + }, + None => continue, + } + } + Ok(maybe_account) + } + + fn primary_enclave_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { + self.primary_enclave_identifier_for_shard(worker_type, shard, at_block)? + .map_or_else(|| Ok(None), |account| self.enclave(&account, at_block)) + } + + fn all_enclaves( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult> { + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + + let enclaves = identifiers + .into_iter() + .filter_map(|account| self.enclave(&account, at_block).ok()?) + .collect(); + Ok(enclaves) + } +} diff --git a/tee-worker/core-primitives/node-api/metadata/src/lib.rs b/tee-worker/core-primitives/node-api/metadata/src/lib.rs index 0a069c0277..956255578d 100644 --- a/tee-worker/core-primitives/node-api/metadata/src/lib.rs +++ b/tee-worker/core-primitives/node-api/metadata/src/lib.rs @@ -21,9 +21,9 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, pallet_vcmp::VCMPCallIndexes, + pallet_proxy::ProxyCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teebag::TeebagCallIndexes, pallet_utility::UtilityCallIndexes, + pallet_vcmp::VCMPCallIndexes, }; use codec::{Decode, Encode}; use sp_core::storage::StorageKey; @@ -35,10 +35,8 @@ pub mod error; pub mod pallet_balances; pub mod pallet_imp; pub mod pallet_proxy; -pub mod pallet_sidechain; pub mod pallet_system; -pub mod pallet_teeracle; -pub mod pallet_teerex; +pub mod pallet_teebag; pub mod pallet_utility; pub mod pallet_vcmp; pub mod runtime_call; @@ -47,8 +45,7 @@ pub mod runtime_call; pub mod metadata_mocks; pub trait NodeMetadataTrait: - TeerexCallIndexes - + SidechainCallIndexes + TeebagCallIndexes + IMPCallIndexes + VCMPCallIndexes + SystemSs58Prefix @@ -57,9 +54,9 @@ pub trait NodeMetadataTrait: + BalancesCallIndexes { } + impl< - T: TeerexCallIndexes - + SidechainCallIndexes + T: TeebagCallIndexes + IMPCallIndexes + VCMPCallIndexes + SystemSs58Prefix diff --git a/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index cdf24e4fcc..458e893759 100644 --- a/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -1,7 +1,7 @@ /* Copyright 2021 Integritee AG and Supercomputing Systems AG - Licensed under the Apache License, Version 2.0 (the "License"); + 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 @@ -17,9 +17,9 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, pallet_vcmp::VCMPCallIndexes, runtime_call::RuntimeCall, + pallet_proxy::ProxyCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teebag::TeebagCallIndexes, pallet_utility::UtilityCallIndexes, + pallet_vcmp::VCMPCallIndexes, runtime_call::RuntimeCall, }; use codec::{Decode, Encode}; @@ -35,23 +35,18 @@ impl TryFrom for Metadata { #[derive(Default, Encode, Decode, Debug, Clone)] pub struct NodeMetadataMock { - teerex_module: u8, + // litentry + // teebag + teebag_module: u8, + set_scheduled_enclave: u8, + remove_scheduled_enclave: u8, register_enclave: u8, - unregister_sovereign_enclave: u8, - unregister_proxied_enclave: u8, + unregister_enclave: u8, register_quoting_enclave: u8, register_tcb_info: u8, - enclave_bridge_module: u8, - invoke: u8, - confirm_processed_parentchain_block: u8, - shield_funds: u8, - unshield_funds: u8, - publish_hash: u8, - update_shard_config: u8, - sidechain_module: u8, - // litentry - update_scheduled_enclave: u8, - remove_scheduled_enclave: u8, + post_opaque_task: u8, + parentchain_block_processed: u8, + sidechain_block_imported: u8, // IMP imp_module: u8, imp_link_identity: u8, @@ -76,7 +71,6 @@ pub struct NodeMetadataMock { utility_dispatch_as: u8, utility_force_batch: u8, - imported_sidechain_block: u8, proxy_module: u8, add_proxy: u8, proxy: u8, @@ -91,23 +85,17 @@ pub struct NodeMetadataMock { impl NodeMetadataMock { pub fn new() -> Self { NodeMetadataMock { - teerex_module: 50u8, - register_enclave: 0u8, - unregister_sovereign_enclave: 1u8, - unregister_proxied_enclave: 2u8, - register_quoting_enclave: 3, - register_tcb_info: 4, - enclave_bridge_module: 54u8, - invoke: 0u8, - confirm_processed_parentchain_block: 1u8, - shield_funds: 2u8, - unshield_funds: 3u8, - publish_hash: 4u8, - update_shard_config: 5u8, - sidechain_module: 53u8, // litentry - update_scheduled_enclave: 10u8, - remove_scheduled_enclave: 11u8, + teebag_module: 50u8, + set_scheduled_enclave: 0u8, + remove_scheduled_enclave: 1u8, + register_enclave: 2u8, + unregister_enclave: 3u8, + register_quoting_enclave: 4u8, + register_tcb_info: 5u8, + post_opaque_task: 6u8, + parentchain_block_processed: 7u8, + sidechain_block_imported: 8u8, imp_module: 64u8, imp_link_identity: 1u8, @@ -132,7 +120,6 @@ impl NodeMetadataMock { utility_dispatch_as: 3u8, utility_force_batch: 4u8, - imported_sidechain_block: 0u8, proxy_module: 7u8, add_proxy: 1u8, proxy: 0u8, @@ -146,63 +133,33 @@ impl NodeMetadataMock { } } -impl TeerexCallIndexes for NodeMetadataMock { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_enclave]) +impl TeebagCallIndexes for NodeMetadataMock { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.set_scheduled_enclave]) } - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unregister_sovereign_enclave]) + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.remove_scheduled_enclave]) } - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unregister_proxied_enclave]) + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.register_enclave]) + } + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.unregister_enclave]) } - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_quoting_enclave]) + Ok([self.teebag_module, self.register_quoting_enclave]) } - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_tcb_info]) - } - - fn invoke_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.invoke]) + Ok([self.teebag_module, self.register_tcb_info]) } - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.confirm_processed_parentchain_block]) - } - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.shield_funds]) + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.post_opaque_task]) } - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unshield_funds]) + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.parentchain_block_processed]) } - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.publish_hash]) - } - - // fn update_shard_config_call_indexes(&self) -> Result<[u8; 2]> { - // Ok([self.teerex_module, self.update_shard_config]) - // } - - fn update_scheduled_enclave(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.update_scheduled_enclave]) - } - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.remove_scheduled_enclave]) - } -} - -impl SidechainCallIndexes for NodeMetadataMock { - fn confirm_imported_sidechain_block_indexes(&self) -> Result<[u8; 2]> { - Ok([self.sidechain_module, self.imported_sidechain_block]) + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.sidechain_block_imported]) } } diff --git a/tee-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs b/tee-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs new file mode 100644 index 0000000000..0dc73cfb25 --- /dev/null +++ b/tee-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs @@ -0,0 +1,71 @@ +/* + Copyright 2021 Integritee AG and Supercomputing Systems AG + + 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. + +*/ +use crate::{error::Result, NodeMetadata}; + +/// Pallet' name: +pub const TEEBAG: &str = "Teebag"; + +// we only list the extrinsics that we care +pub trait TeebagCallIndexes { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]>; + + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]>; + + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]>; + + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]>; +} + +impl TeebagCallIndexes for NodeMetadata { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "set_scheduled_enclave") + } + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "remove_scheduled_enclave") + } + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_enclave") + } + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "unregister_enclave") + } + fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_quoting_enclave") + } + fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_tcb_info") + } + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "post_opaque_task") + } + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "parentchain_block_processed") + } + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "sidechain_block_imported") + } +} diff --git a/tee-worker/core-primitives/sgx-runtime-primitives/Cargo.toml b/tee-worker/core-primitives/sgx-runtime-primitives/Cargo.toml index 8ec87045fa..5290e8da32 100644 --- a/tee-worker/core-primitives/sgx-runtime-primitives/Cargo.toml +++ b/tee-worker/core-primitives/sgx-runtime-primitives/Cargo.toml @@ -12,9 +12,6 @@ pallet-balances = { default-features = false, git = "https://github.com/parityte sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -# litentry -litentry-primitives = { path = "../../litentry/primitives", default-features = false } - [features] default = ["std"] std = [ @@ -22,6 +19,4 @@ std = [ "pallet-balances/std", "sp-core/std", "sp-runtime/std", - # litentry - "litentry-primitives/std", ] diff --git a/tee-worker/core-primitives/sgx-runtime-primitives/src/types.rs b/tee-worker/core-primitives/sgx-runtime-primitives/src/types.rs index bad667791e..035ae982b8 100644 --- a/tee-worker/core-primitives/sgx-runtime-primitives/src/types.rs +++ b/tee-worker/core-primitives/sgx-runtime-primitives/src/types.rs @@ -21,8 +21,6 @@ use sp_runtime::{ MultiSignature, OpaqueExtrinsic, }; -use litentry_primitives::ParentchainAccountId; - /// The address format for describing accounts. pub type Address = sp_runtime::MultiAddress; /// Block header type as expected by this sgx-runtime. @@ -66,21 +64,3 @@ pub type Block = BlockG; pub type SignedBlock = SignedBlockG; pub type BlockHash = sp_core::H256; pub type ShardIdentifier = sp_core::H256; - -// litentry -pub trait ConvertAccountId { - type Input; - type Output; - fn convert(input: Self::Input) -> Self::Output; -} - -pub struct SgxParentchainTypeConverter; - -impl ConvertAccountId for SgxParentchainTypeConverter { - type Input = AccountId; - type Output = ParentchainAccountId; - fn convert(a: AccountId) -> ParentchainAccountId { - // it's an identity converter - a as ParentchainAccountId - } -} diff --git a/tee-worker/core-primitives/test/Cargo.toml b/tee-worker/core-primitives/test/Cargo.toml index ff82183e77..92466e8615 100644 --- a/tee-worker/core-primitives/test/Cargo.toml +++ b/tee-worker/core-primitives/test/Cargo.toml @@ -29,12 +29,12 @@ itp-stf-interface = { path = "../stf-interface", default-features = false } itp-stf-primitives = { path = "../stf-primitives", default-features = false } itp-stf-state-handler = { path = "../stf-state-handler", default-features = false } itp-storage = { path = "../storage", default-features = false } -itp-teerex-storage = { path = "../teerex-storage", default-features = false } itp-time-utils = { path = "../time-utils", default-features = false } itp-types = { path = "../types", default-features = false, features = ["test"] } # litentry hex = { version = "0.4.3", default-features = false } +lc-teebag-storage = { path = "../../litentry/core/teebag-storage", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } [features] @@ -50,7 +50,6 @@ std = [ "itp-stf-primitives/std", "itp-stf-state-handler/std", "itp-storage/std", - "itp-teerex-storage/std", "itp-time-utils/std", "itp-types/std", "log/std", @@ -59,6 +58,7 @@ std = [ "sp-runtime/std", "sp-std/std", "litentry-primitives/std", + "lc-teebag-storage/std", ] sgx = [ "itp-node-api/sgx", diff --git a/tee-worker/core-primitives/test/src/mock/onchain_mock.rs b/tee-worker/core-primitives/test/src/mock/onchain_mock.rs index 22744289b5..a0163f8df0 100644 --- a/tee-worker/core-primitives/test/src/mock/onchain_mock.rs +++ b/tee-worker/core-primitives/test/src/mock/onchain_mock.rs @@ -23,14 +23,14 @@ use itp_ocall_api::{ EnclaveSidechainOCallApi, }; use itp_storage::Error::StorageValueUnavailable; -use itp_teerex_storage::{TeeRexStorage, TeerexStorageKeys}; use itp_types::{ - parentchain::ParentchainId, storage::StorageEntryVerified, BlockHash, Enclave, ShardIdentifier, - WorkerRequest, WorkerResponse, + parentchain::ParentchainId, storage::StorageEntryVerified, AccountId, BlockHash, + ShardIdentifier, WorkerRequest, WorkerResponse, WorkerType, }; +use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; use sgx_types::*; use sp_core::H256; -use sp_runtime::{traits::Header as HeaderTrait, AccountId32, OpaqueExtrinsic}; +use sp_runtime::{traits::Header as HeaderTrait, OpaqueExtrinsic}; use sp_std::prelude::*; use std::{collections::HashMap, string::String}; @@ -55,11 +55,15 @@ impl OnchainMock { pub fn add_validateer_set>( mut self, header: &Header, - set: Option>, + set: Option>, ) -> Self { - let set = set.unwrap_or_else(validateer_set); - self.insert_at_header(header, TeeRexStorage::enclave_count(), (set.len() as u64).encode()); - self.with_storage_entries_at_header(header, into_key_value_storage(set)) + let set: Vec = set.unwrap_or_else(validateer_set); + self.insert_at_header( + header, + TeebagStorage::enclave_identifier(WorkerType::Identity), + set.encode(), + ); + self } pub fn with_mr_enclave(mut self, mr_enclave: [u8; SGX_HASH_SIZE]) -> Self { @@ -224,20 +228,11 @@ impl EnclaveOnChainOCallApi for OnchainMock { } } -pub fn validateer_set() -> Vec { - let default_enclave = Enclave::new( - AccountId32::from([0; 32]), - Default::default(), - Default::default(), - Default::default(), - ); - vec![default_enclave.clone(), default_enclave.clone(), default_enclave.clone(), default_enclave] -} - -fn into_key_value_storage(validateers: Vec) -> Vec<(Vec, Enclave)> { - validateers - .into_iter() - .enumerate() - .map(|(i, e)| (TeeRexStorage::enclave(i as u64 + 1), e)) - .collect() +pub fn validateer_set() -> Vec { + vec![ + AccountId::from([0; 32]), + AccountId::from([1; 32]), + AccountId::from([2; 32]), + AccountId::from([3; 32]), + ] } diff --git a/tee-worker/core-primitives/types/src/lib.rs b/tee-worker/core-primitives/types/src/lib.rs index 911282e427..e04dcaf02e 100644 --- a/tee-worker/core-primitives/types/src/lib.rs +++ b/tee-worker/core-primitives/types/src/lib.rs @@ -37,12 +37,13 @@ pub type PalletString = Vec; pub type PalletString = String; pub use itp_sgx_runtime_primitives::types::*; -pub use litentry_primitives::{Assertion, DecryptableRequest}; +pub use litentry_primitives::{ + Assertion, AttestationType, DecryptableRequest, Enclave, EnclaveFingerprint, MrEnclave, + WorkerType, +}; pub use sp_core::{crypto::AccountId32 as AccountId, H256}; pub type IpfsHash = [u8; 46]; -pub type MrEnclave = [u8; 32]; - pub type CallIndex = [u8; 2]; // pallet teerex @@ -50,7 +51,7 @@ pub type ConfirmCallFn = (CallIndex, ShardIdentifier, H256, Vec); pub type ShieldFundsFn = (CallIndex, Vec, Balance, ShardIdentifier); pub type CallWorkerFn = (CallIndex, RsaRequest); -pub type UpdateScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); +pub type SetScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); pub type RemoveScheduledEnclaveFn = (CallIndex, SidechainBlockNumber); // pallet IMP @@ -67,8 +68,6 @@ pub type ActivateIdentityFn = (CallIndex, DeactivateIdentityParams); pub type RequestVCParams = (ShardIdentifier, Assertion); pub type RequestVCFn = (CallIndex, RequestVCParams); -pub type Enclave = EnclaveGen; - /// Simple blob to hold an encoded call #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct OpaqueCall(pub Vec); @@ -108,23 +107,6 @@ impl DecryptableRequest for RsaRequest { } } -// Todo: move this improved enclave definition into a primitives crate in the pallet_teerex repo. -#[derive(Encode, Decode, Clone, PartialEq, sp_core::RuntimeDebug)] -pub struct EnclaveGen { - pub pubkey: AccountId, - // FIXME: this is redundant information - pub mr_enclave: [u8; 32], - pub timestamp: u64, - // unix epoch in milliseconds - pub url: PalletString, // utf8 encoded url -} - -impl EnclaveGen { - pub fn new(pubkey: AccountId, mr_enclave: [u8; 32], timestamp: u64, url: PalletString) -> Self { - Self { pubkey, mr_enclave, timestamp, url } - } -} - #[derive(Debug, Clone, PartialEq, Encode, Decode, Eq)] pub enum DirectRequestStatus { /// Direct request was successfully executed diff --git a/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs b/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs index 0c2dbcf74c..7132b0bdeb 100644 --- a/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs +++ b/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs @@ -29,7 +29,7 @@ use binary_merkle_tree::merkle_root; use codec::{Decode, Encode}; use core::marker::PhantomData; use itp_node_api::metadata::{ - pallet_teerex::TeerexCallIndexes, provider::AccessNodeMetadata, NodeMetadataTrait, + pallet_teebag::TeebagCallIndexes, provider::AccessNodeMetadata, NodeMetadataTrait, }; use itp_sgx_crypto::{key_repository::AccessKey, ShieldingCryptoDecrypt, ShieldingCryptoEncrypt}; use itp_stf_executor::traits::{StfEnclaveSigning, StfShardVaultQuery}; @@ -217,10 +217,10 @@ impl< ParentchainBlock: ParentchainBlockTrait, { let call = self.node_meta_data_provider.get_from_metadata(|meta_data| { - meta_data.confirm_processed_parentchain_block_call_indexes() + meta_data.parentchain_block_processed_call_indexes() })??; let root: H256 = merkle_root::(extrinsics); - trace!("prepared confirm_processed_parentchain_block() call for block {:?} with index {:?} and merkle root {}", block_number, call, root); + trace!("prepared parentchain_block_processed() call for block {:?} with index {:?} and merkle root {}", block_number, call, root); // Litentry: we don't include `shard` in the extrinsic parameter to be backwards compatible, // however, we should not forget it in case we need it later Ok(OpaqueCall::from_tuple(&(call, block_hash, block_number, root))) @@ -366,41 +366,6 @@ mod test { assert_eq!(1, top_pool_author.pending_tops(shard_id()).unwrap().len()); } - #[test] - fn shielding_call_can_be_added_to_pool_successfully() { - let _ = env_logger::builder().is_test(true).try_init(); - - let mr_enclave = [33u8; 32]; - let (indirect_calls_executor, top_pool_author, shielding_key_repo) = - test_fixtures(mr_enclave.clone(), NodeMetadataMock::new()); - let shielding_key = shielding_key_repo.retrieve_key().unwrap(); - - let opaque_extrinsic = OpaqueExtrinsic::from_bytes( - shield_funds_unchecked_extrinsic(&shielding_key).encode().as_slice(), - ) - .unwrap(); - - let parentchain_block = ParentchainBlockBuilder::default() - .with_extrinsics(vec![opaque_extrinsic]) - .build(); - - indirect_calls_executor - .execute_indirect_calls_in_extrinsics(&parentchain_block, &Vec::new()) - .unwrap(); - - assert_eq!(1, top_pool_author.pending_tops(shard_id()).unwrap().len()); - let submitted_extrinsic = - top_pool_author.pending_tops(shard_id()).unwrap().first().cloned().unwrap(); - let decrypted_extrinsic = shielding_key.decrypt(&submitted_extrinsic).unwrap(); - let decoded_operation = TrustedOperation::::decode( - &mut decrypted_extrinsic.as_slice(), - ) - .unwrap(); - assert_matches!(decoded_operation, TrustedOperation::indirect_call(_)); - let trusted_call_signed = decoded_operation.to_call().unwrap(); - assert!(trusted_call_signed.verify_signature(&mr_enclave, &shard_id())); - } - #[test] fn ensure_empty_extrinsic_vec_triggers_zero_filled_merkle_root() { // given @@ -409,11 +374,10 @@ mod test { let block_hash = H256::from([1; 32]); let extrinsics = Vec::new(); - let confirm_processed_parentchain_block_indexes = - dummy_metadata.confirm_processed_parentchain_block_call_indexes().unwrap(); + let parentchain_block_processed_call_indexes = + dummy_metadata.parentchain_block_processed_call_indexes().unwrap(); let expected_call = - (confirm_processed_parentchain_block_indexes, block_hash, 1u32, H256::default()) - .encode(); + (parentchain_block_processed_call_indexes, block_hash, 1u32, H256::default()).encode(); // when let call = indirect_calls_executor @@ -432,12 +396,11 @@ mod test { let block_hash = H256::from([1; 32]); let extrinsics = vec![H256::from([4; 32]), H256::from([9; 32])]; - let confirm_processed_parentchain_block_indexes = - dummy_metadata.confirm_processed_parentchain_block_call_indexes().unwrap(); + let parentchain_block_processed_call_indexes = + dummy_metadata.parentchain_block_processed_call_indexes().unwrap(); let zero_root_call = - (confirm_processed_parentchain_block_indexes, block_hash, 1u32, H256::default()) - .encode(); + (parentchain_block_processed_call_indexes, block_hash, 1u32, H256::default()).encode(); // when let call = indirect_calls_executor @@ -448,25 +411,10 @@ mod test { assert_ne!(call.0, zero_root_call); } - fn shield_funds_unchecked_extrinsic( - shielding_key: &ShieldingCryptoMock, - ) -> ParentchainUncheckedExtrinsic { - let target_account = shielding_key.encrypt(&AccountId::new([2u8; 32]).encode()).unwrap(); - let dummy_metadata = NodeMetadataMock::new(); - - let shield_funds_indexes = dummy_metadata.shield_funds_call_indexes().unwrap(); - ParentchainUncheckedExtrinsic::::new_signed( - (shield_funds_indexes, target_account, 1000u128, shard_id()), - MultiAddress::Address32([1u8; 32]), - MultiSignature::Ed25519(default_signature()), - default_extrinsic_params().signed_extra(), - ) - } - fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { let request = RsaRequest::new(shard_id(), vec![1u8, 2u8]); let dummy_metadata = NodeMetadataMock::new(); - let call_worker_indexes = dummy_metadata.invoke_call_indexes().unwrap(); + let call_worker_indexes = dummy_metadata.post_opaque_task_call_indexes().unwrap(); ParentchainUncheckedExtrinsic::::new_signed( (call_worker_indexes, request), diff --git a/tee-worker/core/parentchain/indirect-calls-executor/src/mock.rs b/tee-worker/core/parentchain/indirect-calls-executor/src/mock.rs index 38189f44d8..57c1645cc2 100644 --- a/tee-worker/core/parentchain/indirect-calls-executor/src/mock.rs +++ b/tee-worker/core/parentchain/indirect-calls-executor/src/mock.rs @@ -58,11 +58,7 @@ where "[ShieldFundsAndInvokeFilter] attempting to execute indirect call with index {:?}", index ); - if index == metadata.shield_funds_call_indexes().ok()? { - log::debug!("executing shield funds call"); - let args = ShieldFundsArgs::decode(call_args).unwrap(); - Some(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { + if index == metadata.post_opaque_task_call_indexes().ok()? { log::debug!("executing invoke call"); let args = InvokeArgs::decode(call_args).unwrap(); Some(IndirectCall::Invoke(args)) diff --git a/tee-worker/core/rpc-client/Cargo.toml b/tee-worker/core/rpc-client/Cargo.toml index fc06593ed3..1f2b3ff104 100644 --- a/tee-worker/core/rpc-client/Cargo.toml +++ b/tee-worker/core/rpc-client/Cargo.toml @@ -31,7 +31,6 @@ itp-utils = { path = "../../core-primitives/utils" } ita-stf = { path = "../../app-libs/stf" } itp-stf-primitives = { path = "../../core-primitives/stf-primitives" } litentry-primitives = { path = "../../litentry/primitives", default-features = false } -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } [dev-dependencies] env_logger = "0.9.0" diff --git a/tee-worker/core/rpc-client/src/direct_client.rs b/tee-worker/core/rpc-client/src/direct_client.rs index 7814b0e7e3..f5fad98dc2 100644 --- a/tee-worker/core/rpc-client/src/direct_client.rs +++ b/tee-worker/core/rpc-client/src/direct_client.rs @@ -17,6 +17,7 @@ //! Interface for direct access to a workers rpc. +pub use crate::error::{Error, Result}; use crate::ws_client::{WsClient, WsClientControl}; use base58::ToBase58; use codec::{Decode, Encode}; @@ -25,7 +26,7 @@ use ita_stf::{Getter, PublicGetter}; use itp_api_client_types::Metadata; use itp_rpc::{Id, RpcRequest, RpcResponse, RpcReturnValue}; use itp_stf_primitives::types::{AccountId, ShardIdentifier}; -use itp_types::{DirectRequestStatus, RsaRequest}; +use itp_types::{DirectRequestStatus, MrEnclave, RsaRequest}; use itp_utils::{FromHexPrefixed, ToHexPrefixed}; use litentry_primitives::Identity; use log::*; @@ -39,9 +40,6 @@ use std::{ thread, thread::JoinHandle, }; -use teerex_primitives::MrEnclave; - -pub use crate::error::{Error, Result}; #[derive(Clone)] pub struct DirectClient { diff --git a/tee-worker/core/rpc-client/src/mock.rs b/tee-worker/core/rpc-client/src/mock.rs index 8f582ecc27..bffb003e65 100644 --- a/tee-worker/core/rpc-client/src/mock.rs +++ b/tee-worker/core/rpc-client/src/mock.rs @@ -23,10 +23,10 @@ use frame_metadata::RuntimeMetadataPrefixed; use ita_stf::H256; use itp_api_client_types::Metadata; use itp_stf_primitives::types::{AccountId, ShardIdentifier}; +use itp_types::MrEnclave; use litentry_primitives::Identity; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use std::{sync::mpsc::Sender as MpscSender, thread::JoinHandle}; -use teerex_primitives::MrEnclave; #[derive(Clone, Default)] pub struct DirectClientMock { diff --git a/tee-worker/enclave-runtime/Cargo.lock b/tee-worker/enclave-runtime/Cargo.lock index 136db05d22..f5cd1bddbc 100644 --- a/tee-worker/enclave-runtime/Cargo.lock +++ b/tee-worker/enclave-runtime/Cargo.lock @@ -545,6 +545,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "num-traits 0.2.16", + "serde 1.0.193", ] [[package]] @@ -737,6 +738,17 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "der_derive", + "flagset", +] + [[package]] name = "der" version = "0.7.8" @@ -747,6 +759,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef71ddb5b3a1f53dee24817c8f70dfa1cb29e804c18d88c228d4bc9c86ee3b9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote 1.0.33", + "syn 1.0.109", +] + [[package]] name = "derive-syn-parse" version = "0.1.5" @@ -807,7 +831,7 @@ version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ - "der", + "der 0.7.8", "digest 0.10.7", "elliptic-curve", "rfc6979", @@ -937,7 +961,7 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "teerex-primitives 0.1.0 (git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42)", + "teerex-primitives", "webpki", ] @@ -1173,6 +1197,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flagset" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" + [[package]] name = "fnv" version = "1.0.6" @@ -2396,7 +2426,6 @@ name = "itp-sgx-runtime-primitives" version = "0.9.0" dependencies = [ "frame-system", - "litentry-primitives", "pallet-balances", "sp-core", "sp-runtime", @@ -2514,14 +2543,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "itp-teerex-storage" -version = "0.9.0" -dependencies = [ - "itp-storage", - "sp-std", -] - [[package]] name = "itp-test" version = "0.9.0" @@ -2536,10 +2557,10 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-teerex-storage", "itp-time-utils", "itp-types", "jsonrpc-core", + "lc-teebag-storage", "litentry-primitives", "log", "parity-scale-codec", @@ -2825,10 +2846,9 @@ name = "its-validateer-fetch" version = "0.9.0" dependencies = [ "derive_more", - "frame-support", "itp-ocall-api", - "itp-teerex-storage", "itp-types", + "lc-teebag-storage", "parity-scale-codec", "sp-core", "sp-runtime", @@ -2837,9 +2857,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54c0c35952f67de54bb584e9fd912b3023117cbafc0a77d8f3dee1fb5f572fe8" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -3122,6 +3142,15 @@ dependencies = [ "url", ] +[[package]] +name = "lc-teebag-storage" +version = "0.1.0" +dependencies = [ + "itp-storage", + "itp-types", + "sp-std", +] + [[package]] name = "lc-vc-task-receiver" version = "0.1.0" @@ -3257,6 +3286,7 @@ dependencies = [ "litentry-hex-utils", "log", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "pallet-teebag", "parity-scale-codec", "rand 0.7.3", "ring 0.16.20", @@ -3270,7 +3300,6 @@ dependencies = [ "sp-std", "strum", "strum_macros", - "teerex-primitives 0.1.0", ] [[package]] @@ -3687,6 +3716,31 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-teebag" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "chrono 0.4.31", + "der 0.6.1", + "frame-support", + "frame-system", + "hex 0.4.3", + "log", + "pallet-timestamp", + "parity-scale-codec", + "ring 0.16.20", + "rustls-webpki", + "scale-info", + "serde 1.0.193", + "serde_json 1.0.107", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "x509-cert", +] + [[package]] name = "pallet-timestamp" version = "4.0.0-dev" @@ -4065,7 +4119,7 @@ dependencies = [ "cc", "sgx_tstd", "spin", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -4078,7 +4132,7 @@ dependencies = [ "libc", "once_cell 1.18.0", "spin", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] @@ -4177,6 +4231,22 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-pki-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47003264dea418db67060fa420ad16d0d2f8f0a0360d825c00e177ac52cb5d8" + +[[package]] +name = "rustls-webpki" +version = "0.102.0-alpha.3" +source = "git+https://github.com/rustls/webpki?rev=da923ed#da923edaab56f599971e58773617fb574cd019dc" +dependencies = [ + "ring 0.16.20", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -4303,7 +4373,7 @@ source = "git+https://github.com/mesalock-linux/sct.rs?branch=mesalock_sgx#c4d85 dependencies = [ "ring 0.16.19", "sgx_tstd", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -4313,7 +4383,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.8", "generic-array 0.14.7", "subtle", "zeroize", @@ -5131,6 +5201,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "der 0.6.1", +] + [[package]] name = "ss58-registry" version = "1.43.0" @@ -5282,16 +5361,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "teerex-primitives" -version = "0.1.0" -dependencies = [ - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-std", -] - [[package]] name = "teerex-primitives" version = "0.1.0" @@ -5576,6 +5645,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.1.1" @@ -5609,9 +5684,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7daec296f25a1bae309c0cd5c29c4b260e510e6d813c286b19eaadf409d40fce" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if 1.0.0", "wasm-bindgen-macro", @@ -5619,9 +5694,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e397f4664c0e4e428e8313a469aaa58310d302159845980fd23b0f22a847f217" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -5634,9 +5709,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5961017b3b08ad5f3fe39f1e79877f8ee7c23c5e5fd5eb80de95abc41f1f16b2" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote 1.0.33", "wasm-bindgen-macro-support", @@ -5644,9 +5719,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5353b8dab669f5e10f5bd76df26a9360c748f054f862ff5f3f8aae0c7fb3907" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote 1.0.33", @@ -5657,15 +5732,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.88" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d046c5d029ba91a1ed14da14dca44b68bf2f124cfbaf741c54151fdb3e0750b" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "web-sys" -version = "0.3.65" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5db499c5f66323272151db0e666cd34f78617522fb0c1604d31a27c50c206a85" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" dependencies = [ "js-sys", "wasm-bindgen", @@ -5678,7 +5753,7 @@ source = "git+https://github.com/mesalock-linux/webpki?branch=mesalock_sgx#8dbe6 dependencies = [ "ring 0.16.19", "sgx_tstd", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -5739,6 +5814,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d224a125dec5adda27d0346b9cae9794830279c4f9c27e4ab0b6c408d54012" +dependencies = [ + "const-oid", + "der 0.6.1", + "flagset", + "spki", +] + [[package]] name = "yasna" version = "0.3.1" diff --git a/tee-worker/enclave-runtime/src/attestation.rs b/tee-worker/enclave-runtime/src/attestation.rs index 7702cd8f0f..59810d8abd 100644 --- a/tee-worker/enclave-runtime/src/attestation.rs +++ b/tee-worker/enclave-runtime/src/attestation.rs @@ -42,7 +42,7 @@ use itp_attestation_handler::{AttestationHandler, RemoteAttestationType, SgxQlQv use itp_component_container::ComponentGetter; use itp_extrinsics_factory::CreateExtrinsics; use itp_node_api::metadata::{ - pallet_teerex::TeerexCallIndexes, + pallet_teebag::TeebagCallIndexes, provider::{AccessNodeMetadata, Error as MetadataProviderError}, Error as MetadataError, }; @@ -51,7 +51,7 @@ use itp_settings::worker::MR_ENCLAVE_SIZE; use itp_sgx_crypto::{ ed25519_derivation::DeriveEd25519, key_repository::AccessKey, Error as SgxCryptoError, }; -use itp_types::OpaqueCall; +use itp_types::{AttestationType, OpaqueCall, WorkerType}; use itp_utils::write_slice_and_whitespace_pad; use log::*; use sgx_types::*; @@ -142,7 +142,8 @@ pub unsafe extern "C" fn generate_ias_ra_extrinsic( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -178,7 +179,8 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -204,7 +206,7 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic( } pub fn generate_dcap_ra_extrinsic_internal( - url: String, + url: Vec, skip_ra: bool, quoting_enclave_target_info: Option<&sgx_target_info_t>, quote_size: Option<&u32>, @@ -287,7 +289,8 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic_from_quote( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -311,7 +314,7 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic_from_quote( } pub fn generate_dcap_ra_extrinsic_from_quote_internal( - url: String, + url: Vec, quote: &[u8], ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -324,15 +327,24 @@ pub fn generate_dcap_ra_extrinsic_from_quote_internal( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; + let attestation_type = AttestationType::Dcap(Default::default()); // skip_ra should be false here already - let call = OpaqueCall::from_tuple(&(call_ids, quote, url, shielding_pubkey, vc_pubkey)); + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::Identity, + quote, + url, + shielding_pubkey, + vc_pubkey, + attestation_type, + )); info!(" [Enclave] Compose register enclave got extrinsic, returning"); create_extrinsics(call) } pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( - url: String, + url: Vec, quote: &[u8], ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -346,25 +358,34 @@ pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; - let call = OpaqueCall::from_tuple(&(call_ids, quote, url, shielding_pubkey, vc_pubkey)); + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::Identity, + quote, + url, + shielding_pubkey, + vc_pubkey, + AttestationType::Ignore, + )); info!(" [Enclave] Compose register enclave (skip-ra) got extrinsic, returning"); create_extrinsics(call) } fn generate_ias_ra_extrinsic_internal( - url: String, + url: Vec, skip_ra: bool, ) -> EnclaveResult { let attestation_handler = GLOBAL_ATTESTATION_HANDLER_COMPONENT.get()?; let cert_der = attestation_handler.generate_ias_ra_cert(skip_ra)?; - generate_ias_ra_extrinsic_from_der_cert_internal(url, &cert_der) + generate_ias_ra_extrinsic_from_der_cert_internal(url, &cert_der, skip_ra) } pub fn generate_ias_ra_extrinsic_from_der_cert_internal( - url: String, + url: Vec, cert_der: &[u8], + skip_ra: bool, ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -375,8 +396,17 @@ pub fn generate_ias_ra_extrinsic_from_der_cert_internal( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; + let attestation_type = if skip_ra { AttestationType::Ignore } else { AttestationType::Ias }; - let call = OpaqueCall::from_tuple(&(call_ids, cert_der, url, shielding_pubkey, vc_pubkey)); + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::Identity, + cert_der, + url, + shielding_pubkey, + vc_pubkey, + attestation_type, + )); create_extrinsics(call) } diff --git a/tee-worker/enclave-runtime/src/rpc/worker_api_direct.rs b/tee-worker/enclave-runtime/src/rpc/worker_api_direct.rs index 93b9370d57..e02f775f0d 100644 --- a/tee-worker/enclave-runtime/src/rpc/worker_api_direct.rs +++ b/tee-worker/enclave-runtime/src/rpc/worker_api_direct.rs @@ -339,8 +339,8 @@ where if_not_production!({ use itp_types::{MrEnclave, SidechainBlockNumber}; - // state_updateScheduledEnclave, params: sidechainBlockNumber, hex encoded mrenclave - io.add_sync_method("state_updateScheduledEnclave", move |params: Params| { + // state_setScheduledEnclave, params: sidechainBlockNumber, hex encoded mrenclave + io.add_sync_method("state_setScheduledEnclave", move |params: Params| { match params.parse::<(SidechainBlockNumber, String)>() { Ok((bn, mrenclave)) => return match hex::decode(&mrenclave) { @@ -492,8 +492,11 @@ fn forward_dcap_quote_inner(params: Params) -> Result { litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); - let ext = generate_dcap_ra_extrinsic_from_quote_internal(url, &encoded_quote_to_forward) - .map_err(|e| format!("{:?}", e))?; + let ext = generate_dcap_ra_extrinsic_from_quote_internal( + url.as_bytes().to_vec(), + &encoded_quote_to_forward, + ) + .map_err(|e| format!("{:?}", e))?; let validator_access = get_validator_accessor_from_integritee_solo_or_parachain() .map_err(|e| format!("{:?}", e))?; @@ -522,8 +525,12 @@ fn attesteer_forward_ias_attestation_report_inner( litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); - let ext = generate_ias_ra_extrinsic_from_der_cert_internal(url, &ias_attestation_report) - .map_err(|e| format!("{:?}", e))?; + let ext = generate_ias_ra_extrinsic_from_der_cert_internal( + url.as_bytes().to_vec(), + &ias_attestation_report, + false, + ) + .map_err(|e| format!("{:?}", e))?; let validator_access = get_validator_accessor_from_integritee_solo_or_parachain() .map_err(|e| format!("{:?}", e))?; diff --git a/tee-worker/enclave-runtime/src/test/fixtures/components.rs b/tee-worker/enclave-runtime/src/test/fixtures/components.rs index dd1237672d..34f3606010 100644 --- a/tee-worker/enclave-runtime/src/test/fixtures/components.rs +++ b/tee-worker/enclave-runtime/src/test/fixtures/components.rs @@ -27,7 +27,7 @@ use itp_stf_primitives::{ }; use itp_top_pool::pool::Options as PoolOptions; use itp_top_pool_author::api::SidechainApi; -use itp_types::{Block as ParentchainBlock, Enclave, ShardIdentifier}; +use itp_types::{Block as ParentchainBlock, ShardIdentifier}; use sp_core::{ed25519, Pair, H256}; use sp_runtime::traits::Header as HeaderTrait; use std::{boxed::Box, sync::Arc, vec::Vec}; @@ -41,13 +41,7 @@ pub(crate) fn create_ocall_api>( header: &Header, signer: &TestSigner, ) -> Arc { - let enclave_validateer = Enclave::new( - signer.public().into(), - Default::default(), - Default::default(), - Default::default(), - ); - Arc::new(TestOCallApi::default().add_validateer_set(header, Some(vec![enclave_validateer]))) + Arc::new(TestOCallApi::default().add_validateer_set(header, Some(vec![signer.public().into()]))) } pub(crate) fn encrypt_trusted_operation( diff --git a/tee-worker/enclave-runtime/src/test/tests_main.rs b/tee-worker/enclave-runtime/src/test/tests_main.rs index 64416fb0dd..a4e0b0e94b 100644 --- a/tee-worker/enclave-runtime/src/test/tests_main.rs +++ b/tee-worker/enclave-runtime/src/test/tests_main.rs @@ -143,7 +143,8 @@ pub extern "C" fn test_main_entrance() -> size_t { sidechain_aura_tests::produce_sidechain_block_and_import_it, sidechain_event_tests::ensure_events_get_reset_upon_block_proposal, top_pool_tests::process_indirect_call_in_top_pool, - top_pool_tests::submit_shielding_call_to_top_pool, + // TODO: Litentry disables it for now (P-494) + // top_pool_tests::submit_shielding_call_to_top_pool, // tls_ra unit tests tls_ra::seal_handler::test::seal_shielding_key_works, tls_ra::seal_handler::test::seal_shielding_key_fails_for_invalid_key, diff --git a/tee-worker/enclave-runtime/src/test/top_pool_tests.rs b/tee-worker/enclave-runtime/src/test/top_pool_tests.rs index 22776fbd39..21c86ce3a2 100644 --- a/tee-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/tee-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -48,7 +48,7 @@ use itp_node_api::{ }, metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}, }; -use itp_node_api_metadata::pallet_teerex::TeerexCallIndexes; +use itp_node_api_metadata::pallet_teebag::TeebagCallIndexes; use itp_ocall_api::EnclaveAttestationOCallApi; use itp_sgx_crypto::ShieldingCryptoEncrypt; use itp_stf_executor::enclave_signer::StfEnclaveSigner; @@ -59,9 +59,7 @@ use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, traits::AuthorApi, }; -use itp_types::{ - parentchain::Address, AccountId, Block, RsaRequest, ShardIdentifier, ShieldFundsFn, H256, -}; +use itp_types::{parentchain::Address, Block, CallWorkerFn, RsaRequest, ShardIdentifier, H256}; use jsonrpc_core::futures::executor; use litentry_primitives::Identity; use log::*; @@ -107,6 +105,10 @@ pub fn process_indirect_call_in_top_pool() { assert_eq!(1, top_pool_author.get_pending_trusted_calls(shard_id).len()); } +/* + +// TODO: use our trusted call for testing - see P-494 + pub fn submit_shielding_call_to_top_pool() { let _ = env_logger::builder().is_test(true).try_init(); @@ -159,7 +161,7 @@ pub fn submit_shielding_call_to_top_pool() { shielding_key_repo, enclave_signer, top_pool_author.clone(), node_meta_data_repository ); - let block_with_shielding_call = create_shielding_call_extrinsic(shard_id, &shielding_key); + let block_with_shielding_call = create_opaque_call_extrinsic(shard_id, &shielding_key); let _ = indirect_calls_executor .execute_indirect_calls_in_extrinsics(&block_with_shielding_call, &Vec::new()) @@ -171,6 +173,7 @@ pub fn submit_shielding_call_to_top_pool() { let trusted_call = trusted_operation.to_call().unwrap(); assert!(trusted_call.verify_signature(&mr_enclave.m, &shard_id)); } +*/ fn encrypted_indirect_call< AttestationApi: EnclaveAttestationOCallApi, @@ -194,11 +197,10 @@ fn encrypted_indirect_call< encrypt_trusted_operation(shielding_key, &trusted_operation) } -fn create_shielding_call_extrinsic( - shard: ShardIdentifier, - shielding_key: &ShieldingKey, +fn create_opaque_call_extrinsic( + _shard: ShardIdentifier, + _shielding_key: &ShieldingKey, ) -> Block { - let target_account = shielding_key.encrypt(&AccountId::new([2u8; 32]).encode()).unwrap(); let test_signer = ed25519::Pair::from_seed(b"33345678901234567890123456789012"); let signature = test_signer.sign(&[0u8]); @@ -212,15 +214,10 @@ fn create_shielding_call_extrinsic( let dummy_node_metadata = NodeMetadataMock::new(); - let shield_funds_indexes = dummy_node_metadata.shield_funds_call_indexes().unwrap(); + let call_index = dummy_node_metadata.post_opaque_task_call_indexes().unwrap(); let opaque_extrinsic = OpaqueExtrinsic::from_bytes( - ParentchainUncheckedExtrinsic::::new_signed( - ( - shield_funds_indexes, - target_account, - ita_stf::test_genesis::SECOND_ENDOWED_ACC_FUNDS, - shard, - ), + ParentchainUncheckedExtrinsic::::new_signed( + (call_index, RsaRequest::default()), Address::Address32([1u8; 32]), MultiSignature::Ed25519(signature), default_extra_for_test.signed_extra(), diff --git a/tee-worker/litentry/core/teebag-storage/Cargo.toml b/tee-worker/litentry/core/teebag-storage/Cargo.toml new file mode 100644 index 0000000000..c68b9e0d5e --- /dev/null +++ b/tee-worker/litentry/core/teebag-storage/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lc-teebag-storage" +version = "0.1.0" +authors = ['Trust Computing GmbH '] +edition = "2021" + +[dependencies] +itp-storage = { path = "../../../core-primitives/storage", default-features = false } +itp-types = { path = "../../../core-primitives/types", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42", default-features = false } + +[features] +default = ["std"] +std = [ + "sp-std/std", + "itp-storage/std", + "itp-types/std", +] diff --git a/tee-worker/litentry/core/teebag-storage/src/lib.rs b/tee-worker/litentry/core/teebag-storage/src/lib.rs new file mode 100644 index 0000000000..3f931d9f7e --- /dev/null +++ b/tee-worker/litentry/core/teebag-storage/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use itp_storage::{storage_map_key, StorageHasher}; +use itp_types::WorkerType; +use sp_std::prelude::Vec; + +pub struct TeebagStorage; + +pub trait StoragePrefix { + fn prefix() -> &'static str; +} + +impl StoragePrefix for TeebagStorage { + fn prefix() -> &'static str { + "Teebag" + } +} + +pub trait TeebagStorageKeys { + fn enclave_identifier(worker_type: WorkerType) -> Vec; +} + +impl TeebagStorageKeys for S { + fn enclave_identifier(worker_type: WorkerType) -> Vec { + storage_map_key( + Self::prefix(), + "EnclaveIdentifier", + &worker_type, + &StorageHasher::Blake2_128Concat, + ) + } +} diff --git a/tee-worker/litentry/primitives/Cargo.toml b/tee-worker/litentry/primitives/Cargo.toml index 076b5aab29..14c1d6da3e 100644 --- a/tee-worker/litentry/primitives/Cargo.toml +++ b/tee-worker/litentry/primitives/Cargo.toml @@ -31,8 +31,8 @@ sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "m itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } itp-utils = { path = "../../core-primitives/utils", default-features = false } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } +pallet-teebag = { path = "../../../pallets/teebag", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../primitives/core", default-features = false } -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } [dev-dependencies] base64 = { version = "0.13", features = ["alloc"] } @@ -59,7 +59,7 @@ std = [ "sp-runtime/std", "ring/std", "parentchain-primitives/std", - "teerex-primitives/std", + "pallet-teebag/std", "rand", "log/std", "bitcoin/std", diff --git a/tee-worker/litentry/primitives/src/aes_request.rs b/tee-worker/litentry/primitives/src/aes_request.rs index 998d642837..e15c88e40b 100644 --- a/tee-worker/litentry/primitives/src/aes_request.rs +++ b/tee-worker/litentry/primitives/src/aes_request.rs @@ -17,7 +17,7 @@ #[cfg(all(not(feature = "std"), feature = "sgx"))] extern crate sgx_tstd as std; -/// A morphling of itp_types::RsaRequest which stems from teerex_primitives::RsaRequest +/// A morphling of itp_types::RsaRequest which stems from teebag::RsaRequest /// /// Instead of encrypting the TrustedCallSigned with the TEE's shielding key, we encrypt /// it with a 32-byte ephemeral AES key which is generated from the client's side, and diff --git a/tee-worker/litentry/primitives/src/lib.rs b/tee-worker/litentry/primitives/src/lib.rs index af8137c463..82b9c77c64 100644 --- a/tee-worker/litentry/primitives/src/lib.rs +++ b/tee-worker/litentry/primitives/src/lib.rs @@ -43,6 +43,10 @@ use codec::{Decode, Encode, MaxEncodedLen}; use itp_sgx_crypto::ShieldingCryptoDecrypt; use litentry_hex_utils::hex_encode; use log::error; +pub use pallet_teebag::{ + decl_rsa_request, extract_tcb_info_from_raw_dcap_quote, AttestationType, Enclave, + EnclaveFingerprint, MrEnclave, ShardIdentifier, SidechainBlockNumber, WorkerType, +}; pub use parentchain_primitives::{ all_bitcoin_web3networks, all_evm_web3networks, all_substrate_web3networks, all_web3networks, identity::*, AccountId as ParentchainAccountId, AchainableAmount, AchainableAmountHolding, @@ -64,7 +68,6 @@ use sp_io::{ }; use sp_runtime::traits::Verify; use std::string::{String, ToString}; -pub use teerex_primitives::{decl_rsa_request, ShardIdentifier, SidechainBlockNumber}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; diff --git a/tee-worker/service/Cargo.toml b/tee-worker/service/Cargo.toml index 8363903079..c391b81179 100644 --- a/tee-worker/service/Cargo.toml +++ b/tee-worker/service/Cargo.toml @@ -78,8 +78,6 @@ litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-macros = { path = "../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } -sgx-verify = { path = "../../pallets/teerex/sgx-verify", default-features = false } -teerex-primitives = { path = "../../primitives/teerex", default-features = false } [features] default = [] diff --git a/tee-worker/service/src/main_impl.rs b/tee-worker/service/src/main_impl.rs index 3e660cefc2..a44fb49c97 100644 --- a/tee-worker/service/src/main_impl.rs +++ b/tee-worker/service/src/main_impl.rs @@ -39,7 +39,7 @@ use itp_enclave_api::{ teeracle_api::TeeracleApi, }; use itp_node_api::{ - api_client::{AccountApi, PalletTeerexApi, ParentchainApi}, + api_client::{AccountApi, PalletTeebagApi, ParentchainApi}, metadata::NodeMetadata, node_api_factory::{CreateNodeApi, NodeApiFactory}, }; @@ -51,6 +51,7 @@ use its_primitives::types::block::SignedBlock as SignedSidechainBlock; use its_storage::{interface::FetchBlocks, BlockPruner, SidechainStorageLock}; use lc_data_providers::DataProviderConfig; use litentry_macros::if_production_or; +use litentry_primitives::{Enclave as TeebagEnclave, ShardIdentifier, WorkerType}; use log::*; use my_node_runtime::{Hash, Header, RuntimeEvent}; use regex::Regex; @@ -61,10 +62,9 @@ use substrate_api_client::{ ac_primitives::serde_impls::StorageKey, api::XtStatus, rpc::HandleSubscription, storage_key, GetChainInfo, GetStorage, SubmitAndWatch, SubscribeChain, SubscribeEvents, }; -use teerex_primitives::{Enclave as TeerexEnclave, ShardIdentifier}; #[cfg(feature = "dcap")] -use sgx_verify::extract_tcb_info_from_raw_dcap_quote; +use litentry_primitives::extract_tcb_info_from_raw_dcap_quote; use itc_parentchain::primitives::ParentchainId; use itp_enclave_api::Enclave; @@ -521,86 +521,51 @@ fn start_worker( #[cfg(feature = "dcap")] let register_xt = move || enclave2.generate_dcap_ra_extrinsic(&trusted_url2, skip_ra).unwrap(); - let mut register_enclave_xt_header: Option

= None; - let mut we_are_primary_validateer: bool = false; - let send_register_xt = move || { println!("[+] Send register enclave extrinsic"); send_extrinsic(register_xt(), &node_api2, &tee_accountid2, is_development_mode) }; - // litentry: check if the enclave is already registered - // TODO: revisit the registration process (P-10) - match litentry_rpc_api.get_keys(storage_key("Teerex", "EnclaveRegistry"), None) { - Ok(Some(keys)) => { - let trusted_url = trusted_url.as_bytes().to_vec(); - let mrenclave = mrenclave.0.to_vec(); - let mut found = false; - for key in keys { - let key = if key.starts_with("0x") { - let bytes = &key.as_bytes()[b"0x".len()..]; - hex::decode(bytes).unwrap() - } else { - hex::decode(key.as_bytes()).unwrap() - }; - match litentry_rpc_api.get_storage_by_key::>>( - StorageKey(key.clone()), - None, - ) { - Ok(Some(value)) => { - if value.mr_enclave.to_vec() == mrenclave && value.url == trusted_url { - // After calling the perform_ra function, the nonce will be incremented by 1, - // so enclave is already registered, we should reset the nonce_cache - let nonce = - litentry_rpc_api.get_account_next_index(&tee_accountid).unwrap(); - enclave - .set_nonce(nonce, ParentchainId::Litentry) - .expect("Could not set nonce of enclave. Returning here..."); - found = true; - info!("fond enclave: {:?}", value); - break - } - }, - Ok(None) => { - warn!("not found from key: {:?}", key); - }, - Err(_) => {}, - } - } - if !found { - // Todo: Can't unwrap here because the extrinsic is for some reason not found in the block - // even if it was successful: https://github.com/scs/substrate-api-client/issues/624. - let register_enclave_block_hash = send_register_xt(); - let api_register_enclave_xt_header = - litentry_rpc_api.get_header(register_enclave_block_hash).unwrap().unwrap(); - - // TODO: #1451: Fix api-client type hacks - // TODO(Litentry): keep an eye on it - it's a hacky way to convert `SubstrateHeader` to `Header` - let header = - Header::decode(&mut api_register_enclave_xt_header.encode().as_slice()) - .expect("Can decode previously encoded header; qed"); - - println!( - "[+] Enclave registered at block number: {:?}, hash: {:?}", - header.number(), - header.hash() - ); + // Litentry: send the registration extrinsic regardless of being registered or not, + // the reason is the mrenclave could change in between, so we rely on the + // on-chain logic to handle everything. + // this is the same behavior as upstream + let register_enclave_block_hash = + send_register_xt().expect("enclave RA registration must be successful to continue"); + + let api_register_enclave_xt_header = + litentry_rpc_api.get_header(Some(register_enclave_block_hash)).unwrap().unwrap(); + + // TODO: #1451: Fix api-client type hacks + let register_enclave_xt_header = + Header::decode(&mut api_register_enclave_xt_header.encode().as_slice()) + .expect("Can decode previously encoded header; qed"); + + println!( + "[+] Enclave registered at block number: {:?}, hash: {:?}", + register_enclave_xt_header.number(), + register_enclave_xt_header.hash() + ); - register_enclave_xt_header = Some(header); - } - }, - _ => panic!("unknown error"), - } + // double-check + let my_enclave = litentry_rpc_api + .enclave(&tee_accountid, None) + .unwrap() + .expect("our enclave should be registered at this point"); + trace!("verified that our enclave is registered: {:?}", my_enclave); - if let Some(register_enclave_xt_header) = register_enclave_xt_header.clone() { - we_are_primary_validateer = - we_are_primary_worker(&litentry_rpc_api, ®ister_enclave_xt_header).unwrap(); - } + let is_primary_enclave = match litentry_rpc_api + .primary_enclave_identifier_for_shard(WorkerType::Identity, shard, None) + .unwrap() + { + Some(account) => account == tee_accountid, + None => false, + }; - if we_are_primary_validateer { - println!("[+] We are the primary worker"); + if is_primary_enclave { + println!("[+] We are the primary enclave"); } else { - println!("[+] We are NOT the primary worker"); + println!("[+] We are NOT the primary enclave"); } initialization_handler.registered_on_parentchain(); @@ -652,7 +617,7 @@ fn start_worker( let last_synced_header = sidechain_init_block_production( enclave.clone(), register_enclave_xt_header, - we_are_primary_validateer, + is_primary_enclave, parentchain_handler.clone(), sidechain_storage, &last_synced_header, @@ -664,7 +629,7 @@ fn start_worker( start_parentchain_header_subscription_thread(parentchain_handler, last_synced_header); - init_provided_shard_vault(shard, &enclave, we_are_primary_validateer); + init_provided_shard_vault(shard, &enclave, is_primary_enclave); spawn_worker_for_shard_polling(shard, litentry_rpc_api.clone(), initialization_handler); }, @@ -707,14 +672,14 @@ fn start_worker( fn init_provided_shard_vault( shard: &ShardIdentifier, enclave: &Arc, - we_are_primary_validateer: bool, + is_primary_enclave: bool, ) { if let Ok(shard_vault) = enclave.get_ecc_vault_pubkey(shard) { println!( "[Litentry] shard vault account is already initialized in state: {}", shard_vault.to_ss58check() ); - } else if we_are_primary_validateer { + } else if is_primary_enclave { println!("[Litentry] initializing proxied shard vault account now"); enclave.init_proxied_shard_vault(shard, &ParentchainId::Litentry).unwrap(); println!( @@ -824,9 +789,8 @@ where } /// Start polling loop to wait until we have a worker for a shard registered on -/// the parentchain (TEEREX WorkerForShard). This is the pre-requisite to be +/// the parentchain (TEEBAG EnclaveIdentifier). This is the pre-requisite to be /// considered initialized and ready for the next worker to start (in sidechain mode only). -/// considered initialized and ready for the next worker to start. fn spawn_worker_for_shard_polling( shard: &ShardIdentifier, node_api: ParentchainApi, @@ -840,7 +804,11 @@ fn spawn_worker_for_shard_polling( loop { info!("Polling for worker for shard ({} seconds interval)", POLL_INTERVAL_SECS); - if let Ok(Some(_enclave)) = node_api.worker_for_shard(&shard_for_initialized, None) { + if let Ok(Some(_account)) = node_api.primary_enclave_identifier_for_shard( + WorkerType::Identity, + &shard_for_initialized, + None, + ) { // Set that the service is initialized. initialization_handler.worker_for_shard_registered(); println!("[+] Found `WorkerForShard` on parentchain state",); @@ -1053,13 +1021,3 @@ pub fn enclave_account(enclave_api: &E) -> AccountId32 { trace!("[+] Got ed25519 account of TEE = {}", tee_public.to_ss58check()); AccountId32::from(*tee_public.as_array_ref()) } - -/// Checks if we are the first validateer to register on the parentchain. -fn we_are_primary_worker( - node_api: &ParentchainApi, - register_enclave_xt_header: &Header, -) -> Result { - let enclave_count_of_previous_block = - node_api.enclave_count(Some(*register_enclave_xt_header.parent_hash()))?; - Ok(enclave_count_of_previous_block == 0) -} diff --git a/tee-worker/service/src/sidechain_setup.rs b/tee-worker/service/src/sidechain_setup.rs index a499c85fed..55a17cdbe1 100644 --- a/tee-worker/service/src/sidechain_setup.rs +++ b/tee-worker/service/src/sidechain_setup.rs @@ -57,7 +57,7 @@ pub(crate) fn sidechain_start_untrusted_rpc_server( #[allow(clippy::too_many_arguments)] pub(crate) fn sidechain_init_block_production( enclave: Arc, - register_enclave_xt_header: Option
, + register_enclave_xt_header: Header, we_are_primary_validateer: bool, parentchain_handler: Arc, sidechain_storage: Arc, @@ -80,7 +80,7 @@ where ); updated_header = Some(parentchain_handler.sync_and_import_parentchain_until( last_synced_header, - ®ister_enclave_xt_header.unwrap(), + ®ister_enclave_xt_header, overriden_start_block, )?); } diff --git a/tee-worker/service/src/sync_state.rs b/tee-worker/service/src/sync_state.rs index 21d2d4d7e0..29bb91fd5b 100644 --- a/tee-worker/service/src/sync_state.rs +++ b/tee-worker/service/src/sync_state.rs @@ -26,15 +26,15 @@ use itp_enclave_api::{ enclave_base::EnclaveBase, remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, }; -use itp_node_api::api_client::PalletTeerexApi; +use itp_node_api::api_client::PalletTeebagApi; use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; -use itp_types::ShardIdentifier; +use itp_types::{ShardIdentifier, WorkerType}; use sgx_types::sgx_quote_sign_type_t; use std::string::String; pub(crate) fn sync_state< E: TlsRemoteAttestation + EnclaveBase + RemoteAttestation, - NodeApi: PalletTeerexApi, + NodeApi: PalletTeebagApi, WorkerModeProvider: ProvideWorkerMode, >( node_api: &NodeApi, @@ -70,30 +70,32 @@ pub(crate) fn sync_state< /// Note: The sidechainblock author will only change whenever a new parentchain block is /// produced. And even then, it might be the same as the last block. So if several workers /// are started in a timely manner, they will all get the same url. -async fn get_author_url_of_last_finalized_sidechain_block( +async fn get_author_url_of_last_finalized_sidechain_block( node_api: &NodeApi, shard: &ShardIdentifier, ) -> Result { let enclave = node_api - .worker_for_shard(shard, None)? + .primary_enclave_for_shard(WorkerType::Identity, shard, None)? .ok_or_else(|| Error::NoWorkerForShardFound(*shard))?; - let worker_api_direct = DirectWorkerApi::new(enclave.url); + let worker_api_direct = + DirectWorkerApi::new(String::from_utf8_lossy(enclave.url.as_slice()).to_string()); Ok(worker_api_direct.get_mu_ra_url()?) } /// Returns the url of the first Enclave that matches our own MRENCLAVE. /// /// This should be run before we register ourselves as enclave, to ensure we don't get our own url. -async fn get_enclave_url_of_first_registered( +async fn get_enclave_url_of_first_registered( node_api: &NodeApi, enclave_api: &EnclaveApi, ) -> Result { - let self_mr_enclave = enclave_api.get_fingerprint()?; + let self_mrenclave = enclave_api.get_fingerprint()?; let first_enclave = node_api - .all_enclaves(None)? + .all_enclaves(WorkerType::Identity, None)? .into_iter() - .find(|e| e.mr_enclave == self_mr_enclave.to_fixed_bytes()) + .find(|e| e.mrenclave == self_mrenclave.to_fixed_bytes()) .ok_or(Error::NoPeerWorkerFound)?; - let worker_api_direct = DirectWorkerApi::new(first_enclave.url); + let worker_api_direct = + DirectWorkerApi::new(String::from_utf8_lossy(first_enclave.url.as_slice()).to_string()); Ok(worker_api_direct.get_mu_ra_url()?) } diff --git a/tee-worker/service/src/tests/mock.rs b/tee-worker/service/src/tests/mock.rs index 0587669dc4..a36ae0a5ce 100644 --- a/tee-worker/service/src/tests/mock.rs +++ b/tee-worker/service/src/tests/mock.rs @@ -15,8 +15,8 @@ */ -use itp_node_api::api_client::{ApiResult, PalletTeerexApi}; -use itp_types::{Enclave, MrEnclave, ShardIdentifier, H256 as Hash}; +use itp_node_api::api_client::{ApiResult, PalletTeebagApi}; +use itp_types::{AccountId, Enclave, MrEnclave, ShardIdentifier, WorkerType, H256 as Hash}; use std::collections::HashSet; pub struct TestNodeApi; @@ -26,43 +26,44 @@ pub const W2_URL: &str = "127.0.0.1:33333"; pub fn enclaves() -> Vec { vec![ - Enclave::new([0; 32].into(), [1; 32], 1, format!("wss://{}", W1_URL)), - Enclave::new([2; 32].into(), [3; 32], 2, format!("wss://{}", W2_URL)), + Enclave::new(WorkerType::Identity).with_url(W1_URL.into()), + Enclave::new(WorkerType::Identity).with_url(W2_URL.into()), ] } -impl PalletTeerexApi for TestNodeApi { +impl PalletTeebagApi for TestNodeApi { type Hash = Hash; - fn enclave(&self, index: u64, _at_block: Option) -> ApiResult> { - Ok(Some(enclaves().remove(index as usize))) + fn enclave(&self, _account: &AccountId, _at_block: Option) -> ApiResult> { + unreachable!() } - fn enclave_count(&self, _at_block: Option) -> ApiResult { + fn enclave_count(&self, _worker_type: WorkerType, _at_block: Option) -> ApiResult { unreachable!() } - fn all_enclaves(&self, _at_block: Option) -> ApiResult> { + fn all_enclaves( + &self, + _worker_type: WorkerType, + _at_block: Option, + ) -> ApiResult> { Ok(enclaves()) } - fn worker_for_shard( + fn primary_enclave_identifier_for_shard( &self, - _: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { unreachable!() } - fn latest_ipfs_hash( + + fn primary_enclave_for_shard( &self, - _: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { unreachable!() } - - fn all_scheduled_mrenclaves(&self, _at_block: Option) -> ApiResult> { - let enclaves = enclaves(); - let mr_enclaves: HashSet<_> = enclaves.into_iter().map(|e| e.mr_enclave).collect(); - Ok(mr_enclaves.into_iter().collect()) - } } diff --git a/tee-worker/service/src/tests/mocks/enclave_api_mock.rs b/tee-worker/service/src/tests/mocks/enclave_api_mock.rs index af27dd3fae..e9b5d4c884 100644 --- a/tee-worker/service/src/tests/mocks/enclave_api_mock.rs +++ b/tee-worker/service/src/tests/mocks/enclave_api_mock.rs @@ -24,10 +24,9 @@ use itc_parentchain::primitives::{ use itp_enclave_api::{enclave_base::EnclaveBase, sidechain::Sidechain, EnclaveResult}; use itp_settings::worker::MR_ENCLAVE_SIZE; use itp_storage::StorageProof; -use itp_types::ShardIdentifier; +use itp_types::{EnclaveFingerprint, ShardIdentifier}; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sp_core::ed25519; -use teerex_primitives::EnclaveFingerprint; /// mock for EnclaveBase - use in tests pub struct EnclaveMock; diff --git a/tee-worker/service/src/worker.rs b/tee-worker/service/src/worker.rs index 638e4f081b..9f96b2a94f 100644 --- a/tee-worker/service/src/worker.rs +++ b/tee-worker/service/src/worker.rs @@ -25,13 +25,14 @@ use async_trait::async_trait; use codec::{Decode, Encode}; use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; use itp_enclave_api::enclave_base::EnclaveBase; -use itp_node_api::{api_client::PalletTeerexApi, node_api_factory::CreateNodeApi}; +use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; use its_primitives::types::SignedBlock as SignedSidechainBlock; use its_rpc_handler::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; use jsonrpsee::{ types::{to_json_value, traits::Client}, ws_client::WsClientBuilder, }; +use litentry_primitives::WorkerType; use log::*; use std::{ collections::HashSet, @@ -187,11 +188,11 @@ where .node_api_factory .create_api() .map_err(|e| Error::Custom(format!("Failed to create NodeApi: {:?}", e).into()))?; - let enclaves = node_api.all_enclaves(None)?; + let enclaves = node_api.all_enclaves(WorkerType::Identity, None)?; let mut peer_urls = HashSet::::new(); for enclave in enclaves { // FIXME: This is temporary only, as block broadcasting should be moved to trusted ws server. - let enclave_url = enclave.url.clone(); + let enclave_url = String::from_utf8_lossy(enclave.url.as_slice()).to_string(); let worker_api_direct = DirectWorkerApi::new(enclave_url.clone()); match worker_api_direct.get_untrusted_worker_url() { Ok(untrusted_worker_url) => { diff --git a/tee-worker/sidechain/consensus/aura/src/lib.rs b/tee-worker/sidechain/consensus/aura/src/lib.rs index 2ca8b88edb..4114852c73 100644 --- a/tee-worker/sidechain/consensus/aura/src/lib.rs +++ b/tee-worker/sidechain/consensus/aura/src/lib.rs @@ -47,7 +47,7 @@ use its_primitives::{ use its_validateer_fetch::ValidateerFetch; use lc_scheduled_enclave::ScheduledEnclaveUpdater; use litentry_hex_utils::hex_encode; -use sp_core::ByteArray; +use sp_core::crypto::UncheckedFrom; use sp_runtime::{ app_crypto::{sp_core::H256, Pair}, generic::SignedBlock as SignedParentchainBlock, @@ -189,6 +189,7 @@ impl< StateHandler, > where AuthorityPair: Pair, + AuthorityPair::Public: UncheckedFrom<[u8; 32]>, // todo: Relax hash trait bound, but this needs a change to some other parts in the code. ParentchainBlock: ParentchainBlockTrait, E: Environment, @@ -222,7 +223,6 @@ impl< fn epoch_data( &self, header: &ParentchainBlock::Header, - _shard: ShardIdentifierFor, _slot: Slot, ) -> Result { authorities::<_, AuthorityPair, ParentchainBlock::Header>(&self.ocall_api, header) @@ -391,13 +391,14 @@ fn authorities( where ValidateerFetcher: ValidateerFetch + EnclaveOnChainOCallApi, P: Pair, + P::Public: UncheckedFrom<[u8; 32]>, ParentchainHeader: ParentchainHeaderTrait, { Ok(ocall_api - .current_validateers(header) + .current_validateers::(header) .map_err(|e| ConsensusError::CouldNotGetAuthorities(e.to_string()))? - .into_iter() - .filter_map(|e| AuthorityId::

::from_slice(e.pubkey.as_ref()).ok()) + .iter() + .map(|account| P::Public::unchecked_from(*account.as_ref())) .collect()) } @@ -411,14 +412,14 @@ pub enum AnyImportTrigger { mod tests { use super::*; use crate::test::{ - fixtures::{types::TestAura, validateer, SLOT_DURATION}, + fixtures::{types::TestAura, SLOT_DURATION}, mocks::environment_mock::{EnvironmentMock, OutdatedBlockEnvironmentMock}, }; use itc_parentchain_block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; use itc_parentchain_test::{ParentchainBlockBuilder, ParentchainHeaderBuilder}; use itp_test::mock::{handle_state_mock::HandleStateMock, onchain_mock::OnchainMock}; use itp_types::{ - Block as ParentchainBlock, Enclave, Header as ParentchainHeader, ShardIdentifier, + AccountId, Block as ParentchainBlock, Header as ParentchainHeader, ShardIdentifier, SignedBlock as SignedParentchainBlock, }; use its_consensus_slots::PerShardSlotWorkerScheduler; @@ -483,8 +484,8 @@ mod tests { vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()] } - fn create_validateer_set_from_publics(authorities: Vec) -> Vec { - authorities.iter().map(|a| validateer(a.clone().into())).collect() + fn create_validateer_set_from_publics(authorities: Vec) -> Vec { + authorities.iter().map(|a| AccountId::from(a.clone())).collect() } fn onchain_mock( diff --git a/tee-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs b/tee-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs index 447aa18c62..be23004cf0 100644 --- a/tee-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs +++ b/tee-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs @@ -87,10 +87,10 @@ fn test_fixtures( ) -> (TestBlockImporter, Arc, Arc) { let state_handler = Arc::new(HandleStateMock::from_shard(shard()).unwrap()); let top_pool_author = Arc::new(TestTopPoolAuthor::default()); - let ocall_api = Arc::new(OnchainMock::default().add_validateer_set( - parentchain_header, - Some(vec![validateer(Keyring::Alice.public().into())]), - )); + let ocall_api = Arc::new( + OnchainMock::default() + .add_validateer_set(parentchain_header, Some(vec![Keyring::Alice.public().into()])), + ); let state_key_repository = Arc::new(TestStateKeyRepo::new(state_key())); let peer_updater_mock = Arc::new(PeerUpdaterMock {}); diff --git a/tee-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs b/tee-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs index 54d47324fa..de85405549 100644 --- a/tee-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs +++ b/tee-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs @@ -23,5 +23,5 @@ use std::time::Duration; pub const SLOT_DURATION: Duration = Duration::from_millis(300); pub fn validateer(account: AccountId) -> Enclave { - Enclave::new(account, Default::default(), Default::default(), Default::default()) + Enclave::default() } diff --git a/tee-worker/sidechain/consensus/aura/src/verifier.rs b/tee-worker/sidechain/consensus/aura/src/verifier.rs index 0c1f64b138..121cef58eb 100644 --- a/tee-worker/sidechain/consensus/aura/src/verifier.rs +++ b/tee-worker/sidechain/consensus/aura/src/verifier.rs @@ -24,6 +24,7 @@ use its_primitives::{ types::block::BlockHash, }; use its_validateer_fetch::ValidateerFetch; +use sp_core::crypto::UncheckedFrom; use sp_runtime::{app_crypto::Pair, traits::Block as ParentchainBlockTrait}; use std::{fmt::Debug, time::Duration}; @@ -57,7 +58,7 @@ impl for AuraVerifier where AuthorityPair: Pair, - AuthorityPair::Public: Debug, + AuthorityPair::Public: Debug + UncheckedFrom<[u8; 32]>, // todo: Relax hash trait bound, but this needs a change to some other parts in the code. ParentchainBlock: ParentchainBlockTrait, SignedSidechainBlock: SignedSidechainBlockTrait + 'static, diff --git a/tee-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs b/tee-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs index be93feb51c..ee2b5f3ede 100644 --- a/tee-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs +++ b/tee-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs @@ -20,7 +20,7 @@ use itc_parentchain_light_client::{ concurrent_access::ValidatorAccess, BlockNumberOps, ExtrinsicSender, NumberFor, }; use itp_extrinsics_factory::CreateExtrinsics; -use itp_node_api_metadata::{pallet_sidechain::SidechainCallIndexes, NodeMetadataTrait}; +use itp_node_api_metadata::{pallet_teebag::TeebagCallIndexes, NodeMetadataTrait}; use itp_node_api_metadata_provider::AccessNodeMetadata; use itp_settings::worker::BLOCK_NUMBER_FINALIZATION_DIFF; use itp_types::{OpaqueCall, ShardIdentifier}; @@ -102,7 +102,7 @@ impl< fn confirm_import(&self, header: &SidechainHeader, shard: &ShardIdentifier) -> Result<()> { let call = self .metadata_repository - .get_from_metadata(|m| m.confirm_imported_sidechain_block_indexes()) + .get_from_metadata(|m| m.sidechain_block_imported_call_indexes()) .map_err(|e| Error::Other(e.into()))? .map_err(|e| Error::Other(format!("{:?}", e).into()))?; diff --git a/tee-worker/sidechain/consensus/slots/src/lib.rs b/tee-worker/sidechain/consensus/slots/src/lib.rs index e09e55b1d6..c27f9d2450 100644 --- a/tee-worker/sidechain/consensus/slots/src/lib.rs +++ b/tee-worker/sidechain/consensus/slots/src/lib.rs @@ -206,7 +206,6 @@ pub trait SimpleSlotWorker { fn epoch_data( &self, header: &ParentchainBlock::Header, - shard: ShardIdentifierFor, slot: Slot, ) -> Result; @@ -329,7 +328,7 @@ pub trait SimpleSlotWorker { maybe_latest_target_b_parentchain_header.clone().map(|h| *h.number()) ); - let epoch_data = match self.epoch_data(&latest_integritee_parentchain_header, shard, slot) { + let epoch_data = match self.epoch_data(&latest_integritee_parentchain_header, slot) { Ok(epoch_data) => epoch_data, Err(e) => { warn!( diff --git a/tee-worker/sidechain/consensus/slots/src/mocks.rs b/tee-worker/sidechain/consensus/slots/src/mocks.rs index c84467640d..72d6f9fee8 100644 --- a/tee-worker/sidechain/consensus/slots/src/mocks.rs +++ b/tee-worker/sidechain/consensus/slots/src/mocks.rs @@ -70,12 +70,7 @@ where todo!() } - fn epoch_data( - &self, - _header: &B::Header, - _shard: ShardIdentifierFor, - _slot: Slot, - ) -> Result { + fn epoch_data(&self, _header: &B::Header, _slot: Slot) -> Result { todo!() } diff --git a/tee-worker/sidechain/peer-fetch/Cargo.toml b/tee-worker/sidechain/peer-fetch/Cargo.toml index 63e2612d91..66c10302c9 100644 --- a/tee-worker/sidechain/peer-fetch/Cargo.toml +++ b/tee-worker/sidechain/peer-fetch/Cargo.toml @@ -16,6 +16,7 @@ thiserror = { version = "1.0" } # local itc-rpc-client = { path = "../../core/rpc-client" } itp-node-api = { path = "../../core-primitives/node-api" } +itp-types = { path = "../../core-primitives/types" } its-primitives = { path = "../primitives" } its-rpc-handler = { path = "../rpc-handler" } its-storage = { path = "../storage" } diff --git a/tee-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs b/tee-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs index 7ff9434103..488590f761 100644 --- a/tee-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs +++ b/tee-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs @@ -17,7 +17,8 @@ use crate::error::{Error, Result}; use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; -use itp_node_api::{api_client::PalletTeerexApi, node_api_factory::CreateNodeApi}; +use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; +use itp_types::WorkerType; use its_primitives::types::ShardIdentifier; use std::sync::Arc; @@ -50,10 +51,11 @@ where let node_api = self.node_api_factory.create_api()?; let validateer = node_api - .worker_for_shard(shard, None)? + .primary_enclave_for_shard(WorkerType::Identity, shard, None)? .ok_or_else(|| Error::NoPeerFoundForShard(*shard))?; - let trusted_worker_client = DirectWorkerApi::new(validateer.url); + let trusted_worker_client = + DirectWorkerApi::new(String::from_utf8_lossy(validateer.url.as_slice()).to_string()); Ok(trusted_worker_client.get_untrusted_worker_url()?) } } diff --git a/tee-worker/sidechain/validateer-fetch/Cargo.toml b/tee-worker/sidechain/validateer-fetch/Cargo.toml index 7aca2dbf7a..2df88400d5 100644 --- a/tee-worker/sidechain/validateer-fetch/Cargo.toml +++ b/tee-worker/sidechain/validateer-fetch/Cargo.toml @@ -15,11 +15,8 @@ sp-std = { default-features = false, git = "https://github.com/paritytech/substr # local deps itp-ocall-api = { path = "../../core-primitives/ocall-api", default-features = false } -itp-teerex-storage = { path = "../../core-primitives/teerex-storage", default-features = false } itp-types = { path = "../../core-primitives/types", default-features = false } - -# litentry -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +lc-teebag-storage = { path = "../../litentry/core/teebag-storage", default-features = false } [features] default = ["std"] @@ -30,7 +27,7 @@ std = [ "sp-std/std", "itp-types/std", "itp-ocall-api/std", - "frame-support/std", + "lc-teebag-storage/std", ] [dev-dependencies] diff --git a/tee-worker/sidechain/validateer-fetch/src/validateer.rs b/tee-worker/sidechain/validateer-fetch/src/validateer.rs index 4af8d86274..37373c0f32 100644 --- a/tee-worker/sidechain/validateer-fetch/src/validateer.rs +++ b/tee-worker/sidechain/validateer-fetch/src/validateer.rs @@ -16,10 +16,9 @@ */ use crate::error::{Error, Result}; -use frame_support::ensure; use itp_ocall_api::EnclaveOnChainOCallApi; -use itp_teerex_storage::{TeeRexStorage, TeerexStorageKeys}; -use itp_types::{parentchain::ParentchainId, Enclave}; +use itp_types::{parentchain::ParentchainId, AccountId, WorkerType}; +use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; use sp_core::H256; use sp_runtime::traits::Header as HeaderT; use sp_std::prelude::Vec; @@ -28,7 +27,8 @@ pub trait ValidateerFetch { fn current_validateers>( &self, latest_header: &Header, - ) -> Result>; + ) -> Result>; + fn validateer_count>(&self, latest_header: &Header) -> Result; } @@ -37,31 +37,21 @@ impl ValidateerFetch for OnchainStorage fn current_validateers>( &self, header: &Header, - ) -> Result> { - let count = self.validateer_count(header)?; - - let mut hashes = Vec::with_capacity(count as usize); - for i in 1..=count { - hashes.push(TeeRexStorage::enclave(i)) - } - - let enclaves: Vec = self - .get_multiple_storages_verified(hashes, header, &ParentchainId::Litentry)? - .into_iter() - .filter_map(|e| e.into_tuple().1) - .collect(); - ensure!( - enclaves.len() == count as usize, - Error::Other("Found less validateers onchain than validateer count") - ); - Ok(enclaves) + ) -> Result> { + let identifiers = self + .get_storage_verified( + TeebagStorage::enclave_identifier(WorkerType::Identity), + header, + &ParentchainId::Litentry, + )? + .into_tuple() + .1 + .ok_or_else(|| Error::Other("Could not get validateer list from chain"))?; + Ok(identifiers) } fn validateer_count>(&self, header: &Header) -> Result { - self.get_storage_verified(TeeRexStorage::enclave_count(), header, &ParentchainId::Litentry)? - .into_tuple() - .1 - .ok_or_else(|| Error::Other("Could not get validateer count from chain")) + Ok(self.current_validateers::

(header)?.len() as u64) } } @@ -84,21 +74,7 @@ mod tests { pub fn get_validateer_set_works() { let header = ParentchainHeaderBuilder::default().build(); let mock = OnchainMock::default().add_validateer_set(&header, None); - let validateers = validateer_set(); - assert_eq!(mock.current_validateers(&header).unwrap(), validateers); } - - #[test] - pub fn if_validateer_count_bigger_than_returned_validateers_return_err() { - let header = ParentchainHeaderBuilder::default().build(); - let mut mock = OnchainMock::default().add_validateer_set(&header, None); - mock.insert_at_header(&header, TeeRexStorage::enclave_count(), 5u64.encode()); - - assert_eq!( - mock.current_validateers(&header).unwrap_err().to_string(), - "Found less validateers onchain than validateer count".to_string() - ); - } } diff --git a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts index 54e4c91906..7a28ebf5a4 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts @@ -6,7 +6,7 @@ import { assert, expect } from 'chai'; import * as ed from '@noble/ed25519'; import { parseIdGraph } from './identity-helper'; import type { PalletIdentityManagementTeeError } from 'sidechain-api'; -import { TeerexPrimitivesEnclave, CorePrimitivesIdentity } from 'parachain-api'; +import { PalletTeebagEnclave, CorePrimitivesIdentity } from 'parachain-api'; import type { IntegrationTestContext } from '../common-types'; import { getIdGraphHash } from '../di-utils'; import type { HexString } from '@polkadot/util/types'; @@ -135,14 +135,17 @@ export async function checkErrorDetail(events: Event[], expectedDetail: string) } export async function verifySignature(data: any, index: HexString, proofJson: any, api: ApiPromise) { - const count = await api.query.teerex.enclaveCount(); - const enclaveRegistry = (await api.query.teerex.enclaveRegistry(count)) as unknown as TeerexPrimitivesEnclave; + const enclaveIdentifier = api.createType('Vec', await api.query.teebag.enclaveIdentifier('Identity')); + const primaryEnclave = ( + await api.query.teebag.enclaveRegistry(enclaveIdentifier[0]) + ).toHuman() as unknown as PalletTeebagEnclave; // Check vc index expect(index).to.be.eq(data.id); const signature = Buffer.from(hexToU8a(`0x${proofJson.proofValue}`)); const message = Buffer.from(data.issuer.mrenclave); - const vcPubkey = Buffer.from(hexToU8a(enclaveRegistry.toHuman()['vcPubkey'] as HexString)); + const vcPubkeyBytes = api.createType('Option', primaryEnclave.vcPubkey).unwrap(); + const vcPubkey = Buffer.from(hexToU8a(vcPubkeyBytes.toHex())); const isValid = await ed.verify(signature, message, vcPubkey); @@ -277,19 +280,20 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri const { proof, ...vcWithoutProof } = vcPayloadJson; // step 5 - // prepare teerex enclave registry data for further checks + // prepare teebag enclave registry data for further checks const parachainBlockHash = await context.api.query.system.blockHash(vcPayloadJson.parachainBlockNumber); const apiAtVcIssuedBlock = await context.api.at(parachainBlockHash); - const enclaveCount = await apiAtVcIssuedBlock.query.teerex.enclaveCount(); - - const lastRegisteredEnclave = (await apiAtVcIssuedBlock.query.teerex.enclaveRegistry(enclaveCount)) - .value as TeerexPrimitivesEnclave; + const enclaveIdentifier = await apiAtVcIssuedBlock.query.teebag.enclaveIdentifier('Identity'); + const lastRegisteredEnclave = ( + await apiAtVcIssuedBlock.query.teebag.enclaveRegistry(enclaveIdentifier[enclaveIdentifier.length - 1]) + ).unwrap(); // step 6 // check vc signature const signature = Buffer.from(hexToU8a(`0x${proof.proofValue}`)); const message = Buffer.from(JSON.stringify(vcWithoutProof)); - const vcPubkey = Buffer.from(lastRegisteredEnclave.vcPubkey.value); + const vcPubkeyBytes = context.api.createType('Option', lastRegisteredEnclave.vcPubkey).unwrap(); + const vcPubkey = Buffer.from(hexToU8a(vcPubkeyBytes.toHex())); const signatureStatus = await ed.verify(signature, message, vcPubkey); assert.isTrue(signatureStatus, 'Check Vc signature error: signature should be valid'); @@ -297,7 +301,7 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri // step 7 // check VC mrenclave with enclave's mrenclave from registry assert.equal( - base58.encode(lastRegisteredEnclave.mrEnclave), + base58.encode(lastRegisteredEnclave.mrenclave), vcPayloadJson.issuer.mrenclave, 'Check VC mrenclave: it should equals enclaves mrenclave from parachains enclave registry' ); @@ -305,7 +309,7 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri // step 8 // check vc issuer id assert.equal( - `did:litentry:substrate:${lastRegisteredEnclave.vcPubkey.value.toHex()}`, + `did:litentry:substrate:${vcPubkeyBytes.toHex()}`, vcPayloadJson.issuer.id, 'Check VC id: it should equals enclaves pubkey from parachains enclave registry' ); diff --git a/tee-worker/ts-tests/integration-tests/common/utils/context.ts b/tee-worker/ts-tests/integration-tests/common/utils/context.ts index 2903aa1227..4b9d5d9d26 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/context.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/context.ts @@ -1,6 +1,7 @@ -import { WsProvider, ApiPromise, TeerexPrimitivesEnclave } from 'parachain-api'; +import { WsProvider, ApiPromise } from 'parachain-api'; import { Keyring } from '@polkadot/api'; import { cryptoWaitReady } from '@polkadot/util-crypto'; +import { hexToString } from '@polkadot/util'; import { ethers } from 'ethers'; import WebSocketAsPromised from 'websocket-as-promised'; import WebSocket from 'ws'; @@ -85,28 +86,24 @@ export async function getEnclave(api: ApiPromise): Promise<{ mrEnclave: HexString; teeShieldingKey: KeyObject; }> { - const count = await api.query.teerex.enclaveCount(); + const enclaveIdentifier = api.createType('Vec', await api.query.teebag.enclaveIdentifier('Identity')); + const primaryEnclave = (await api.query.teebag.enclaveRegistry(enclaveIdentifier[0])).unwrap(); - const enclaveRegistry = ( - await api.query.teerex.enclaveRegistry(count) - ).toHuman() as unknown as TeerexPrimitivesEnclave; + const shieldingPubkeyBytes = api.createType('Option', primaryEnclave.shieldingPubkey).unwrap(); + const shieldingPubkey = hexToString(shieldingPubkeyBytes.toHex()); const teeShieldingKey = crypto.createPublicKey({ key: { alg: 'RSA-OAEP-256', kty: 'RSA', use: 'enc', - n: Buffer.from(JSON.parse(enclaveRegistry.shieldingKey as unknown as HexString).n.reverse()).toString( - 'base64url' - ), - e: Buffer.from(JSON.parse(enclaveRegistry.shieldingKey as unknown as HexString).e.reverse()).toString( - 'base64url' - ), + n: Buffer.from(JSON.parse(shieldingPubkey).n.reverse()).toString('base64url'), + e: Buffer.from(JSON.parse(shieldingPubkey).e.reverse()).toString('base64url'), }, format: 'jwk', }); //@TODO mrEnclave should verify from storage - const mrEnclave = enclaveRegistry.mrEnclave as unknown as HexString; + const mrEnclave = primaryEnclave.mrenclave.toHex(); return { mrEnclave, teeShieldingKey, diff --git a/tee-worker/ts-tests/integration-tests/vc.issuer.attest.example.ts b/tee-worker/ts-tests/integration-tests/vc.issuer.attest.example.ts index 3e25a7b1b7..1945a06f92 100644 --- a/tee-worker/ts-tests/integration-tests/vc.issuer.attest.example.ts +++ b/tee-worker/ts-tests/integration-tests/vc.issuer.attest.example.ts @@ -69,11 +69,10 @@ const vc = { * */ const enclaveRegistry = { - pubkey: 'jcPcHgptXWGsTAefDqW7GpbX8LYrNVEYLLKihV3RsizqSga1Z', - mrEnclave: '0x168b47aceff04e8cd20f4606a7eb255ffc1981cd3b8ba1d44face858f9a4c25b', - timestamp: '1,677,164,874,078', + mrenclave: '0x168b47aceff04e8cd20f4606a7eb255ffc1981cd3b8ba1d44face858f9a4c25b', + lastSeen: '1,677,164,874,078', url: 'wss://localhost:2000', - shieldingKey: + shieldingPubkey: '{n:[189,64,222,165,185,105,241,193,170,87,19,231,76,95,247,110,231,7,196,65,135,231,55,75,60,58,158,23,77,230,154,23,203,167,163,219,18,113,83,23,172,131,29,222,200,73,217,159,155,120,217,194,74,33,79,99,88,227,2,242,225,141,116,231,89,68,119,109,183,56,135,70,151,177,245,199,196,222,193,33,28,47,252,83,240,120,238,81,99,154,219,75,84,108,96,199,108,42,64,70,217,164,89,81,156,188,168,181,169,228,21,140,90,18,126,77,50,31,19,149,26,86,160,108,197,78,134,19,54,25,89,80,239,106,95,226,42,109,202,54,158,128,224,243,1,197,209,131,48,1,208,207,48,197,66,44,203,76,113,150,100,73,81,17,94,153,217,11,14,193,230,43,207,24,236,200,207,15,63,16,75,173,191,245,127,191,186,18,111,111,90,24,177,167,177,7,61,94,60,161,130,242,31,210,158,152,31,35,202,80,179,138,219,244,139,19,60,134,108,94,151,228,224,22,29,139,21,241,71,221,65,145,210,108,80,0,76,137,98,128,107,224,16,32,135,232,168,150,9,225,30,120,17,176,26,2,8,100,185,121,158,67,89,110,130,126,122,113,248,169,73,27,52,90,109,66,249,255,161,105,174,129,163,7,14,180,63,178,218,81,86,108,116,118,81,185,248,231,84,150,13,140,49,239,103,44,119,97,37,30,13,230,100,73,24,229,178,3,89,14,26,155,245,254,12,152,7,72,209,209,24,224,5,131,144,124,254,204,209,138,57,196,176,244,231,185,190,187,118,215,46,45,57,81,238,163,11,152,73,217,252,9,77,95,86,4,201,34,68,88,235,103,15,120,159,134,5,182,83,122,128,111,160,141],e:[1,0,0,1]}', vcPubkey: '0xde17d6daedb66ec9f5e096cc0317bd6cbf881c0d8273e54105ee7c22a2e48648', sgxMode: 'Debug', @@ -95,7 +94,7 @@ function checkIssuerAttestation() { console.log(' [IssuerAttestation] mrEnclaveFromVc: ', mrEnclaveFromVc); // Fetch mrEnclave from parachain - const mrEnclaveFromParachain = enclaveRegistry.mrEnclave; + const mrEnclaveFromParachain = enclaveRegistry.mrenclave; console.log(' [IssuerAttestation] mrEnclaveFromParachain: ', mrEnclaveFromParachain); // >>>0. Check mrEnclave diff --git a/tee-worker/ts-tests/stress/src/litentry-api.ts b/tee-worker/ts-tests/stress/src/litentry-api.ts index af3b425aed..7f8e9f79a1 100644 --- a/tee-worker/ts-tests/stress/src/litentry-api.ts +++ b/tee-worker/ts-tests/stress/src/litentry-api.ts @@ -8,6 +8,7 @@ import { u8aToHex, u8aConcat, stringToU8a, + hexToString, } from '@polkadot/util'; import { blake2AsHex } from '@polkadot/util-crypto'; import crypto, { KeyObject, createPublicKey } from 'crypto'; @@ -341,27 +342,25 @@ export async function getEnclave(api: ParachainApiPromise): Promise<{ mrEnclave: `0x${string}`; teeShieldingKey: KeyObject; }> { - const count = await api.query.teerex.enclaveCount(); + const enclaveIdentifier = api.createType('Vec', await api.query.teebag.enclaveIdentifier('Identity')); + const primaryEnclave = (await api.query.teebag.enclaveRegistry(enclaveIdentifier[0])).unwrap(); - const res = (await api.query.teerex.enclaveRegistry(count)).toHuman() as { - mrEnclave: `0x${string}`; - shieldingKey: `0x${string}`; - vcPubkey: `0x${string}`; - sgxMetadata: object; - }; + const shieldingPubkeyBytes = api.createType('Option', primaryEnclave.shieldingPubkey).unwrap(); + const shieldingPubkey = hexToString(shieldingPubkeyBytes.toHex()); const teeShieldingKey = crypto.createPublicKey({ key: { alg: 'RSA-OAEP-256', kty: 'RSA', use: 'enc', - n: Buffer.from(JSON.parse(res.shieldingKey).n.reverse()).toString('base64url'), - e: Buffer.from(JSON.parse(res.shieldingKey).e.reverse()).toString('base64url'), + n: Buffer.from(JSON.parse(shieldingPubkey).n.reverse()).toString('base64url'), + e: Buffer.from(JSON.parse(shieldingPubkey).e.reverse()).toString('base64url'), }, format: 'jwk', }); //@TODO mrEnclave should verify from storage - const mrEnclave = res.mrEnclave; + const mrEnclave = primaryEnclave.mrenclave.toHex(); + return { mrEnclave, teeShieldingKey, diff --git a/tee-worker/ts-tests/worker/resuming_worker.test.ts b/tee-worker/ts-tests/worker/resuming_worker.test.ts index f5a7f49c1d..0e3060de9d 100644 --- a/tee-worker/ts-tests/worker/resuming_worker.test.ts +++ b/tee-worker/ts-tests/worker/resuming_worker.test.ts @@ -141,7 +141,7 @@ async function spawnWorkerJob( let shard: HexString | undefined = undefined; const job = spawn( - `./litentry-worker`, + `RUST_LOG=info ./litentry-worker`, [generateWorkerCommandArguments(command, nodeConfig, workerConfig)], { cwd: workingDir, diff --git a/ts-tests/common/setup/setup-enclave.ts b/ts-tests/common/setup/setup-enclave.ts index 36faf3db7b..818236a978 100644 --- a/ts-tests/common/setup/setup-enclave.ts +++ b/ts-tests/common/setup/setup-enclave.ts @@ -11,17 +11,17 @@ async function setAliceAsAdmin(api: ApiPromise, config: any) { const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice'); - const tx = api.tx.sudo.sudo(api.tx.teerex.setAdmin('esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5')); + const tx = api.tx.sudo.sudo(api.tx.teebag.setAdmin('esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5')); - console.log(`Setting Alice as Admin for Teerex`); + console.log(`Setting Alice as Admin for Teebag`); return signAndSend(tx, alice); } -async function updateScheduledEnclave(api: ApiPromise, config: any) { +async function setScheduledEnclave(api: ApiPromise, config: any) { const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice'); - const tx = api.tx.teerex.updateScheduledEnclave(block, hexToU8a(`0x${mrenclave}`)); + const tx = api.tx.teebag.setScheduledEnclave('Identity', block, hexToU8a(`0x${mrenclave}`)); console.log('Schedule Enclave Extrinsic sent'); return signAndSend(tx, alice); @@ -37,7 +37,7 @@ async function updateScheduledEnclave(api: ApiPromise, config: any) { }); await setAliceAsAdmin(api, config); - await updateScheduledEnclave(api, config); + await setScheduledEnclave(api, config); await api.disconnect(); provider.on('disconnected', () => { diff --git a/ts-tests/common/setup/skip-schedule-enclave-check.ts b/ts-tests/common/setup/teebag-set-dev-mode.ts similarity index 68% rename from ts-tests/common/setup/skip-schedule-enclave-check.ts rename to ts-tests/common/setup/teebag-set-dev-mode.ts index 47e150d6d5..fc41a5c1dd 100644 --- a/ts-tests/common/setup/skip-schedule-enclave-check.ts +++ b/ts-tests/common/setup/teebag-set-dev-mode.ts @@ -1,29 +1,25 @@ import '@polkadot/api-augment'; import { ApiPromise, Keyring, WsProvider } from '@polkadot/api'; import { loadConfig, signAndSend } from '../utils'; -import { hexToU8a } from '@polkadot/util'; - -const mrenclave = process.argv[2]; -const block = process.argv[3]; async function setAliceAsAdmin(api: ApiPromise, config: any) { // Get keyring of Alice, who is also the sudo in dev chain spec const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice'); - const tx = api.tx.sudo.sudo(api.tx.teerex.setAdmin('esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5')); + const tx = api.tx.sudo.sudo(api.tx.teebag.setAdmin('esqZdrqhgH8zy1wqYh1aLKoRyoRWLFbX9M62eKfaTAoK67pJ5')); - console.log(`Setting Alice as Admin for Teerex`); + console.log(`Setting Alice as Admin for Teebag`); return signAndSend(tx, alice); } -async function updateSkipScheduledEnclaveCheck(api: ApiPromise, config: any) { +async function setDevelopmentMode(api: ApiPromise, config: any) { const keyring = new Keyring({ type: 'sr25519' }); const alice = keyring.addFromUri('//Alice'); - const tx = api.tx.teerex.setSkipScheduledEnclaveCheck(true); + const tx = api.tx.teebag.setMode('Development'); - console.log('set Skip Schedule Enclave Extrinsic sent'); + console.log('set development mode Extrinsic sent'); return signAndSend(tx, alice); } @@ -37,7 +33,7 @@ async function updateSkipScheduledEnclaveCheck(api: ApiPromise, config: any) { }); await setAliceAsAdmin(api, config); - await updateSkipScheduledEnclaveCheck(api, config); + await setDevelopmentMode(api, config); await api.disconnect(); provider.on('disconnected', () => { From 5b9b21edb70044ff0e5b45c1a2b0a2c92ec3c14f Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Sat, 10 Feb 2024 10:19:07 +0100 Subject: [PATCH 20/64] Cleanup: remove IMP/VCMP stuff from bc-worker (#2477) * license update * init * fix some more --- .../indirect_calls/litentry/args_executor.rs | 2 +- .../src/indirect_calls/litentry/mod.rs | 2 +- .../litentry/scheduled_enclave.rs | 2 +- bitacross-worker/app-libs/stf/src/helpers.rs | 40 ------- .../app-libs/stf/src/trusted_call.rs | 56 +--------- .../app-libs/stf/src/trusted_call_result.rs | 3 +- .../core/bc-task-receiver/src/lib.rs | 3 - .../cli/src/base_cli/commands/litentry/mod.rs | 2 +- .../litentry/set_heartbeat_timeout.rs | 2 +- .../node-api/metadata/src/lib.rs | 13 +-- .../node-api/metadata/src/metadata_mocks.rs | 101 ++---------------- .../node-api/metadata/src/pallet_bitacross.rs | 2 +- .../node-api/metadata/src/pallet_imp.rs | 71 ------------ .../node-api/metadata/src/pallet_utility.rs | 2 +- .../node-api/metadata/src/pallet_vcmp.rs | 42 -------- .../node-api/metadata/src/runtime_call.rs | 2 +- .../stf-executor/src/executor.rs | 5 +- .../stf-interface/src/runtime_upgrade.rs | 2 +- .../stf-primitives/src/error.rs | 71 +----------- .../core-primitives/types/src/lib.rs | 18 +--- .../core/direct-rpc-client/src/lib.rs | 2 +- .../indirect-calls-executor/src/error.rs | 21 ---- .../core/peer-top-broadcaster/src/lib.rs | 2 +- .../src/test/mocks/peer_updater_mock.rs | 2 +- .../core/scheduled-enclave/src/error.rs | 2 +- .../litentry/core/scheduled-enclave/src/io.rs | 2 +- .../core/scheduled-enclave/src/lib.rs | 2 +- bitacross-worker/litentry/macros/src/lib.rs | 2 +- .../litentry/primitives/src/aes.rs | 2 +- .../litentry/primitives/src/aes_request.rs | 2 +- .../primitives/src/bitcoin_signature.rs | 2 +- .../primitives/src/ethereum_signature.rs | 2 +- .../litentry/primitives/src/lib.rs | 19 +--- .../primitives/src/validation_data.rs | 2 +- .../service/src/prometheus_metrics.rs | 1 - .../node-api/metadata/src/metadata_mocks.rs | 2 +- .../core/mock-server/src/litentry_archive.rs | 2 +- 37 files changed, 48 insertions(+), 462 deletions(-) delete mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_imp.rs delete mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_vcmp.rs diff --git a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/args_executor.rs b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/args_executor.rs index 05084bfee0..3b9c018b7c 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/args_executor.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/args_executor.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/mod.rs b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/mod.rs index 830f452476..763319b821 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/mod.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs index a0d1ff65bc..f2ae695c9e 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/app-libs/stf/src/helpers.rs b/bitacross-worker/app-libs/stf/src/helpers.rs index a0d3fa75b9..639133470b 100644 --- a/bitacross-worker/app-libs/stf/src/helpers.rs +++ b/bitacross-worker/app-libs/stf/src/helpers.rs @@ -16,14 +16,10 @@ */ use crate::ENCLAVE_ACCOUNT_KEY; use codec::{Decode, Encode}; -use frame_support::ensure; use itp_stf_primitives::error::{StfError, StfResult}; use itp_storage::{storage_double_map_key, storage_map_key, storage_value_key, StorageHasher}; -use itp_types::Index; use itp_utils::stringify::account_id_to_string; -use litentry_primitives::{ErrorDetail, Identity, Web3ValidationData}; use log::*; -use sp_core::blake2_256; use std::prelude::v1::*; #[cfg(not(feature = "production"))] @@ -126,42 +122,6 @@ pub fn ensure_enclave_signer_or_self( } } -// verification message format: -// ``` -// blake2_256( + + ) -// ``` -// where <> means SCALE-encoded -// see https://github.com/litentry/litentry-parachain/issues/1739 and P-174 -pub fn get_expected_raw_message( - who: &Identity, - identity: &Identity, - sidechain_nonce: Index, -) -> Vec { - let mut payload = Vec::new(); - payload.append(&mut sidechain_nonce.encode()); - payload.append(&mut who.encode()); - payload.append(&mut identity.encode()); - blake2_256(payload.as_slice()).to_vec() -} - -pub fn verify_web3_identity( - identity: &Identity, - raw_msg: &[u8], - data: &Web3ValidationData, -) -> StfResult<()> { - ensure!( - raw_msg == data.message().as_slice(), - StfError::LinkIdentityFailed(ErrorDetail::UnexpectedMessage) - ); - - ensure!( - data.signature().verify(raw_msg, identity), - StfError::LinkIdentityFailed(ErrorDetail::VerifyWeb3SignatureFailed) - ); - - Ok(()) -} - #[cfg(not(feature = "production"))] mod non_prod { use super::*; diff --git a/bitacross-worker/app-libs/stf/src/trusted_call.rs b/bitacross-worker/app-libs/stf/src/trusted_call.rs index 2ebeb97741..ace42c1f56 100644 --- a/bitacross-worker/app-libs/stf/src/trusted_call.rs +++ b/bitacross-worker/app-libs/stf/src/trusted_call.rs @@ -35,8 +35,8 @@ use ita_sgx_runtime::{AddressMapping, HashedAddressMapping}; pub use ita_sgx_runtime::{Balance, Index, Runtime, System}; use itp_node_api::metadata::{provider::AccessNodeMetadata, NodeMetadataTrait}; use itp_node_api_metadata::{ - pallet_balances::BalancesCallIndexes, pallet_imp::IMPCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_teerex::TeerexCallIndexes, pallet_vcmp::VCMPCallIndexes, + pallet_balances::BalancesCallIndexes, pallet_proxy::ProxyCallIndexes, + pallet_teerex::TeerexCallIndexes, }; use itp_stf_interface::{ExecuteCall, SHARD_VAULT_KEY}; pub use itp_stf_primitives::{ @@ -51,9 +51,8 @@ use itp_types::{ pub use itp_types::{OpaqueCall, H256}; use itp_utils::stringify::account_id_to_string; pub use litentry_primitives::{ - aes_encrypt_default, all_evm_web3networks, all_substrate_web3networks, AesOutput, Assertion, - ErrorDetail, IMPError, Identity, LitentryMultiSignature, ParentchainBlockNumber, RequestAesKey, - RequestAesKeyNonce, VCMPError, ValidationData, Web3Network, + aes_encrypt_default, AesOutput, Identity, LitentryMultiSignature, ParentchainBlockNumber, + RequestAesKey, RequestAesKeyNonce, ValidationData, }; use log::*; use sp_core::{ @@ -609,53 +608,6 @@ where pallet_sudo::Pallet::::key().map_or(false, |k| account == &k) } -pub fn push_call_imp_some_error( - calls: &mut Vec, - node_metadata_repo: Arc, - identity: Option, - e: IMPError, - req_ext_hash: H256, -) where - NodeMetadataRepository: AccessNodeMetadata, - NodeMetadataRepository::MetadataType: NodeMetadataTrait, -{ - debug!("pushing IMP::some_error call ..."); - // TODO: anyway to simplify this? `and_then` won't be applicable here - match node_metadata_repo.get_from_metadata(|m| m.imp_some_error_call_indexes()) { - Ok(Ok(call_index)) => calls.push(ParentchainCall::Litentry(OpaqueCall::from_tuple(&( - call_index, - identity, - e, - req_ext_hash, - )))), - Ok(e) => warn!("error getting IMP::some_error call indexes: {:?}", e), - Err(e) => warn!("error getting IMP::some_error call indexes: {:?}", e), - } -} - -pub fn push_call_vcmp_some_error( - calls: &mut Vec, - node_metadata_repo: Arc, - identity: Option, - e: VCMPError, - req_ext_hash: H256, -) where - NodeMetadataRepository: AccessNodeMetadata, - NodeMetadataRepository::MetadataType: NodeMetadataTrait, -{ - debug!("pushing VCMP::some_error call ..."); - match node_metadata_repo.get_from_metadata(|m| m.vcmp_some_error_call_indexes()) { - Ok(Ok(call_index)) => calls.push(ParentchainCall::Litentry(OpaqueCall::from_tuple(&( - call_index, - identity, - e, - req_ext_hash, - )))), - Ok(e) => warn!("error getting VCMP::some_error call indexes: {:?}", e), - Err(e) => warn!("error getting VCMP::some_error call indexes: {:?}", e), - } -} - #[cfg(test)] mod tests { use super::*; diff --git a/bitacross-worker/app-libs/stf/src/trusted_call_result.rs b/bitacross-worker/app-libs/stf/src/trusted_call_result.rs index f9cbbeff95..60e303ce3b 100644 --- a/bitacross-worker/app-libs/stf/src/trusted_call_result.rs +++ b/bitacross-worker/app-libs/stf/src/trusted_call_result.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify @@ -16,7 +16,6 @@ // This file contain the RPC response struct which will be encoded and // passed back to the requester of trustedCall direct invocation (DI). -// They are mostly translated from the callback extrinsics in IMP. use codec::{Decode, Encode}; use itp_stf_interface::StfExecutionResult; diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs index ce76c7bb92..286c210efa 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs @@ -41,9 +41,6 @@ pub enum Error { #[error("Request error: {0}")] RequestError(String), - #[error("Assertion error: {0}")] - AssertionError(String), - #[error("Other error: {0}")] OtherError(String), } diff --git a/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs b/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs index b5700e1258..4a7cec7cd4 100644 --- a/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs +++ b/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs b/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs index f4efb49ae7..996b369a59 100644 --- a/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs +++ b/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs index 813d1ef979..a1bc01d865 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs @@ -21,10 +21,9 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, - pallet_imp::IMPCallIndexes, pallet_proxy::ProxyCallIndexes, - pallet_sidechain::SidechainCallIndexes, pallet_system::SystemSs58Prefix, - pallet_teerex::TeerexCallIndexes, pallet_utility::UtilityCallIndexes, - pallet_vcmp::VCMPCallIndexes, + pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, + pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, + pallet_utility::UtilityCallIndexes, }; use codec::{Decode, Encode}; use sp_core::storage::StorageKey; @@ -35,13 +34,11 @@ pub use itp_api_client_types::{Metadata, MetadataError}; pub mod error; pub mod pallet_balances; pub mod pallet_bitacross; -pub mod pallet_imp; pub mod pallet_proxy; pub mod pallet_sidechain; pub mod pallet_system; pub mod pallet_teerex; pub mod pallet_utility; -pub mod pallet_vcmp; pub mod runtime_call; #[cfg(feature = "mocks")] @@ -50,8 +47,6 @@ pub mod metadata_mocks; pub trait NodeMetadataTrait: TeerexCallIndexes + SidechainCallIndexes - + IMPCallIndexes - + VCMPCallIndexes + SystemSs58Prefix + UtilityCallIndexes + ProxyCallIndexes @@ -62,8 +57,6 @@ pub trait NodeMetadataTrait: impl< T: TeerexCallIndexes + SidechainCallIndexes - + IMPCallIndexes - + VCMPCallIndexes + SystemSs58Prefix + UtilityCallIndexes + ProxyCallIndexes diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index a1289c489c..0a899df8ca 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -17,10 +17,9 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, - pallet_imp::IMPCallIndexes, pallet_proxy::ProxyCallIndexes, - pallet_sidechain::SidechainCallIndexes, pallet_system::SystemSs58Prefix, - pallet_teerex::TeerexCallIndexes, pallet_utility::UtilityCallIndexes, - pallet_vcmp::VCMPCallIndexes, runtime_call::RuntimeCall, + pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, + pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, + pallet_utility::UtilityCallIndexes, runtime_call::RuntimeCall, }; use codec::{Decode, Encode}; @@ -42,6 +41,8 @@ pub struct NodeMetadataMock { unregister_proxied_enclave: u8, register_quoting_enclave: u8, register_tcb_info: u8, + update_scheduled_enclave: u8, + remove_scheduled_enclave: u8, enclave_bridge_module: u8, invoke: u8, confirm_processed_parentchain_block: u8, @@ -50,25 +51,6 @@ pub struct NodeMetadataMock { publish_hash: u8, update_shard_config: u8, sidechain_module: u8, - // litentry - update_scheduled_enclave: u8, - remove_scheduled_enclave: u8, - // IMP - imp_module: u8, - imp_link_identity: u8, - imp_deactivate_identity: u8, - imp_activate_identity: u8, - imp_update_id_graph_hash: u8, - imp_identity_linked: u8, - imp_identity_deactivated: u8, - imp_identity_activated: u8, - imp_identity_networks_set: u8, - imp_some_error: u8, - // VCMP - vcmp_module: u8, - vcmp_request_vc: u8, - vcmp_vc_issued: u8, - vcmp_some_error: u8, utility_module: u8, utility_batch: u8, @@ -101,6 +83,8 @@ impl NodeMetadataMock { unregister_proxied_enclave: 2u8, register_quoting_enclave: 3, register_tcb_info: 4, + update_scheduled_enclave: 5, + remove_scheduled_enclave: 6, enclave_bridge_module: 54u8, invoke: 0u8, confirm_processed_parentchain_block: 1u8, @@ -109,25 +93,6 @@ impl NodeMetadataMock { publish_hash: 4u8, update_shard_config: 5u8, sidechain_module: 53u8, - // litentry - update_scheduled_enclave: 10u8, - remove_scheduled_enclave: 11u8, - - imp_module: 64u8, - imp_link_identity: 1u8, - imp_deactivate_identity: 2u8, - imp_activate_identity: 3u8, - imp_update_id_graph_hash: 4u8, - imp_identity_linked: 6u8, - imp_identity_deactivated: 7u8, - imp_identity_activated: 8u8, - imp_identity_networks_set: 9u8, - imp_some_error: 10u8, - - vcmp_module: 66u8, - vcmp_request_vc: 0u8, - vcmp_vc_issued: 3u8, - vcmp_some_error: 9u8, utility_module: 80u8, utility_batch: 0u8, @@ -213,58 +178,6 @@ impl SidechainCallIndexes for NodeMetadataMock { } } -impl IMPCallIndexes for NodeMetadataMock { - fn link_identity_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_link_identity]) - } - - fn deactivate_identity_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_deactivate_identity]) - } - - fn activate_identity_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_activate_identity]) - } - - fn update_id_graph_hash_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_update_id_graph_hash]) - } - - fn identity_linked_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_identity_linked]) - } - - fn identity_deactivated_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_identity_deactivated]) - } - - fn identity_activated_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_identity_activated]) - } - - fn identity_networks_set_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_identity_networks_set]) - } - - fn imp_some_error_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.imp_module, self.imp_some_error]) - } -} - -impl VCMPCallIndexes for NodeMetadataMock { - fn request_vc_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.vcmp_module, self.vcmp_request_vc]) - } - - fn vc_issued_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.vcmp_module, self.vcmp_vc_issued]) - } - - fn vcmp_some_error_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.vcmp_module, self.vcmp_some_error]) - } -} - impl UtilityCallIndexes for NodeMetadataMock { fn batch_call_indexes(&self) -> Result<[u8; 2]> { Ok([self.utility_module, self.utility_batch]) diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs index a7e5a033ee..74af1d46ff 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_imp.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_imp.rs deleted file mode 100644 index 636d93cdab..0000000000 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_imp.rs +++ /dev/null @@ -1,71 +0,0 @@ -// Copyright 2020-2023 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -// TODO: maybe use macros to simplify this -use crate::{error::Result, NodeMetadata}; - -/// Pallet' name: -const IMP: &str = "IdentityManagement"; - -pub trait IMPCallIndexes { - fn link_identity_call_indexes(&self) -> Result<[u8; 2]>; - fn deactivate_identity_call_indexes(&self) -> Result<[u8; 2]>; - fn activate_identity_call_indexes(&self) -> Result<[u8; 2]>; - fn update_id_graph_hash_call_indexes(&self) -> Result<[u8; 2]>; - fn identity_linked_call_indexes(&self) -> Result<[u8; 2]>; - fn identity_deactivated_call_indexes(&self) -> Result<[u8; 2]>; - fn identity_activated_call_indexes(&self) -> Result<[u8; 2]>; - fn identity_networks_set_call_indexes(&self) -> Result<[u8; 2]>; - fn imp_some_error_call_indexes(&self) -> Result<[u8; 2]>; -} - -impl IMPCallIndexes for NodeMetadata { - fn link_identity_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "link_identity") - } - - fn deactivate_identity_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "deactivate_identity") - } - - fn activate_identity_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "activate_identity") - } - - fn update_id_graph_hash_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "update_id_graph_hash") - } - - fn identity_linked_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "identity_linked") - } - - fn identity_deactivated_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "identity_deactivated") - } - - fn identity_activated_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "identity_activated") - } - - fn identity_networks_set_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "identity_networks_set") - } - - fn imp_some_error_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(IMP, "some_error") - } -} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_utility.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_utility.rs index 909e4a7d30..0eeef1339a 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_utility.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_utility.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_vcmp.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_vcmp.rs deleted file mode 100644 index 210d55e74f..0000000000 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_vcmp.rs +++ /dev/null @@ -1,42 +0,0 @@ -// Copyright 2020-2023 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -// TODO: maybe use macros to simplify this -use crate::{error::Result, NodeMetadata}; - -const VCMP: &str = "VCManagement"; - -pub trait VCMPCallIndexes { - fn request_vc_call_indexes(&self) -> Result<[u8; 2]>; - - fn vc_issued_call_indexes(&self) -> Result<[u8; 2]>; - - fn vcmp_some_error_call_indexes(&self) -> Result<[u8; 2]>; -} - -impl VCMPCallIndexes for NodeMetadata { - fn request_vc_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(VCMP, "request_vc") - } - - fn vc_issued_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(VCMP, "vc_issued") - } - - fn vcmp_some_error_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(VCMP, "some_error") - } -} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/runtime_call.rs b/bitacross-worker/core-primitives/node-api/metadata/src/runtime_call.rs index a484e6f779..8fa69cc9ad 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/runtime_call.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/runtime_call.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core-primitives/stf-executor/src/executor.rs b/bitacross-worker/core-primitives/stf-executor/src/executor.rs index a980563774..42a2f9b6e9 100644 --- a/bitacross-worker/core-primitives/stf-executor/src/executor.rs +++ b/bitacross-worker/core-primitives/stf-executor/src/executor.rs @@ -21,7 +21,6 @@ use crate::{ BatchExecutionResult, ExecutedOperation, }; use codec::{Decode, Encode}; -use itp_enclave_metrics::EnclaveMetric; use itp_node_api::metadata::{provider::AccessNodeMetadata, NodeMetadataTrait}; use itp_ocall_api::{EnclaveAttestationOCallApi, EnclaveMetricsOCallApi, EnclaveOnChainOCallApi}; use itp_sgx_externalities::{SgxExternalitiesTrait, StateHash}; @@ -43,8 +42,8 @@ use itp_types::{ use log::*; use sp_runtime::traits::Header as HeaderTrait; use std::{ - collections::BTreeMap, fmt::Debug, marker::PhantomData, string::ToString, sync::Arc, - time::Duration, vec, vec::Vec, + collections::BTreeMap, fmt::Debug, marker::PhantomData, sync::Arc, time::Duration, vec, + vec::Vec, }; pub struct StfExecutor diff --git a/bitacross-worker/core-primitives/stf-interface/src/runtime_upgrade.rs b/bitacross-worker/core-primitives/stf-interface/src/runtime_upgrade.rs index 30ee22140e..649ba34ca5 100644 --- a/bitacross-worker/core-primitives/stf-interface/src/runtime_upgrade.rs +++ b/bitacross-worker/core-primitives/stf-interface/src/runtime_upgrade.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core-primitives/stf-primitives/src/error.rs b/bitacross-worker/core-primitives/stf-primitives/src/error.rs index c69514f109..a4fa844a88 100644 --- a/bitacross-worker/core-primitives/stf-primitives/src/error.rs +++ b/bitacross-worker/core-primitives/stf-primitives/src/error.rs @@ -15,10 +15,9 @@ */ use crate::types::{AccountId, Nonce}; -use alloc::{format, string::String}; +use alloc::string::String; use codec::{Decode, Encode}; use derive_more::Display; -use litentry_primitives::{Assertion, ErrorDetail, ErrorString, IMPError, VCMPError}; pub type StfResult = Result; @@ -45,74 +44,6 @@ pub enum StfError { InvalidStorageDiff, #[codec(index = 7)] InvalidMetadata, - // litentry #[codec(index = 8)] - #[display(fmt = "LinkIdentityFailed: {:?}", _0)] - LinkIdentityFailed(ErrorDetail), - #[codec(index = 9)] - #[display(fmt = "DeactivateIdentityFailed: {:?}", _0)] - DeactivateIdentityFailed(ErrorDetail), - #[codec(index = 10)] - #[display(fmt = "ActivateIdentityFailed: {:?}", _0)] - ActivateIdentityFailed(ErrorDetail), - #[codec(index = 11)] - #[display(fmt = "RequestVCFailed: {:?} {:?}", _0, _1)] - RequestVCFailed(Assertion, ErrorDetail), - #[codec(index = 12)] - SetScheduledMrEnclaveFailed, - #[codec(index = 13)] - #[display(fmt = "SetIdentityNetworksFailed: {:?}", _0)] - SetIdentityNetworksFailed(ErrorDetail), - #[codec(index = 14)] InvalidAccount, - #[codec(index = 15)] - UnclassifiedError, - #[codec(index = 16)] - #[display(fmt = "RemovingIdentityFailed: {:?}", _0)] - RemoveIdentityFailed(ErrorDetail), - #[codec(index = 17)] - EmptyIDGraph, -} - -impl From for StfError { - fn from(e: IMPError) -> Self { - match e { - IMPError::LinkIdentityFailed(d) => StfError::LinkIdentityFailed(d), - IMPError::DeactivateIdentityFailed(d) => StfError::DeactivateIdentityFailed(d), - IMPError::ActivateIdentityFailed(d) => StfError::ActivateIdentityFailed(d), - _ => StfError::UnclassifiedError, - } - } -} - -impl From for StfError { - fn from(e: VCMPError) -> Self { - match e { - VCMPError::RequestVCFailed(a, d) => StfError::RequestVCFailed(a, d), - _ => StfError::UnclassifiedError, - } - } -} - -impl StfError { - // Convert StfError to IMPError that would be sent to parentchain - pub fn to_imp_error(&self) -> IMPError { - match self { - StfError::LinkIdentityFailed(d) => IMPError::LinkIdentityFailed(d.clone()), - StfError::DeactivateIdentityFailed(d) => IMPError::DeactivateIdentityFailed(d.clone()), - StfError::ActivateIdentityFailed(d) => IMPError::ActivateIdentityFailed(d.clone()), - _ => IMPError::UnclassifiedError(ErrorDetail::StfError(ErrorString::truncate_from( - format!("{:?}", self).as_bytes().to_vec(), - ))), - } - } - // Convert StfError to VCMPError that would be sent to parentchain - pub fn to_vcmp_error(&self) -> VCMPError { - match self { - StfError::RequestVCFailed(a, d) => VCMPError::RequestVCFailed(a.clone(), d.clone()), - _ => VCMPError::UnclassifiedError(ErrorDetail::StfError(ErrorString::truncate_from( - format!("{:?}", self).as_bytes().to_vec(), - ))), - } - } } diff --git a/bitacross-worker/core-primitives/types/src/lib.rs b/bitacross-worker/core-primitives/types/src/lib.rs index 911282e427..cdaeaa5685 100644 --- a/bitacross-worker/core-primitives/types/src/lib.rs +++ b/bitacross-worker/core-primitives/types/src/lib.rs @@ -21,7 +21,7 @@ use crate::storage::StorageEntry; use codec::{Decode, Encode}; use itp_sgx_crypto::ShieldingCryptoDecrypt; -use litentry_primitives::{decl_rsa_request, RequestAesKeyNonce}; +use litentry_primitives::decl_rsa_request; use sp_std::{boxed::Box, fmt::Debug, vec::Vec}; pub mod parentchain; @@ -37,7 +37,7 @@ pub type PalletString = Vec; pub type PalletString = String; pub use itp_sgx_runtime_primitives::types::*; -pub use litentry_primitives::{Assertion, DecryptableRequest}; +pub use litentry_primitives::DecryptableRequest; pub use sp_core::{crypto::AccountId32 as AccountId, H256}; pub type IpfsHash = [u8; 46]; @@ -53,20 +53,6 @@ pub type CallWorkerFn = (CallIndex, RsaRequest); pub type UpdateScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); pub type RemoveScheduledEnclaveFn = (CallIndex, SidechainBlockNumber); -// pallet IMP -pub type LinkIdentityParams = (ShardIdentifier, AccountId, Vec, Vec, RequestAesKeyNonce); -pub type LinkIdentityFn = (CallIndex, LinkIdentityParams); - -pub type DeactivateIdentityParams = (ShardIdentifier, Vec); -pub type DeactivateIdentityFn = (CallIndex, DeactivateIdentityParams); - -pub type ActivateIdentityParams = (ShardIdentifier, Vec); -pub type ActivateIdentityFn = (CallIndex, DeactivateIdentityParams); - -// pallet VCMP -pub type RequestVCParams = (ShardIdentifier, Assertion); -pub type RequestVCFn = (CallIndex, RequestVCParams); - pub type Enclave = EnclaveGen; /// Simple blob to hold an encoded call diff --git a/bitacross-worker/core/direct-rpc-client/src/lib.rs b/bitacross-worker/core/direct-rpc-client/src/lib.rs index 6de127f7df..ba5253fdf7 100644 --- a/bitacross-worker/core/direct-rpc-client/src/lib.rs +++ b/bitacross-worker/core/direct-rpc-client/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/src/error.rs b/bitacross-worker/core/parentchain/indirect-calls-executor/src/error.rs index 2973f984f8..cbb80d384e 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/src/error.rs +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/src/error.rs @@ -17,7 +17,6 @@ #[cfg(all(not(feature = "std"), feature = "sgx"))] use crate::sgx_reexport_prelude::*; -pub use litentry_primitives::{ErrorDetail, IMPError, VCMPError}; use itp_types::parentchain::ParentchainError; use lc_scheduled_enclave::error::Error as ScheduledEnclaveError; @@ -44,14 +43,6 @@ pub enum Error { Other(#[from] Box), #[error("AccountId lookup error")] AccountIdLookup, - #[error("convert parent chain block number error")] - ConvertParentchainBlockNumber, - #[error("IMP handling error: {0:?}")] - IMPHandlingError(IMPError), - #[error("VCMP handling error: {0:?}")] - VCMPHandlingError(VCMPError), - #[error("BatchAll handling error")] - BatchAllHandlingError, #[error("ScheduledEnclave Error: {0:?}")] ImportScheduledEnclave(ScheduledEnclaveError), } @@ -92,18 +83,6 @@ impl From for Error { } } -impl From for Error { - fn from(e: IMPError) -> Self { - Self::IMPHandlingError(e) - } -} - -impl From for Error { - fn from(e: VCMPError) -> Self { - Self::VCMPHandlingError(e) - } -} - impl From for Error { fn from(e: ScheduledEnclaveError) -> Self { Self::ImportScheduledEnclave(e) diff --git a/bitacross-worker/core/peer-top-broadcaster/src/lib.rs b/bitacross-worker/core/peer-top-broadcaster/src/lib.rs index eef091de21..9e34033741 100644 --- a/bitacross-worker/core/peer-top-broadcaster/src/lib.rs +++ b/bitacross-worker/core/peer-top-broadcaster/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs b/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs index 63a60108df..2fe4526c43 100644 --- a/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs +++ b/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/core/scheduled-enclave/src/error.rs b/bitacross-worker/litentry/core/scheduled-enclave/src/error.rs index 6353db15f5..70228b33a0 100644 --- a/bitacross-worker/litentry/core/scheduled-enclave/src/error.rs +++ b/bitacross-worker/litentry/core/scheduled-enclave/src/error.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/core/scheduled-enclave/src/io.rs b/bitacross-worker/litentry/core/scheduled-enclave/src/io.rs index 9912fe4a6f..7aad2bc57d 100644 --- a/bitacross-worker/litentry/core/scheduled-enclave/src/io.rs +++ b/bitacross-worker/litentry/core/scheduled-enclave/src/io.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/core/scheduled-enclave/src/lib.rs b/bitacross-worker/litentry/core/scheduled-enclave/src/lib.rs index e71edfb88a..ddb47e2e3d 100644 --- a/bitacross-worker/litentry/core/scheduled-enclave/src/lib.rs +++ b/bitacross-worker/litentry/core/scheduled-enclave/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/macros/src/lib.rs b/bitacross-worker/litentry/macros/src/lib.rs index b57ac19473..f76bbe03c0 100644 --- a/bitacross-worker/litentry/macros/src/lib.rs +++ b/bitacross-worker/litentry/macros/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/primitives/src/aes.rs b/bitacross-worker/litentry/primitives/src/aes.rs index d63b02432a..8abbb7b149 100644 --- a/bitacross-worker/litentry/primitives/src/aes.rs +++ b/bitacross-worker/litentry/primitives/src/aes.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/primitives/src/aes_request.rs b/bitacross-worker/litentry/primitives/src/aes_request.rs index 7c133429e2..998d642837 100644 --- a/bitacross-worker/litentry/primitives/src/aes_request.rs +++ b/bitacross-worker/litentry/primitives/src/aes_request.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/primitives/src/bitcoin_signature.rs b/bitacross-worker/litentry/primitives/src/bitcoin_signature.rs index cb6db71a23..689e088fbc 100644 --- a/bitacross-worker/litentry/primitives/src/bitcoin_signature.rs +++ b/bitacross-worker/litentry/primitives/src/bitcoin_signature.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/primitives/src/ethereum_signature.rs b/bitacross-worker/litentry/primitives/src/ethereum_signature.rs index 75496fa61d..e0869efd08 100644 --- a/bitacross-worker/litentry/primitives/src/ethereum_signature.rs +++ b/bitacross-worker/litentry/primitives/src/ethereum_signature.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/litentry/primitives/src/lib.rs b/bitacross-worker/litentry/primitives/src/lib.rs index baceb51c69..9ac4ac0dd1 100644 --- a/bitacross-worker/litentry/primitives/src/lib.rs +++ b/bitacross-worker/litentry/primitives/src/lib.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify @@ -44,17 +44,10 @@ use itp_sgx_crypto::ShieldingCryptoDecrypt; use litentry_hex_utils::hex_encode; use log::error; pub use parentchain_primitives::{ - all_bitcoin_web3networks, all_evm_web3networks, all_substrate_web3networks, all_web3networks, - identity::*, AccountId as ParentchainAccountId, AchainableAmount, AchainableAmountHolding, - AchainableAmountToken, AchainableAmounts, AchainableBasic, AchainableBetweenPercents, - AchainableClassOfYear, AchainableDate, AchainableDateInterval, AchainableDatePercent, - AchainableMirror, AchainableParams, AchainableToken, AmountHoldingTimeType, Assertion, - Balance as ParentchainBalance, BlockNumber as ParentchainBlockNumber, BnbDigitDomainType, - BoundedWeb3Network, ContestType, EVMTokenType, ErrorDetail, ErrorString, - GenericDiscordRoleType, Hash as ParentchainHash, Header as ParentchainHeader, IMPError, - Index as ParentchainIndex, IntoErrorDetail, OneBlockCourseType, ParameterString, - SchemaContentString, SchemaIdString, Signature as ParentchainSignature, SoraQuizType, - VCMPError, VIP3MembershipCardLevel, Web3Network, MINUTES, + identity::*, AccountId as ParentchainAccountId, Balance as ParentchainBalance, + BlockNumber as ParentchainBlockNumber, ErrorDetail, ErrorString, Hash as ParentchainHash, + Header as ParentchainHeader, Index as ParentchainIndex, ParameterString, + Signature as ParentchainSignature, MINUTES, }; use scale_info::TypeInfo; use sp_core::{ecdsa, ed25519, sr25519, ByteArray}; @@ -228,8 +221,6 @@ fn evm_eip191_wrap(msg: &[u8]) -> Vec { ["\x19Ethereum Signed Message:\n".as_bytes(), msg.len().to_string().as_bytes(), msg].concat() } -pub type IdentityNetworkTuple = (Identity, Vec); - // Represent a request that can be decrypted by the enclave // Both itp_types::RsaRequest and AesRequest should impelement this pub trait DecryptableRequest { diff --git a/bitacross-worker/litentry/primitives/src/validation_data.rs b/bitacross-worker/litentry/primitives/src/validation_data.rs index aac3427799..0b9eb19001 100644 --- a/bitacross-worker/litentry/primitives/src/validation_data.rs +++ b/bitacross-worker/litentry/primitives/src/validation_data.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify diff --git a/bitacross-worker/service/src/prometheus_metrics.rs b/bitacross-worker/service/src/prometheus_metrics.rs index 55f0edbfc8..7ce1befc3a 100644 --- a/bitacross-worker/service/src/prometheus_metrics.rs +++ b/bitacross-worker/service/src/prometheus_metrics.rs @@ -34,7 +34,6 @@ use itc_rest_client::{ }; use itp_enclave_metrics::EnclaveMetric; use lazy_static::lazy_static; -use litentry_primitives::{Assertion, Identity}; use log::*; use prometheus::{ proto::MetricFamily, register_counter_vec, register_histogram, register_histogram_vec, diff --git a/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index 458e893759..d5b22c96b5 100644 --- a/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/tee-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -1,7 +1,7 @@ /* Copyright 2021 Integritee AG and Supercomputing Systems AG - Licensed under the Apache License, Version 2.0 (the "License); + 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 diff --git a/tee-worker/litentry/core/mock-server/src/litentry_archive.rs b/tee-worker/litentry/core/mock-server/src/litentry_archive.rs index f256fc7d41..65f98b3b90 100644 --- a/tee-worker/litentry/core/mock-server/src/litentry_archive.rs +++ b/tee-worker/litentry/core/mock-server/src/litentry_archive.rs @@ -1,4 +1,4 @@ -// Copyright 2020-2023 Trust Computing GmbH. +// Copyright 2020-2024 Trust Computing GmbH. // This file is part of Litentry. // // Litentry is free software: you can redistribute it and/or modify From 67799c7f4360e60f6bba0ab1fd8ac12a8a31d679 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Sat, 10 Feb 2024 11:58:57 +0100 Subject: [PATCH 21/64] BitAcross direct calls (#2474) * signing direct calls * handling signing requests * revert account changes * break cyclic dep * direct call result aes encryption * import cleanup * uncomment precomit check * one more cleanup * review suggestions --- bitacross-worker/Cargo.lock | 90 +++++---- bitacross-worker/Cargo.toml | 1 + .../core/bc-task-receiver/Cargo.toml | 7 + .../core/bc-task-receiver/src/lib.rs | 107 +++++++++-- bitacross-worker/cli/Cargo.toml | 1 + .../bitacross/direct_call_sign_bitcoin.rs | 57 ++++++ .../bitacross/direct_call_sign_ethereum.rs | 57 ++++++ .../commands/bitacross/mod.rs | 20 ++ .../commands/bitacross/utils.rs | 68 +++++++ .../cli/src/trusted_base_cli/commands/mod.rs | 1 + .../cli/src/trusted_base_cli/mod.rs | 20 +- .../core-primitives/sgx/crypto/Cargo.toml | 2 +- .../sgx/crypto/src/{secp256k1.rs => ecdsa.rs} | 52 +++-- .../core-primitives/sgx/crypto/src/lib.rs | 3 +- .../core-primitives/sgx/crypto/src/schnorr.rs | 178 ++++++++++++++++++ bitacross-worker/enclave-runtime/Cargo.lock | 31 ++- bitacross-worker/enclave-runtime/Cargo.toml | 1 + .../src/initialization/global_components.rs | 7 +- .../enclave-runtime/src/initialization/mod.rs | 19 +- .../enclave-runtime/src/test/tests_main.rs | 2 +- .../litentry/core/direct-call/Cargo.toml | 40 ++++ .../litentry/core/direct-call/src/lib.rs | 74 ++++++++ primitives/core/proc-macros/src/lib.rs | 2 +- scripts/pre-commit.sh | 2 +- 24 files changed, 743 insertions(+), 99 deletions(-) create mode 100644 bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_bitcoin.rs create mode 100644 bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs create mode 100644 bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/mod.rs create mode 100644 bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/utils.rs rename bitacross-worker/core-primitives/sgx/crypto/src/{secp256k1.rs => ecdsa.rs} (78%) create mode 100644 bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs create mode 100644 bitacross-worker/litentry/core/direct-call/Cargo.toml create mode 100644 bitacross-worker/litentry/core/direct-call/src/lib.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 6695ba9514..b6e7ef5f12 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -213,7 +213,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" dependencies = [ "getrandom 0.2.10", - "once_cell 1.18.0", + "once_cell 1.19.0", "version_check", ] @@ -225,7 +225,7 @@ checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" dependencies = [ "cfg-if 1.0.0", "getrandom 0.2.10", - "once_cell 1.18.0", + "once_cell 1.19.0", "version_check", ] @@ -613,6 +613,8 @@ dependencies = [ "itp-top-pool-author", "itp-types", "itp-utils", + "lc-direct-call", + "litentry-macros", "litentry-primitives", "log 0.4.20", "parity-scale-codec", @@ -718,6 +720,7 @@ dependencies = [ "itp-time-utils", "itp-types", "itp-utils", + "lc-direct-call", "litentry-primitives", "log 0.4.20", "pallet-balances", @@ -1325,7 +1328,7 @@ dependencies = [ "clap_derive", "clap_lex", "is-terminal", - "once_cell 1.18.0", + "once_cell 1.19.0", "strsim 0.10.0", "termcolor", ] @@ -1359,7 +1362,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a90d114103adbc625300f346d4d09dfb4ab1c4a8df6868435dd903392ecf4354" dependencies = [ "libc", - "once_cell 1.18.0", + "once_cell 1.19.0", "wasi 0.11.0+wasi-snapshot-preview1", "wasm-bindgen", ] @@ -2056,7 +2059,7 @@ checksum = "5c0c11acd0e63bae27dcd2afced407063312771212b7a823b4fd72d633be30fb" dependencies = [ "cc", "codespan-reporting", - "once_cell 1.18.0", + "once_cell 1.19.0", "proc-macro2", "quote", "scratch", @@ -2438,7 +2441,7 @@ checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ "der 0.7.8", "digest 0.10.7", - "elliptic-curve 0.13.5", + "elliptic-curve 0.13.8", "rfc6979 0.4.0", "signature 2.1.0", "spki 0.7.2", @@ -2511,9 +2514,9 @@ dependencies = [ [[package]] name = "elliptic-curve" -version = "0.13.5" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct 0.2.0", "crypto-bigint 0.5.3", @@ -3420,7 +3423,7 @@ dependencies = [ "impl-trait-for-tuples", "k256", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "parity-scale-codec", "paste", "scale-info", @@ -5308,7 +5311,7 @@ dependencies = [ "derive_more", "itp-sgx-io", "itp-sgx-temp-dir", - "libsecp256k1", + "k256", "log 0.4.20", "ofb", "parity-scale-codec", @@ -6186,15 +6189,16 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if 1.0.0", "ecdsa 0.16.8", - "elliptic-curve 0.13.5", - "once_cell 1.18.0", + "elliptic-curve 0.13.8", + "once_cell 1.19.0", "sha2 0.10.7", + "signature 2.1.0", ] [[package]] @@ -6264,6 +6268,20 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lc-direct-call" +version = "0.1.0" +dependencies = [ + "core-primitives", + "itp-stf-primitives", + "litentry-primitives", + "parity-scale-codec", + "sgx_tstd", + "sp-core", + "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "sp-runtime", +] + [[package]] name = "lc-scheduled-enclave" version = "0.8.0" @@ -6360,7 +6378,7 @@ dependencies = [ "multiaddr 0.16.0", "multihash 0.16.3", "multistream-select", - "once_cell 1.18.0", + "once_cell 1.19.0", "parking_lot 0.12.1", "pin-project", "prost", @@ -6392,7 +6410,7 @@ dependencies = [ "multiaddr 0.17.1", "multihash 0.17.0", "multistream-select", - "once_cell 1.18.0", + "once_cell 1.19.0", "parking_lot 0.12.1", "pin-project", "quick-protobuf", @@ -6548,7 +6566,7 @@ dependencies = [ "futures 0.3.28", "libp2p-core 0.38.0", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "prost", "prost-build", "rand 0.8.5", @@ -8056,9 +8074,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.18.0" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "opaque-debug" @@ -8082,7 +8100,7 @@ dependencies = [ "cfg-if 1.0.0", "foreign-types", "libc", - "once_cell 1.18.0", + "once_cell 1.19.0", "openssl-macros", "openssl-sys", ] @@ -9541,7 +9559,7 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2674c66ebb4b4d9036012091b537aae5878970d6999f81a265034d85b136b341" dependencies = [ - "once_cell 1.18.0", + "once_cell 1.19.0", "pest", "sha2 0.10.7", ] @@ -10103,7 +10121,7 @@ version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ - "once_cell 1.18.0", + "once_cell 1.19.0", "toml_edit 0.19.15", ] @@ -10728,7 +10746,7 @@ dependencies = [ "log 0.4.20", "mime", "native-tls", - "once_cell 1.18.0", + "once_cell 1.19.0", "percent-encoding 2.3.0", "pin-project-lite 0.2.10", "serde 1.0.193", @@ -10794,7 +10812,7 @@ dependencies = [ "cc", "libc", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "rkyv", "spin 0.5.2", "untrusted 0.7.1", @@ -11610,7 +11628,7 @@ dependencies = [ "cfg-if 1.0.0", "libc", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "rustix 0.36.15", "sc-allocator", "sc-executor-common", @@ -11833,7 +11851,7 @@ dependencies = [ "hyper-rustls 0.23.2", "libp2p", "num_cpus", - "once_cell 1.18.0", + "once_cell 1.19.0", "parity-scale-codec", "parking_lot 0.12.1", "rand 0.8.5", @@ -12095,7 +12113,7 @@ dependencies = [ "lazy_static", "libc", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "parking_lot 0.12.1", "regex 1.9.5", "rustc-hash", @@ -14263,7 +14281,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdd6f064ccff2d6567adcb3873ca630700f00b5ad3f060c25b5dcfd9a4ce152" dependencies = [ "cfg-if 1.0.0", - "once_cell 1.18.0", + "once_cell 1.19.0", ] [[package]] @@ -14363,7 +14381,7 @@ checksum = "62cc94d358b5a1e84a5cb9109f559aa3c4d634d2b1b4de3d0fa4adc7c78e2861" dependencies = [ "anyhow", "hmac 0.12.1", - "once_cell 1.18.0", + "once_cell 1.19.0", "pbkdf2 0.11.0", "rand 0.8.5", "rustc-hash", @@ -14664,7 +14682,7 @@ version = "0.1.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0955b8137a1df6f1a2e9a37d8a6656291ff0297c1a97c24e0d8425fe2312f79a" dependencies = [ - "once_cell 1.18.0", + "once_cell 1.19.0", "valuable", ] @@ -15274,7 +15292,7 @@ checksum = "5ef2b6d3c510e9625e5fe6f509ab07d66a760f0885d858736483c32ed7809abd" dependencies = [ "bumpalo", "log 0.4.20", - "once_cell 1.18.0", + "once_cell 1.19.0", "proc-macro2", "quote", "syn 2.0.32", @@ -15444,7 +15462,7 @@ dependencies = [ "libc", "log 0.4.20", "object 0.29.0", - "once_cell 1.18.0", + "once_cell 1.19.0", "paste", "psm", "rayon", @@ -15559,7 +15577,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eed41cbcbf74ce3ff6f1d07d1b707888166dc408d1a880f651268f4f7c9194b2" dependencies = [ "object 0.29.0", - "once_cell 1.18.0", + "once_cell 1.19.0", "rustix 0.36.15", ] @@ -15903,7 +15921,7 @@ checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" dependencies = [ "either", "libc", - "once_cell 1.18.0", + "once_cell 1.19.0", ] [[package]] @@ -16475,9 +16493,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 948912ce1b..1f9a0b90c4 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -71,6 +71,7 @@ members = [ "sidechain/state", "sidechain/validateer-fetch", "litentry/primitives", + "litentry/core/direct-call", "bitacross/core/bc-task-receiver", "bitacross/core/bc-task-sender", ] diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml index e129517dda..1075e7ce5f 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml @@ -43,6 +43,8 @@ itp-types = { path = "../../../core-primitives/types", default-features = false itp-utils = { path = "../../../core-primitives/utils", default-features = false } # litentry primities +lc-direct-call = { path = "../../../litentry/core/direct-call", default-features = false } +litentry-macros = { path = "../../../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../../../litentry/primitives", default-features = false } bc-task-sender = { path = "../bc-task-sender", default-features = false } @@ -54,6 +56,7 @@ sgx = [ "hex-sgx", "sgx_tstd", "bc-task-sender/sgx", + "lc-direct-call/sgx", "litentry-primitives/sgx", "ita-stf/sgx", "itp-enclave-metrics/sgx", @@ -72,6 +75,7 @@ std = [ "threadpool", "log/std", "bc-task-sender/std", + "lc-direct-call/std", "litentry-primitives/std", "ita-sgx-runtime/std", "ita-stf/std", @@ -90,3 +94,6 @@ std = [ "futures", "thiserror", ] +production = [ + "litentry-macros/production", +] diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs index 286c210efa..f9eac4d685 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs @@ -19,9 +19,14 @@ compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the sam pub use crate::sgx_reexport_prelude::*; use bc_task_sender::init_bit_across_task_sender_storage; -use litentry_primitives::AesRequest; +use codec::{Decode, Encode}; +use frame_support::ensure; +use lc_direct_call::{DirectCall, DirectCallSigned}; +use litentry_primitives::{aes_encrypt_default, AesRequest}; use log::*; use std::{ + boxed::Box, + format, string::{String, ToString}, sync::Arc, vec::Vec, @@ -35,6 +40,9 @@ use itp_stf_executor::traits::StfEnclaveSigning; use itp_stf_state_handler::handle_state::HandleState; use ita_stf::TrustedCallSigned; +use itp_sgx_crypto::{ecdsa::Pair as EcdsaPair, schnorr::Pair as SchnorrPair}; +use litentry_macros::if_production_or; +use litentry_primitives::DecryptableRequest; #[derive(Debug, thiserror::Error, Clone)] pub enum Error { @@ -46,47 +54,67 @@ pub enum Error { } pub struct BitAcrossTaskContext< - ShieldingKeyRepository, + SKR, + EKR, + BKR, S: StfEnclaveSigning, H: HandleState, O: EnclaveOnChainOCallApi, > where - ShieldingKeyRepository: AccessKey, - ::KeyType: ShieldingCryptoEncrypt + 'static, + SKR: AccessKey, + EKR: AccessKey, + BKR: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + 'static, { - pub shielding_key: Arc, + pub shielding_key: Arc, + pub ethereum_key_repository: Arc, + pub bitcoin_key_repository: Arc, pub enclave_signer: Arc, pub state_handler: Arc, pub ocall_api: Arc, } impl< - ShieldingKeyRepository, + SKR, + EKR, + BKR, S: StfEnclaveSigning, H: HandleState, O: EnclaveOnChainOCallApi, - > BitAcrossTaskContext + > BitAcrossTaskContext where - ShieldingKeyRepository: AccessKey, - ::KeyType: ShieldingCryptoEncrypt + 'static, + SKR: AccessKey, + EKR: AccessKey, + BKR: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + 'static, H::StateT: SgxExternalitiesTrait, { pub fn new( - shielding_key: Arc, + shielding_key: Arc, + ethereum_key_repository: Arc, + bitcoin_key_repository: Arc, enclave_signer: Arc, state_handler: Arc, ocall_api: Arc, ) -> Self { - Self { shielding_key, enclave_signer, state_handler, ocall_api } + Self { + shielding_key, + ethereum_key_repository, + bitcoin_key_repository, + enclave_signer, + state_handler, + ocall_api, + } } } -pub fn run_bit_across_handler_runner( - _context: Arc>, +pub fn run_bit_across_handler_runner( + context: Arc>, ) where - ShieldingKeyRepository: AccessKey + Send + Sync + 'static, - ::KeyType: - ShieldingCryptoEncrypt + ShieldingCryptoDecrypt + 'static, + SKR: AccessKey + Send + Sync + 'static, + EKR: AccessKey + Send + Sync + 'static, + BKR: AccessKey + Send + Sync + 'static, + ::KeyType: ShieldingCryptoEncrypt + ShieldingCryptoDecrypt + 'static, S: StfEnclaveSigning + Send + Sync + 'static, H: HandleState + Send + Sync + 'static, H::StateT: SgxExternalitiesTrait, @@ -97,8 +125,9 @@ pub fn run_bit_across_handler_runner( let pool = ThreadPool::new(n_workers); while let Ok(mut req) = bit_across_task_receiver.recv() { + let context_pool = context.clone(); pool.execute(move || { - if let Err(e) = req.sender.send(handle_request(&mut req.request)) { + if let Err(e) = req.sender.send(handle_request(&mut req.request, context_pool)) { warn!("Unable to submit response back to the handler: {:?}", e); } }); @@ -108,8 +137,48 @@ pub fn run_bit_across_handler_runner( warn!("bit_across_task_receiver loop terminated"); } -pub fn handle_request(_request: &mut AesRequest) -> Result, String> +pub fn handle_request( + request: &mut AesRequest, + context: Arc>, +) -> Result, String> where + SKR: AccessKey, + EKR: AccessKey, + BKR: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + ShieldingCryptoDecrypt + 'static, + S: StfEnclaveSigning + Send + Sync + 'static, + H: HandleState + Send + Sync + 'static, + O: EnclaveOnChainOCallApi + EnclaveMetricsOCallApi + EnclaveAttestationOCallApi + 'static, { - Err("Not Implemented".to_string()) + let enclave_shielding_key = context + .shielding_key + .retrieve_key() + .map_err(|e| format!("Failed to retrieve shielding key: {:?}", e))?; + let dc = request + .decrypt(Box::new(enclave_shielding_key)) + .ok() + .and_then(|v| DirectCallSigned::decode(&mut v.as_slice()).ok()) + .ok_or_else(|| "Failed to decode payload".to_string())?; + + let mrenclave = match context.ocall_api.get_mrenclave_of_self() { + Ok(m) => m.m, + Err(_) => return Err("Failed to get mrenclave".to_string()), + }; + ensure!(dc.verify_signature(&mrenclave, &request.shard), "Failed to verify sig".to_string()); + match dc.call { + DirectCall::SignBitcoin(_, aes_key, payload) => { + if_production_or!(unimplemented!(), { + let key = context.bitcoin_key_repository.retrieve_key().unwrap(); + let signature = key.sign(&payload).unwrap(); + Ok(aes_encrypt_default(&aes_key, &signature).encode()) + }) + }, + DirectCall::SignEthereum(_, aes_key, payload) => { + if_production_or!(unimplemented!(), { + let key = context.ethereum_key_repository.retrieve_key().unwrap(); + let signature = key.sign(&payload).unwrap(); + Ok(aes_encrypt_default(&aes_key, &signature).encode()) + }) + }, + } } diff --git a/bitacross-worker/cli/Cargo.toml b/bitacross-worker/cli/Cargo.toml index 1aa70df536..75f82aae5b 100644 --- a/bitacross-worker/cli/Cargo.toml +++ b/bitacross-worker/cli/Cargo.toml @@ -52,6 +52,7 @@ itp-utils = { path = "../core-primitives/utils" } # litentry frame-metadata = "15.0.0" ita-sgx-runtime = { path = "../app-libs/sgx-runtime" } +lc-direct-call = { path = "../litentry/core/direct-call" } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } pallet-teerex = { path = "../../pallets/teerex", default-features = false } diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_bitcoin.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_bitcoin.rs new file mode 100644 index 0000000000..4cfddb4982 --- /dev/null +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_bitcoin.rs @@ -0,0 +1,57 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{ + trusted_base_cli::commands::bitacross::utils::{random_aes_key, send_direct_request}, + trusted_cli::TrustedCli, + trusted_command_utils::{get_identifiers, get_pair_from_str}, + Cli, CliResult, CliResultOk, +}; +use itp_rpc::{RpcResponse, RpcReturnValue}; +use itp_stf_primitives::types::KeyPair; +use itp_utils::FromHexPrefixed; +use lc_direct_call::DirectCall; +use sp_core::Pair; + +#[derive(Parser)] +pub struct RequestDirectCallSignBitcoinCommand { + payload: Vec, +} + +impl RequestDirectCallSignBitcoinCommand { + pub(crate) fn run(&self, cli: &Cli, trusted_cli: &TrustedCli) -> CliResult { + let alice = get_pair_from_str(trusted_cli, "//Alice", cli); + let (mrenclave, shard) = get_identifiers(trusted_cli, cli); + let key: [u8; 32] = random_aes_key(); + + let dc = DirectCall::SignBitcoin(alice.public().into(), key, self.payload.clone()).sign( + &KeyPair::Sr25519(Box::new(alice)), + &mrenclave, + &shard, + ); + + let result: String = send_direct_request(cli, trusted_cli, dc, key).unwrap(); + let response: RpcResponse = serde_json::from_str(&result).unwrap(); + if let Ok(return_value) = RpcReturnValue::from_hex(&response.result) { + println!("Got return value: {:?}", return_value); + } else { + println!("Could not decode return value: {:?}", response.result); + } + println!("Got result: {:?}", result); + + Ok(CliResultOk::None) + } +} diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs new file mode 100644 index 0000000000..c53b6bf351 --- /dev/null +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs @@ -0,0 +1,57 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{ + trusted_base_cli::commands::bitacross::utils::{random_aes_key, send_direct_request}, + trusted_cli::TrustedCli, + trusted_command_utils::{get_identifiers, get_pair_from_str}, + Cli, CliResult, CliResultOk, +}; +use itp_rpc::{RpcResponse, RpcReturnValue}; +use itp_stf_primitives::types::KeyPair; +use itp_utils::FromHexPrefixed; +use lc_direct_call::DirectCall; +use sp_core::Pair; + +#[derive(Parser)] +pub struct RequestDirectCallSignEthereumCommand { + payload: Vec, +} + +impl RequestDirectCallSignEthereumCommand { + pub(crate) fn run(&self, cli: &Cli, trusted_cli: &TrustedCli) -> CliResult { + let alice = get_pair_from_str(trusted_cli, "//Alice", cli); + let (mrenclave, shard) = get_identifiers(trusted_cli, cli); + let key: [u8; 32] = random_aes_key(); + + let dc = DirectCall::SignEthereum(alice.public().into(), key, self.payload.clone()).sign( + &KeyPair::Sr25519(Box::new(alice)), + &mrenclave, + &shard, + ); + + let result: String = send_direct_request(cli, trusted_cli, dc, key).unwrap(); + let response: RpcResponse = serde_json::from_str(&result).unwrap(); + if let Ok(return_value) = RpcReturnValue::from_hex(&response.result) { + println!("Got return value: {:?}", return_value); + } else { + println!("Could not decode return value: {:?}", response.result); + } + println!("Got result: {:?}", result); + + Ok(CliResultOk::None) + } +} diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/mod.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/mod.rs new file mode 100644 index 0000000000..149e066e59 --- /dev/null +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/mod.rs @@ -0,0 +1,20 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +pub mod direct_call_sign_bitcoin; +pub mod direct_call_sign_ethereum; + +pub mod utils; diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/utils.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/utils.rs new file mode 100644 index 0000000000..768a3b3fda --- /dev/null +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/utils.rs @@ -0,0 +1,68 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{ + command_utils::{get_shielding_key, get_worker_api_direct}, + trusted_cli::TrustedCli, + trusted_operation::read_shard, + Cli, +}; +use codec::Encode; +use itc_rpc_client::direct_client::DirectApi; +use itp_rpc::{Id, RpcRequest}; +use itp_sgx_crypto::ShieldingCryptoEncrypt; +use itp_utils::ToHexPrefixed; +use lc_direct_call::DirectCallSigned; +use litentry_primitives::{ + aes_encrypt_default, AesRequest, RequestAesKey, ShardIdentifier, REQUEST_AES_KEY_LEN, +}; + +pub fn random_aes_key() -> RequestAesKey { + let random: Vec = (0..REQUEST_AES_KEY_LEN).map(|_| rand::random::()).collect(); + random[0..REQUEST_AES_KEY_LEN].try_into().unwrap() +} + +pub fn send_direct_request( + cli: &Cli, + trusted_args: &TrustedCli, + call: DirectCallSigned, + key: RequestAesKey, +) -> Result { + let encryption_key = get_shielding_key(cli).unwrap(); + let shard = read_shard(trusted_args, cli).unwrap(); + let jsonrpc_call: String = get_bitacross_json_request(shard, call, encryption_key, key); + let direct_api = get_worker_api_direct(cli); + direct_api.get(&jsonrpc_call).map_err(|e| e.to_string()) +} + +pub fn get_bitacross_json_request( + shard: ShardIdentifier, + call: DirectCallSigned, + shielding_pubkey: sgx_crypto_helper::rsa3072::Rsa3072PubKey, + key: RequestAesKey, +) -> String { + let encrypted_key = shielding_pubkey.encrypt(&key).unwrap(); + let encrypted_top = aes_encrypt_default(&key, &call.encode()); + + // compose jsonrpc call + let request = AesRequest { shard, key: encrypted_key, payload: encrypted_top }; + RpcRequest::compose_jsonrpc_call( + Id::Number(1), + "bitacross_submitRequest".to_string(), + vec![request.to_hex()], + ) + .unwrap() +} diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/mod.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/mod.rs index 0687a4fe1d..f4edfe02ef 100644 --- a/bitacross-worker/cli/src/trusted_base_cli/commands/mod.rs +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/mod.rs @@ -1,4 +1,5 @@ pub mod balance; +pub mod bitacross; pub mod get_shard; pub mod get_shard_vault; pub mod nonce; diff --git a/bitacross-worker/cli/src/trusted_base_cli/mod.rs b/bitacross-worker/cli/src/trusted_base_cli/mod.rs index e964e1f3df..0297923847 100644 --- a/bitacross-worker/cli/src/trusted_base_cli/mod.rs +++ b/bitacross-worker/cli/src/trusted_base_cli/mod.rs @@ -17,8 +17,16 @@ use crate::{ trusted_base_cli::commands::{ - balance::BalanceCommand, get_shard::GetShardCommand, get_shard_vault::GetShardVaultCommand, - nonce::NonceCommand, set_balance::SetBalanceCommand, transfer::TransferCommand, + balance::BalanceCommand, + bitacross::{ + direct_call_sign_bitcoin::RequestDirectCallSignBitcoinCommand, + direct_call_sign_ethereum::RequestDirectCallSignEthereumCommand, + }, + get_shard::GetShardCommand, + get_shard_vault::GetShardVaultCommand, + nonce::NonceCommand, + set_balance::SetBalanceCommand, + transfer::TransferCommand, unshield_funds::UnshieldFundsCommand, }, trusted_cli::TrustedCli, @@ -61,6 +69,12 @@ pub enum TrustedBaseCommand { /// get shard vault for shielding (if defined for this worker) GetShardVault(GetShardVaultCommand), + + /// sign bitcoin transaction using custodian wallet + RequestDirectCallSignBitcoin(RequestDirectCallSignBitcoinCommand), + + /// sign ethereum transaction using custodian wallet + RequestDirectCallSignEthereum(RequestDirectCallSignEthereumCommand), } impl TrustedBaseCommand { @@ -75,6 +89,8 @@ impl TrustedBaseCommand { TrustedBaseCommand::Nonce(cmd) => cmd.run(cli, trusted_cli), TrustedBaseCommand::GetShard(cmd) => cmd.run(cli, trusted_cli), TrustedBaseCommand::GetShardVault(cmd) => cmd.run(cli, trusted_cli), + TrustedBaseCommand::RequestDirectCallSignBitcoin(cmd) => cmd.run(cli, trusted_cli), + TrustedBaseCommand::RequestDirectCallSignEthereum(cmd) => cmd.run(cli, trusted_cli), } } } diff --git a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml index c7f4f35360..17f23d5810 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml +++ b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" aes = { version = "0.6.0" } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } derive_more = { version = "0.99.5" } -libsecp256k1 = { version = "0.7.1", default-features = false } +k256 = { version = "0.13.3", default-features = false, features = ["ecdsa-core", "schnorr"] } log = { version = "0.4", default-features = false } ofb = { version = "0.4.0" } serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs similarity index 78% rename from bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs rename to bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index e31d6f0c26..3c06ebec8a 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/secp256k1.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -16,37 +16,56 @@ #[cfg(feature = "sgx")] pub use sgx::*; -pub use libsecp256k1; - -use libsecp256k1::{PublicKey, SecretKey}; +use crate::error::{Error, Result}; +use k256::{ + ecdsa::{signature::Signer, Signature, SigningKey}, + elliptic_curve::group::GroupEncoding, + PublicKey, +}; +use std::{string::ToString, vec::Vec}; /// File name of the sealed seed file. -pub const SEALED_SIGNER_SEED_FILE: &str = "secp256k1_key_sealed.bin"; +pub const SEALED_SIGNER_SEED_FILE: &str = "ecdsa_key_sealed.bin"; #[derive(Clone, PartialEq)] pub struct Pair { pub public: PublicKey, - pub secret: SecretKey, + private: SigningKey, +} + +impl Pair { + pub fn public_bytes(&self) -> Vec { + self.public.as_affine().to_bytes().as_slice().to_vec() + } + + pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { + let signature: Signature = + self.private.try_sign(payload).map_err(|e| Error::Other(e.to_string().into()))?; + Ok(signature.to_bytes().into()) + } } #[cfg(feature = "sgx")] pub mod sgx { use super::SEALED_SIGNER_SEED_FILE; use crate::{ + ecdsa::Pair, error::{Error, Result}, key_repository::KeyRepository, - secp256k1::Pair, std::string::ToString, }; use itp_sgx_io::{seal, unseal, SealedIO}; - use libsecp256k1::{PublicKey, SecretKey}; + use k256::{ + ecdsa::{SigningKey, VerifyingKey}, + PublicKey, + }; use log::*; use sgx_rand::{Rng, StdRng}; use std::{path::PathBuf, string::String}; - /// Creates a repository for an secp256k1 keypair and initializes + /// Creates a repository for ecdsa keypair and initializes /// a fresh private key if it doesn't exist at `path`. - pub fn create_secp256k1_repository( + pub fn create_ecdsa_repository( path: PathBuf, key_file_prefix: &str, ) -> Result> { @@ -98,16 +117,15 @@ pub mod sgx { fn unseal(&self) -> Result { let raw = unseal(self.path())?; - - let secret = SecretKey::parse_slice(&raw) + let secret = SigningKey::from_slice(&raw) .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - let public = PublicKey::from_secret_key(&secret); - Ok(Pair { public, secret }) + let public = PublicKey::from(VerifyingKey::from(&secret)); + Ok(Pair { public, private: secret }) } fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { - let raw = unsealed.secret.serialize(); + let raw = unsealed.private.to_bytes(); seal(&raw, self.path()).map_err(|e| e.into()) } } @@ -115,16 +133,12 @@ pub mod sgx { #[cfg(feature = "test")] pub mod sgx_tests { - use super::sgx::*; - use crate::{key_repository::AccessKey, secp256k1::Pair, std::string::ToString, ToPubkey}; - use itp_sgx_temp_dir::TempDir; - use std::path::{Path, PathBuf}; #[test] pub fn creating_repository_with_same_path_and_prefix_results_in_same_key() { let key_file_prefix = "test"; fn get_key_from_repo(path: PathBuf, prefix: &str) -> Pair { - create_secp256k1_repository(path, prefix).unwrap().retrieve_key().unwrap() + create_ecdsa_repository(path, prefix).unwrap().retrieve_key().unwrap() } let temp_dir = TempDir::with_prefix( "creating_repository_with_same_path_and_prefix_results_in_same_key", diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs index b530e16788..5f18dd86c0 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs @@ -33,12 +33,13 @@ pub mod sgx_reexport_prelude { } pub mod aes; +pub mod ecdsa; pub mod ed25519; pub mod ed25519_derivation; pub mod error; pub mod key_repository; pub mod rsa3072; -pub mod secp256k1; +pub mod schnorr; pub mod traits; pub use self::{aes::*, ed25519::*, rsa3072::*}; diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs new file mode 100644 index 0000000000..2be85eef82 --- /dev/null +++ b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs @@ -0,0 +1,178 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . +#[cfg(feature = "sgx")] +pub use sgx::*; + +use crate::error::{Error, Result}; +use k256::{ + elliptic_curve::group::GroupEncoding, + schnorr::{signature::Signer, Signature, SigningKey}, + PublicKey, +}; +use std::{string::ToString, vec::Vec}; + +/// File name of the sealed seed file. +pub const SEALED_SIGNER_SEED_FILE: &str = "schnorr_key_sealed.bin"; + +#[derive(Clone)] +pub struct Pair { + pub public: PublicKey, + private: SigningKey, +} + +impl Pair { + pub fn public_bytes(&self) -> Vec { + self.public.as_affine().to_bytes().as_slice().to_vec() + } + + pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { + let signature: Signature = + self.private.try_sign(payload).map_err(|e| Error::Other(e.to_string().into()))?; + Ok(signature.to_bytes()) + } +} + +#[cfg(feature = "sgx")] +pub mod sgx { + use super::SEALED_SIGNER_SEED_FILE; + use crate::{ + error::{Error, Result}, + key_repository::KeyRepository, + schnorr::Pair, + std::string::ToString, + }; + use itp_sgx_io::{seal, unseal, SealedIO}; + use k256::{schnorr::SigningKey, PublicKey}; + use log::*; + use sgx_rand::{Rng, StdRng}; + use std::{path::PathBuf, string::String}; + + /// Creates a repository for schnorr keypair and initializes + /// a fresh private key if it doesn't exist at `path`. + pub fn create_schnorr_repository( + path: PathBuf, + key_file_prefix: &str, + ) -> Result> { + let seal = Seal::new(path, key_file_prefix.to_string()); + Ok(KeyRepository::new(seal.init()?, seal.into())) + } + + #[derive(Clone, Debug)] + pub struct Seal { + base_path: PathBuf, + key_file_prefix: String, + } + + impl Seal { + pub fn new(base_path: PathBuf, key_file_prefix: String) -> Self { + Self { base_path, key_file_prefix } + } + + pub fn path(&self) -> PathBuf { + self.base_path + .join(self.key_file_prefix.clone() + "_" + SEALED_SIGNER_SEED_FILE) + } + } + + impl Seal { + fn unseal_pair(&self) -> Result { + self.unseal() + } + + fn exists(&self) -> bool { + self.path().exists() + } + + fn init(&self) -> Result { + if !self.exists() { + info!("Keyfile not found, creating new! {}", self.path().display()); + let mut seed = [0u8; 32]; + let mut rand = StdRng::new()?; + rand.fill_bytes(&mut seed); + seal(&seed, self.path())?; + } + self.unseal_pair() + } + } + + impl SealedIO for Seal { + type Error = Error; + type Unsealed = Pair; + + fn unseal(&self) -> Result { + let raw = unseal(self.path())?; + let secret = SigningKey::from_bytes(&raw) + .map_err(|e| Error::Other(format!("{:?}", e).into()))?; + let public = PublicKey::from(secret.verifying_key().clone()); + Ok(Pair { public, private: secret }) + } + + fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { + let raw = unsealed.private.to_bytes(); + seal(&raw, self.path()).map_err(|e| e.into()) + } + } +} + +#[cfg(feature = "test")] +pub mod sgx_tests { + + #[test] + pub fn creating_repository_with_same_path_and_prefix_results_in_same_key() { + let key_file_prefix = "test"; + fn get_key_from_repo(path: PathBuf, prefix: &str) -> Pair { + create_schnorr_repository(path, prefix).unwrap().retrieve_key().unwrap() + } + let temp_dir = TempDir::with_prefix( + "creating_repository_with_same_path_and_prefix_results_in_same_key", + ) + .unwrap(); + let temp_path = temp_dir.path().to_path_buf(); + assert_eq!( + get_key_from_repo(temp_path.clone(), key_file_prefix), + get_key_from_repo(temp_path.clone(), key_file_prefix) + ); + } + + #[test] + pub fn seal_init_should_create_new_key_if_not_present() { + //given + let temp_dir = + TempDir::with_prefix("seal_init_should_create_new_key_if_not_present").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + assert!(!seal.exists()); + + //when + seal.init().unwrap(); + + //then + assert!(seal.exists()); + } + + #[test] + pub fn seal_init_should_not_change_key_if_exists() { + //given + let temp_dir = TempDir::with_prefix("seal_init_should_not_change_key_if_exists").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + let pair = seal.init().unwrap(); + + //when + let new_pair = seal.init().unwrap(); + + //then + assert!(pair, new_pair); + } +} diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 81ba58674d..54ade19207 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -270,6 +270,8 @@ dependencies = [ "itp-top-pool-author", "itp-types", "itp-utils", + "lc-direct-call", + "litentry-macros", "litentry-primitives", "log", "parity-scale-codec", @@ -860,9 +862,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" [[package]] name = "elliptic-curve" -version = "0.13.5" +version = "0.13.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" dependencies = [ "base16ct", "crypto-bigint", @@ -2331,7 +2333,7 @@ dependencies = [ "derive_more", "itp-sgx-io", "itp-sgx-temp-dir", - "libsecp256k1", + "k256", "log", "ofb", "parity-scale-codec", @@ -2832,14 +2834,15 @@ dependencies = [ [[package]] name = "k256" -version = "0.13.1" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +checksum = "956ff9b67e26e1a6a866cb758f12c6f8746208489e3e4a4b5580802f2f0a587b" dependencies = [ "cfg-if 1.0.0", "ecdsa", "elliptic-curve", "sha2 0.10.7", + "signature", ] [[package]] @@ -2866,6 +2869,20 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "lc-direct-call" +version = "0.1.0" +dependencies = [ + "core-primitives", + "itp-stf-primitives", + "litentry-primitives", + "parity-scale-codec", + "sgx_tstd", + "sp-core", + "sp-io", + "sp-runtime", +] + [[package]] name = "lc-scheduled-enclave" version = "0.8.0" @@ -5331,9 +5348,9 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" dependencies = [ "zeroize_derive", ] diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index c17f580554..57605ed92b 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -23,6 +23,7 @@ production = [ "itp-attestation-handler/production", "litentry-primitives/production", "litentry-macros/production", + "bc-task-receiver/production", ] sidechain = ["itp-settings/sidechain", "itp-top-pool-author/sidechain"] offchain-worker = [ diff --git a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs index 5b598ae0e5..6ebddbc911 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs @@ -64,8 +64,9 @@ use itp_node_api::{ }; use itp_nonce_cache::NonceCache; use itp_sgx_crypto::{ + ecdsa::{Pair as EcdsaPair, Seal as EcdsaSeal}, key_repository::KeyRepository, - secp256k1::{Pair as Secp256k1Pair, Seal as Secp256k1Seal}, + schnorr::{Pair as SchnorrPair, Seal as SchnorrSeal}, Aes, AesSeal, Ed25519Seal, Rsa3072Seal, }; use itp_stf_executor::{ @@ -110,8 +111,8 @@ pub type EnclaveStf = Stf; pub type EnclaveShieldingKeyRepository = KeyRepository; pub type EnclaveSigningKeyRepository = KeyRepository; -pub type EnclaveBitcoinKeyRepository = KeyRepository; -pub type EnclaveEthereumKeyRepository = KeyRepository; +pub type EnclaveBitcoinKeyRepository = KeyRepository; +pub type EnclaveEthereumKeyRepository = KeyRepository; pub type EnclaveStateFileIo = SgxStateFileIo; pub type EnclaveStateSnapshotRepository = StateSnapshotRepository; pub type EnclaveStateObserver = StateObserver; diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index 3ba32756d4..402cf1f474 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -73,8 +73,8 @@ use itp_settings::files::{ TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, }; use itp_sgx_crypto::{ - get_aes_repository, get_ed25519_repository, get_rsa3072_repository, key_repository::AccessKey, - secp256k1::create_secp256k1_repository, + ecdsa::create_ecdsa_repository, get_aes_repository, get_ed25519_repository, + get_rsa3072_repository, key_repository::AccessKey, schnorr::create_schnorr_repository, }; use itp_stf_state_handler::{ file_io::StateDir, handle_state::HandleState, query_shard_state::QueryShardState, @@ -105,17 +105,15 @@ pub(crate) fn init_enclave( let signer = signing_key_repository.retrieve_key()?; info!("[Enclave initialized] Ed25519 prim raw : {:?}", signer.public().0); - let bitcoin_key_repository = - Arc::new(create_secp256k1_repository(base_dir.clone(), "bitcoin")?); + let bitcoin_key_repository = Arc::new(create_schnorr_repository(base_dir.clone(), "bitcoin")?); GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT.initialize(bitcoin_key_repository.clone()); let bitcoin_key = bitcoin_key_repository.retrieve_key()?; - info!("[Enclave initialized] Bitcoin public key raw : {:?}", bitcoin_key.public.serialize()); + info!("[Enclave initialized] Bitcoin public key raw : {:?}", bitcoin_key.public_bytes()); - let ethereum_key_repository = - Arc::new(create_secp256k1_repository(base_dir.clone(), "ethereum")?); + let ethereum_key_repository = Arc::new(create_ecdsa_repository(base_dir.clone(), "ethereum")?); GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.initialize(ethereum_key_repository.clone()); let ethereum_key = ethereum_key_repository.retrieve_key()?; - info!("[Enclave initialized] Ethereum public key raw : {:?}", ethereum_key.public.serialize()); + info!("[Enclave initialized] Ethereum public key raw : {:?}", ethereum_key.public_bytes()); let shielding_key_repository = Arc::new(get_rsa3072_repository(base_dir.clone())?); GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT.initialize(shielding_key_repository.clone()); @@ -256,6 +254,9 @@ fn run_bit_across_handler() -> Result<(), Error> { let state_observer = GLOBAL_STATE_OBSERVER_COMPONENT.get()?; let shielding_key_repository = GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT.get()?; + let ethereum_key_repository = GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.get()?; + let bitcoin_key_repository = GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT.get()?; + #[allow(clippy::unwrap_used)] let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let stf_enclave_signer = Arc::new(EnclaveStfEnclaveSigner::new( @@ -267,6 +268,8 @@ fn run_bit_across_handler() -> Result<(), Error> { let stf_task_context = BitAcrossTaskContext::new( shielding_key_repository, + ethereum_key_repository, + bitcoin_key_repository, stf_enclave_signer, state_handler, ocall_api, diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index 0a63f5454f..21b9b0aad6 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -143,7 +143,7 @@ pub extern "C" fn test_main_entrance() -> size_t { sidechain_aura_tests::produce_sidechain_block_and_import_it, sidechain_event_tests::ensure_events_get_reset_upon_block_proposal, top_pool_tests::process_indirect_call_in_top_pool, - top_pool_tests::submit_shielding_call_to_top_pool, + // top_pool_tests::submit_shielding_call_to_top_pool, // tls_ra unit tests tls_ra::seal_handler::test::seal_shielding_key_works, tls_ra::seal_handler::test::seal_shielding_key_fails_for_invalid_key, diff --git a/bitacross-worker/litentry/core/direct-call/Cargo.toml b/bitacross-worker/litentry/core/direct-call/Cargo.toml new file mode 100644 index 0000000000..b40a525ee3 --- /dev/null +++ b/bitacross-worker/litentry/core/direct-call/Cargo.toml @@ -0,0 +1,40 @@ +[package] +authors = ["Trust Computing GmbH "] +edition = "2021" +name = "lc-direct-call" +version = "0.1.0" + +[dependencies] +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } + +# internal dependencies +itp-stf-primitives = { path = "../../../core-primitives/stf-primitives", default-features = false } +litentry-primitives = { path = "../../primitives", default-features = false } +parentchain-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } + +# sgx dependencies +sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true } + + +[dev-dependencies] + +[features] +default = ["std"] +production = [ + "parentchain-primitives/production", +] +sgx = [ + "sgx_tstd", + "litentry-primitives/sgx", +] +std = [ + "itp-stf-primitives/std", + "litentry-primitives/std", + "sp-core/std", + "sp-io/std", + "sp-runtime/std", + "parentchain-primitives/std", +] diff --git a/bitacross-worker/litentry/core/direct-call/src/lib.rs b/bitacross-worker/litentry/core/direct-call/src/lib.rs new file mode 100644 index 0000000000..dc32ebf3b7 --- /dev/null +++ b/bitacross-worker/litentry/core/direct-call/src/lib.rs @@ -0,0 +1,74 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use codec::{Decode, Encode}; +use itp_stf_primitives::types::KeyPair; +use litentry_primitives::{LitentryMultiSignature, RequestAesKey, ShardIdentifier}; +use parentchain_primitives::Identity; +use sp_io::hashing::blake2_256; +use std::vec::Vec; + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct DirectCallSigned { + pub call: DirectCall, + pub signature: LitentryMultiSignature, +} + +impl DirectCallSigned { + pub fn verify_signature(&self, mrenclave: &[u8; 32], shard: &ShardIdentifier) -> bool { + let mut payload = self.call.encode(); + payload.append(&mut mrenclave.encode()); + payload.append(&mut shard.encode()); + + self.signature.verify(blake2_256(&payload).as_slice(), self.call.signer()) + } +} + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub enum DirectCall { + SignBitcoin(Identity, RequestAesKey, Vec), + SignEthereum(Identity, RequestAesKey, Vec), +} + +impl DirectCall { + pub fn signer(&self) -> &Identity { + match self { + Self::SignBitcoin(signer, ..) => signer, + Self::SignEthereum(signer, ..) => signer, + } + } + + pub fn sign( + &self, + pair: &KeyPair, + mrenclave: &[u8; 32], + shard: &ShardIdentifier, + ) -> DirectCallSigned { + let mut payload = self.encode(); + payload.append(&mut mrenclave.encode()); + payload.append(&mut shard.encode()); + + DirectCallSigned { + call: self.clone(), + signature: pair.sign(blake2_256(&payload).as_slice()), + } + } +} diff --git a/primitives/core/proc-macros/src/lib.rs b/primitives/core/proc-macros/src/lib.rs index 16ac88fe39..4e803e10e7 100644 --- a/primitives/core/proc-macros/src/lib.rs +++ b/primitives/core/proc-macros/src/lib.rs @@ -35,7 +35,7 @@ This works similar with `#[cfg(..)]` that sets the target only appear on the spe ``` use litentry_proc_macros::reuse; -#[reuse(x, y)] . // Define the cases that the following implementation expands for each one +#[reuse(x, y)] // Define the cases that the following implementation expands for each one mod __ { // Leave mod name with double discards, which is to be replaced by the cases #[x] // This item would only appear on case `x` fn u() { diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index b3f96a690c..f490cbc93e 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -64,7 +64,7 @@ RUST_LOG=info SKIP_WASM_BUILD=1 cargo test --release -- --show-output echo "[Step 5], tee-worker service test" clean_up cd "$root_dir/tee-worker" -#SGX_MODE=SW SKIP_WASM_BUILD=1 make +SGX_MODE=SW SKIP_WASM_BUILD=1 make cd "$root_dir/tee-worker/bin" ./litentry-worker test --all From 9bd0002293500ebd675a0f22223e30fae7b07ab6 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Sat, 10 Feb 2024 23:40:16 +0100 Subject: [PATCH 22/64] Use get_account_next_index (#2475) --- bitacross-worker/service/src/main_impl.rs | 2 +- tee-worker/service/src/main_impl.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 764eb8aabf..36f24fcd9d 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -755,7 +755,7 @@ where let last_synced_header = parentchain_handler.init_parentchain_components().unwrap(); println!("[{:?}] last synced parentchain block: {}", parentchain_id, last_synced_header.number); - let nonce = node_api.get_nonce_of(tee_account_id).unwrap(); + let nonce = node_api.get_account_next_index(tee_account_id).unwrap(); info!("[{:?}] Enclave nonce = {:?}", parentchain_id, nonce); enclave.set_nonce(nonce, parentchain_id).unwrap_or_else(|_| { panic!("[{:?}] Could not set nonce of enclave. Returning here...", parentchain_id) diff --git a/tee-worker/service/src/main_impl.rs b/tee-worker/service/src/main_impl.rs index a44fb49c97..6c10924238 100644 --- a/tee-worker/service/src/main_impl.rs +++ b/tee-worker/service/src/main_impl.rs @@ -767,7 +767,7 @@ where let last_synced_header = parentchain_handler.init_parentchain_components().unwrap(); println!("[{:?}] last synced parentchain block: {}", parentchain_id, last_synced_header.number); - let nonce = node_api.get_nonce_of(tee_account_id).unwrap(); + let nonce = node_api.get_account_next_index(tee_account_id).unwrap(); info!("[{:?}] Enclave nonce = {:?}", parentchain_id, nonce); enclave.set_nonce(nonce, parentchain_id).unwrap_or_else(|_| { panic!("[{:?}] Could not set nonce of enclave. Returning here...", parentchain_id) From 5ce842fd0cdea76aafd220da86c90b17b8e1530b Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Sun, 11 Feb 2024 17:03:17 +0530 Subject: [PATCH 23/64] Parachain `pallet-bitacross` implementation (#2471) * fix: add extrinsic and parsing for indirect executor * fix: seal unseal operations for GLOBAL RELAYER REGISTRY * fix: update indirect executor to update Relayer Registry * feat: add relayer account type * debug: misc * debug: fix event * debug: change pallet folder name * debug: fmt * debug: using existing struct * debug: fmt * pallet-bitacross skeleton * runtime integration * some more update * fix: update indirect call executor and registry to use Identity instead of AccountId * fix: clippy * refactor: remove accidental commit * refactor: remove error log * refactor: remove comments --------- Co-authored-by: Minqi Wang Co-authored-by: WMQ <46511820+wangminqi@users.noreply.github.com> Co-authored-by: Kailai Wang --- Cargo.lock | 4 - Cargo.toml | 3 +- bitacross-worker/Cargo.lock | 45 +++- bitacross-worker/Cargo.toml | 1 + .../app-libs/parentchain-interface/Cargo.toml | 4 + .../src/integritee/mod.rs | 54 ++++- .../core/bc-relayer-registry/Cargo.toml | 83 ++++++++ .../core/bc-relayer-registry/src/lib.rs | 195 +++++++++++++++++ .../node-api/metadata/src/metadata_mocks.rs | 14 +- .../node-api/metadata/src/pallet_bitacross.rs | 13 +- .../core-primitives/settings/src/lib.rs | 3 + bitacross-worker/enclave-runtime/Cargo.lock | 39 ++++ bitacross-worker/enclave-runtime/Cargo.toml | 1 + .../enclave-runtime/src/initialization/mod.rs | 4 + .../litentry/primitives/Cargo.toml | 11 + node/src/chain_specs/rococo.rs | 11 +- pallets/bitacross-pallet/src/lib.rs | 68 ------ .../Cargo.toml | 26 +-- pallets/bitacross/src/custodial_wallet.rs | 36 ++++ pallets/bitacross/src/lib.rs | 196 ++++++++++++++++++ primitives/core/src/identity.rs | 16 +- runtime/rococo/Cargo.toml | 2 +- runtime/rococo/src/lib.rs | 8 +- tee-worker/Cargo.lock | 2 - 24 files changed, 712 insertions(+), 127 deletions(-) create mode 100644 bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml create mode 100644 bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs delete mode 100644 pallets/bitacross-pallet/src/lib.rs rename pallets/{bitacross-pallet => bitacross}/Cargo.toml (76%) create mode 100644 pallets/bitacross/src/custodial_wallet.rs create mode 100644 pallets/bitacross/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 9a38f33515..44bdec2c59 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6945,8 +6945,6 @@ dependencies = [ "frame-support", "frame-system", "pallet-balances", - "pallet-group", - "pallet-teerex", "pallet-timestamp", "parity-scale-codec", "scale-info", @@ -6954,8 +6952,6 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "teerex-primitives", - "test-utils", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index a1f6ac55df..636b081f91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ 'node', 'pallets/account-fix', + 'pallets/bitacross', 'pallets/bridge', 'pallets/bridge-transfer', 'pallets/drop3', @@ -17,7 +18,7 @@ members = [ 'pallets/parentchain', 'pallets/test-utils', 'pallets/group', - 'pallets/bitacross-pallet', + 'pallets/bitacross', 'precompiles/*', 'primitives/common', 'primitives/core', diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index b6e7ef5f12..730e62fd53 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -590,6 +590,43 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" +[[package]] +name = "bc-relayer-registry" +version = "0.1.0" +dependencies = [ + "base64 0.13.0 (git+https://github.com/mesalock-linux/rust-base64-sgx?rev=sgx_1.1.3)", + "base64 0.13.1", + "bitcoin", + "core-primitives", + "hex 0.4.3", + "itp-settings", + "itp-sgx-crypto", + "itp-sgx-io", + "itp-utils", + "lazy_static", + "litentry-hex-utils", + "litentry-primitives", + "log 0.4.20", + "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "parity-scale-codec", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", + "rand 0.7.3 (git+https://github.com/mesalock-linux/rand-sgx?tag=sgx_1.1.3)", + "ring 0.16.20", + "scale-info", + "secp256k1 0.28.0", + "serde 1.0.193", + "sgx_tstd", + "sp-core", + "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "sp-runtime", + "sp-std 5.0.0", + "strum 0.26.1", + "strum_macros 0.26.1", + "teerex-primitives", + "thiserror 1.0.44", + "thiserror 1.0.9", +] + [[package]] name = "bc-task-receiver" version = "0.1.0" @@ -4657,6 +4694,7 @@ dependencies = [ name = "ita-parentchain-interface" version = "0.9.0" dependencies = [ + "bc-relayer-registry", "bs58", "env_logger 0.9.3", "ita-sgx-runtime", @@ -6932,8 +6970,11 @@ dependencies = [ "bitcoin", "core-primitives", "hex 0.4.3", + "itp-settings", "itp-sgx-crypto", + "itp-sgx-io", "itp-utils", + "lazy_static", "litentry-hex-utils", "log 0.4.20", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", @@ -6952,6 +6993,8 @@ dependencies = [ "strum 0.26.1", "strum_macros 0.26.1", "teerex-primitives", + "thiserror 1.0.44", + "thiserror 1.0.9", ] [[package]] @@ -8446,13 +8489,11 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "pallet-teerex", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 1f9a0b90c4..e722ab669c 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -74,6 +74,7 @@ members = [ "litentry/core/direct-call", "bitacross/core/bc-task-receiver", "bitacross/core/bc-task-sender", + "bitacross/core/bc-relayer-registry", ] [patch."https://github.com/apache/teaclave-sgx-sdk.git"] diff --git a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml index 59ec735302..a31f3ee956 100644 --- a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml +++ b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml @@ -28,11 +28,13 @@ sp-core = { default-features = false, features = ["full_crypto"], git = "https:/ sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } # litentry +bc-relayer-registry = { path = "../../bitacross/core/bc-relayer-registry", default-features = false } lc-scheduled-enclave = { path = "../../litentry/core/scheduled-enclave", default-features = false, optional = true } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } + [dev-dependencies] env_logger = "0.9.0" itp-node-api = { path = "../../core-primitives/node-api", features = ["mocks"] } @@ -66,6 +68,7 @@ std = [ "litentry-primitives/std", "lc-scheduled-enclave/std", "sp-std/std", + "bc-relayer-registry/std", ] sgx = [ "sgx_tstd", @@ -77,4 +80,5 @@ sgx = [ "itp-top-pool-author/sgx", "litentry-primitives/sgx", "lc-scheduled-enclave/sgx", + "bc-relayer-registry/sgx", ] diff --git a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs index 3995175f48..3e79c8ac9e 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs @@ -24,6 +24,7 @@ use crate::{ indirect_calls::{RemoveScheduledEnclaveArgs, UpdateScheduledEnclaveArgs}, integritee::extrinsic_parser::ParseExtrinsic, }; +use bc_relayer_registry::{RelayerRegistryUpdater, GLOBAL_RELAYER_REGISTRY}; use codec::{Decode, Encode}; use core::marker::PhantomData; pub use event_filter::FilterableEvents; @@ -37,6 +38,7 @@ use itc_parentchain_indirect_calls_executor::{ }; use itp_node_api::metadata::NodeMetadataTrait; use itp_stf_primitives::traits::IndirectExecutor; +use litentry_primitives::Identity; use log::trace; use sp_core::crypto::AccountId32; @@ -49,6 +51,10 @@ pub enum IndirectCall { UpdateScheduledEnclave(UpdateScheduledEnclaveArgs), #[codec(index = 2)] RemoveScheduledEnclave(RemoveScheduledEnclaveArgs), + #[codec(index = 3)] + AddRelayer(AddRelayerArgs), + #[codec(index = 4)] + RemoveRelayer(RemoveRelayerArgs), } impl> @@ -63,6 +69,9 @@ impl> update_scheduled_enclave_args.dispatch(executor, ()), IndirectCall::RemoveScheduledEnclave(remove_scheduled_enclave_args) => remove_scheduled_enclave_args.dispatch(executor, ()), + IndirectCall::AddRelayer(add_relayer_args) => add_relayer_args.dispatch(executor, ()), + IndirectCall::RemoveRelayer(remove_relayer_args) => + remove_relayer_args.dispatch(executor, ()), } } } @@ -82,6 +91,38 @@ impl> } } +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] +pub struct AddRelayerArgs { + account_id: Identity, +} + +impl> + IndirectDispatch for AddRelayerArgs +{ + type Args = (); + fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { + log::info!("Adding Relayer Account to Registry: {:?}", self.account_id); + GLOBAL_RELAYER_REGISTRY.update(self.account_id.clone()).unwrap(); + Ok(()) + } +} + +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] +pub struct RemoveRelayerArgs { + account_id: Identity, +} + +impl> + IndirectDispatch for RemoveRelayerArgs +{ + type Args = (); + fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { + log::info!("Remove Relayer Account from Registry: {:?}", self.account_id); + GLOBAL_RELAYER_REGISTRY.remove(self.account_id.clone()).unwrap(); + Ok(()) + } +} + /// Default filter we use for the Integritee-Parachain. pub struct BitAcrossIndirectCallsFilter { _phantom: PhantomData, @@ -116,15 +157,20 @@ where let index = xt.call_index; let call_args = &mut &xt.call_args[..]; - if index == metadata.placeholder_call_indexes().ok()? { - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::BitAcross(args)) - } else if index == metadata.update_scheduled_enclave().ok()? { + if index == metadata.update_scheduled_enclave().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::UpdateScheduledEnclave(args)) } else if index == metadata.remove_scheduled_enclave().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::RemoveScheduledEnclave(args)) + } else if index == metadata.add_relayer_call_indexes().ok()? { + log::error!("Received Add Relayer indirect call"); + let args = decode_and_log_error::(call_args)?; + Some(IndirectCall::AddRelayer(args)) + } else if index == metadata.remove_relayer_call_indexes().ok()? { + log::error!("Processing Remove Relayer Call"); + let args = decode_and_log_error::(call_args)?; + Some(IndirectCall::RemoveRelayer(args)) } else { None } diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml new file mode 100644 index 0000000000..6750739b39 --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml @@ -0,0 +1,83 @@ +[package] +name = "bc-relayer-registry" +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +bitcoin = { version = "0.31.0", default-features = false, features = ["secp-recovery", "no-std"] } +codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } +hex = { version = "0.4.3", default-features = false } +lazy_static = { version = "1.1.0", features = ["spin_no_std"] } +log = { version = "0.4", default-features = false } +pallet-evm = { default-features = false, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } +rand = { version = "0.7", optional = true } +rand-sgx = { package = "rand", git = "https://github.com/mesalock-linux/rand-sgx", tag = "sgx_1.1.3", features = ["sgx_tstd"], optional = true } +ring = { version = "0.16.20", default-features = false } +scale-info = { version = "2.4.0", default-features = false, features = ["derive"] } +secp256k1 = { version = "0.28.0", default-features = false } +serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } +sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +strum = { version = "0.26", default-features = false } +strum_macros = { version = "0.26", default-features = false } +thiserror = { version = "1.0.26", optional = true } + + +# sgx dependencies +base64_sgx = { package = "base64", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base64-sgx", optional = true } +sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true, features = ["net", "thread"] } +thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } + + +# internal dependencies +itp-settings = { path = "../../../core-primitives/settings", default-features = false } +itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } +itp-sgx-io = { path = "../../../core-primitives/sgx/io", default-features = false } +itp-utils = { path = "../../../core-primitives/utils", default-features = false } +litentry-hex-utils = { path = "../../../../primitives/hex", default-features = false } +parentchain-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } +teerex-primitives = { path = "../../../../primitives/teerex", default-features = false } +# litentry primities +litentry-primitives = { path = "../../../litentry/primitives", default-features = false } + +[dev-dependencies] +base64 = { version = "0.13", features = ["alloc"] } + +[features] +default = ["std"] +production = [ + "parentchain-primitives/production", +] +sgx = [ + "sgx_tstd", + "rand-sgx", + "itp-sgx-crypto/sgx", + "thiserror-sgx", + "itp-sgx-io/sgx", + "litentry-primitives/sgx", +] +std = [ + "strum/std", + "hex/std", + "serde/std", + "itp-sgx-crypto/std", + "itp-utils/std", + "sp-core/std", + "sp-std/std", + "sp-io/std", + "sp-runtime/std", + "ring/std", + "parentchain-primitives/std", + "teerex-primitives/std", + "rand", + "log/std", + "bitcoin/std", + "secp256k1/std", + "thiserror", + "itp-sgx-io/std", + "litentry-primitives/std", +] diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs new file mode 100644 index 0000000000..a7d5eefff5 --- /dev/null +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs @@ -0,0 +1,195 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate core; +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +use sp_std::{boxed::Box, fmt::Debug}; + +use lazy_static::lazy_static; +use litentry_primitives::Identity; +use log::error; +use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; +pub use teerex_primitives::{decl_rsa_request, ShardIdentifier, SidechainBlockNumber}; + +#[cfg(feature = "std")] +use std::sync::RwLock; +#[cfg(feature = "sgx")] +use std::sync::SgxRwLock as RwLock; + +lazy_static! { + /// Global instance of a RelayerRegistry + pub static ref GLOBAL_RELAYER_REGISTRY: Arc = Default::default(); +} + +pub type RelayerRegistryMap = BTreeMap; + +#[derive(Default)] +pub struct RelayerRegistry { + pub registry: RwLock, + pub seal_path: PathBuf, +} + +pub type RegistryResult = core::result::Result; + +#[cfg(feature = "sgx")] +use thiserror_sgx as thiserror; + +#[derive(Debug, thiserror::Error)] +pub enum RegistryError { + #[error("poison lock")] + PoisonLock, + #[error("empty Relayer registry")] + EmptyRegistry, + #[error(transparent)] + Other(#[from] Box), +} + +impl From for RegistryError { + fn from(e: std::io::Error) -> Self { + Self::Other(e.into()) + } +} + +impl From for RegistryError { + #[cfg(feature = "std")] + fn from(e: codec::Error) -> Self { + Self::Other(e.into()) + } + + #[cfg(feature = "sgx")] + fn from(e: codec::Error) -> Self { + Self::Other(std::format!("{:?}", e).into()) + } +} + +#[cfg(feature = "sgx")] +mod sgx { + use crate::{RegistryError as Error, RegistryResult as Result, RelayerRegistryMap}; + pub use codec::{Decode, Encode}; + pub use itp_settings::files::RELAYER_REGISTRY_FILE; + pub use itp_sgx_io::{seal, unseal, SealedIO}; + pub use log::*; + pub use std::{boxed::Box, fs, path::PathBuf, sgxfs::SgxFile, sync::Arc}; + + #[derive(Clone, Debug)] + pub struct RelayerRegistrySeal { + base_path: PathBuf, + } + + impl RelayerRegistrySeal { + pub fn new(base_path: PathBuf) -> Self { + Self { base_path } + } + + pub fn path(&self) -> PathBuf { + self.base_path.join(RELAYER_REGISTRY_FILE) + } + } + + impl SealedIO for RelayerRegistrySeal { + type Error = Error; + type Unsealed = RelayerRegistryMap; + + fn unseal(&self) -> Result { + Ok(unseal(self.path()).map(|b| Decode::decode(&mut b.as_slice()))??) + } + + fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { + info!("Seal relayer registry to file: {:?}", unsealed); + Ok(unsealed.using_encoded(|bytes| seal(bytes, self.path()))?) + } + } +} + +#[cfg(feature = "sgx")] +use sgx::*; + +pub trait RelayerRegistryUpdater { + fn init(&self) -> RegistryResult<()>; + fn update(&self, account: Identity) -> RegistryResult<()>; + fn remove(&self, account: Identity) -> RegistryResult<()>; + fn contains_key(&self, account: Identity) -> bool; +} + +impl RelayerRegistryUpdater for RelayerRegistry { + #[cfg(feature = "std")] + fn init(&self) -> RegistryResult<()> { + Ok(()) + } + + #[cfg(feature = "std")] + fn update(&self, _account: Identity) -> RegistryResult<()> { + Ok(()) + } + + #[cfg(feature = "std")] + fn remove(&self, _account: Identity) -> RegistryResult<()> { + Ok(()) + } + + #[cfg(feature = "std")] + fn contains_key(&self, _account: Identity) -> bool { + true + } + + // if `RELAYER_REGISTRY_FILE` exists, unseal and init from it + // otherwise create a new instance and seal to static file + #[cfg(feature = "sgx")] + fn init(&self) -> RegistryResult<()> { + let enclave_seal = RelayerRegistrySeal::new(self.seal_path.clone()); + if SgxFile::open(RELAYER_REGISTRY_FILE).is_err() { + info!( + "[Enclave] RelayerRegistry file not found, creating new! {}", + RELAYER_REGISTRY_FILE + ); + let registry = GLOBAL_RELAYER_REGISTRY + .registry + .write() + .map_err(|_| RegistryError::PoisonLock)?; + enclave_seal.seal(&*registry) + } else { + let m = enclave_seal.unseal()?; + info!("[Enclave] RelayerRegistry unsealed from file: {:?}", m); + let mut registry = GLOBAL_RELAYER_REGISTRY + .registry + .write() + .map_err(|_| RegistryError::PoisonLock)?; + *registry = m; + Ok(()) + } + } + + #[cfg(feature = "sgx")] + fn update(&self, account: Identity) -> RegistryResult<()> { + let mut registry = GLOBAL_RELAYER_REGISTRY + .registry + .write() + .map_err(|_| RegistryError::PoisonLock)?; + registry.insert(account, ()); + RelayerRegistrySeal::new(self.seal_path.clone()).seal(&*registry) + } + + #[cfg(feature = "sgx")] + fn remove(&self, account: Identity) -> RegistryResult<()> { + let mut registry = GLOBAL_RELAYER_REGISTRY + .registry + .write() + .map_err(|_| RegistryError::PoisonLock)?; + let old_value = registry.remove(&account); + if old_value.is_some() { + return RelayerRegistrySeal::new(self.seal_path.clone()).seal(&*registry) + } + Ok(()) + } + + #[cfg(feature = "sgx")] + fn contains_key(&self, account: Identity) -> bool { + // Using unwrap becaused poisoned locks are unrecoverable errors + let registry = GLOBAL_RELAYER_REGISTRY.registry.read().unwrap(); + registry.contains_key(&account) + } +} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index 0a899df8ca..2ddc45ad6a 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -71,7 +71,8 @@ pub struct NodeMetadataMock { runtime_transaction_version: u32, bitacross_module: u8, - bitacross_placeholder: u8, + bitacross_add_relayer: u8, + bitacross_remove_relayer: u8, } impl NodeMetadataMock { @@ -113,7 +114,8 @@ impl NodeMetadataMock { runtime_transaction_version: 4, bitacross_module: 69u8, - bitacross_placeholder: 70u8, + bitacross_add_relayer: 71u8, + bitacross_remove_relayer: 72u8, } } } @@ -237,7 +239,11 @@ impl BalancesCallIndexes for NodeMetadataMock { } impl BitAcrossCallIndexes for NodeMetadataMock { - fn placeholder_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.bitacross_module, self.bitacross_placeholder]) + fn add_relayer_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.bitacross_module, self.bitacross_add_relayer]) + } + + fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.bitacross_module, self.bitacross_remove_relayer]) } } diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs index 74af1d46ff..b18f575c15 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs @@ -17,14 +17,19 @@ // TODO: maybe use macros to simplify this use crate::{error::Result, NodeMetadata}; -const BITACROSS: &str = "BitAcross"; +const BITACROSS: &str = "Bitacross"; pub trait BitAcrossCallIndexes { - fn placeholder_call_indexes(&self) -> Result<[u8; 2]>; + fn add_relayer_call_indexes(&self) -> Result<[u8; 2]>; + fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]>; } impl BitAcrossCallIndexes for NodeMetadata { - fn placeholder_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(BITACROSS, "placeholder") + fn add_relayer_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(BITACROSS, "add_relayer") + } + + fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(BITACROSS, "remove_relayer") } } diff --git a/bitacross-worker/core-primitives/settings/src/lib.rs b/bitacross-worker/core-primitives/settings/src/lib.rs index 1ca8960c68..22989cda90 100644 --- a/bitacross-worker/core-primitives/settings/src/lib.rs +++ b/bitacross-worker/core-primitives/settings/src/lib.rs @@ -48,6 +48,9 @@ pub mod files { // litentry pub const SCHEDULED_ENCLAVE_FILE: &str = "scheduled_enclave_sealed.bin"; + // bitacross + pub const RELAYER_REGISTRY_FILE: &str = "relayer_registry_sealed.bin"; + pub const RA_DUMP_CERT_DER_FILE: &str = "ra_dump_cert.der"; // used by worker and enclave diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 54ade19207..81ad46a745 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -248,6 +248,39 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "bc-relayer-registry" +version = "0.1.0" +dependencies = [ + "bitcoin", + "core-primitives", + "hex 0.4.3", + "itp-settings", + "itp-sgx-crypto", + "itp-sgx-io", + "itp-utils", + "lazy_static", + "litentry-hex-utils", + "litentry-primitives", + "log", + "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "parity-scale-codec", + "rand 0.7.3", + "ring 0.16.20", + "scale-info", + "secp256k1 0.28.0", + "serde 1.0.193", + "sgx_tstd", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "strum", + "strum_macros", + "teerex-primitives 0.1.0", + "thiserror", +] + [[package]] name = "bc-task-receiver" version = "0.1.0" @@ -883,6 +916,7 @@ name = "enclave-runtime" version = "0.0.1" dependencies = [ "array-bytes 6.1.0", + "bc-relayer-registry", "bc-task-receiver", "cid", "derive_more", @@ -1855,6 +1889,7 @@ dependencies = [ name = "ita-parentchain-interface" version = "0.9.0" dependencies = [ + "bc-relayer-registry", "bs58", "ita-sgx-runtime", "ita-stf", @@ -2975,8 +3010,11 @@ dependencies = [ "bitcoin", "core-primitives", "hex 0.4.3", + "itp-settings", "itp-sgx-crypto", + "itp-sgx-io", "itp-utils", + "lazy_static", "litentry-hex-utils", "log", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", @@ -2994,6 +3032,7 @@ dependencies = [ "strum", "strum_macros", "teerex-primitives 0.1.0", + "thiserror", ] [[package]] diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 57605ed92b..aaabf0d4f9 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -134,6 +134,7 @@ its-primitives = { path = "../sidechain/primitives", default-features = false } its-sidechain = { path = "../sidechain/sidechain-crate", default-features = false, features = ["sgx"] } # litentry +bc-relayer-registry = { path = "../bitacross/core/bc-relayer-registry", default-features = false, features = ["sgx"] } lc-scheduled-enclave = { path = "../litentry/core/scheduled-enclave", default-features = false, features = ["sgx"] } litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-macros = { path = "../../primitives/core/macros", default-features = false } diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index 402cf1f474..136a071b75 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -52,6 +52,7 @@ use crate::{ Hash, }; use base58::ToBase58; +use bc_relayer_registry::{RelayerRegistryUpdater, GLOBAL_RELAYER_REGISTRY}; use bc_task_receiver::{run_bit_across_handler_runner, BitAcrossTaskContext}; use codec::Encode; use core::str::FromStr; @@ -101,6 +102,7 @@ pub(crate) fn init_enclave( base_dir: PathBuf, ) -> EnclaveResult<()> { let signing_key_repository = Arc::new(get_ed25519_repository(base_dir.clone())?); + GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT.initialize(signing_key_repository.clone()); let signer = signing_key_repository.retrieve_key()?; info!("[Enclave initialized] Ed25519 prim raw : {:?}", signer.public().0); @@ -228,6 +230,8 @@ pub(crate) fn init_enclave( Arc::new(IntelAttestationHandler::new(ocall_api, signing_key_repository)); GLOBAL_ATTESTATION_HANDLER_COMPONENT.initialize(attestation_handler); + GLOBAL_RELAYER_REGISTRY.init().map_err(|e| Error::Other(e.into()))?; + std::thread::spawn(move || run_bit_across_handler().unwrap()); Ok(()) diff --git a/bitacross-worker/litentry/primitives/Cargo.toml b/bitacross-worker/litentry/primitives/Cargo.toml index 076b5aab29..6de056d426 100644 --- a/bitacross-worker/litentry/primitives/Cargo.toml +++ b/bitacross-worker/litentry/primitives/Cargo.toml @@ -8,6 +8,7 @@ version = "0.1.0" bitcoin = { version = "0.31.0", default-features = false, features = ["secp-recovery", "no-std"] } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } hex = { version = "0.4.3", default-features = false } +lazy_static = { version = "1.1.0", features = ["spin_no_std"] } log = { version = "0.4", default-features = false } pallet-evm = { default-features = false, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } rand = { version = "0.7", optional = true } @@ -22,13 +23,19 @@ sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkad sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } strum = { version = "0.26", default-features = false } strum_macros = { version = "0.26", default-features = false } +thiserror = { version = "1.0.26", optional = true } + # sgx dependencies base64_sgx = { package = "base64", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base64-sgx", optional = true } sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true, features = ["net", "thread"] } +thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } + # internal dependencies +itp-settings = { path = "../../core-primitives/settings", default-features = false } itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } +itp-sgx-io = { path = "../../core-primitives/sgx/io", default-features = false } itp-utils = { path = "../../core-primitives/utils", default-features = false } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../primitives/core", default-features = false } @@ -46,6 +53,8 @@ sgx = [ "sgx_tstd", "rand-sgx", "itp-sgx-crypto/sgx", + "thiserror-sgx", + "itp-sgx-io/sgx", ] std = [ "strum/std", @@ -64,4 +73,6 @@ std = [ "log/std", "bitcoin/std", "secp256k1/std", + "thiserror", + "itp-sgx-io/std", ] diff --git a/node/src/chain_specs/rococo.rs b/node/src/chain_specs/rococo.rs index 973640a429..433b77769a 100644 --- a/node/src/chain_specs/rococo.rs +++ b/node/src/chain_specs/rococo.rs @@ -17,10 +17,10 @@ use super::*; use cumulus_primitives_core::ParaId; use rococo_parachain_runtime::{ - AccountId, AuraId, Balance, BalancesConfig, CouncilMembershipConfig, GenesisConfig, - ParachainInfoConfig, ParachainStakingConfig, PolkadotXcmConfig, SessionConfig, SudoConfig, - SystemConfig, TechnicalCommitteeMembershipConfig, TeebagConfig, TeebagOperationalMode, - TeerexConfig, VCManagementConfig, UNIT, WASM_BINARY, + AccountId, AuraId, Balance, BalancesConfig, BitacrossConfig, CouncilMembershipConfig, + GenesisConfig, ParachainInfoConfig, ParachainStakingConfig, PolkadotXcmConfig, SessionConfig, + SudoConfig, SystemConfig, TechnicalCommitteeMembershipConfig, TeebagConfig, + TeebagOperationalMode, TeerexConfig, VCManagementConfig, UNIT, WASM_BINARY, }; use sc_service::ChainType; use sc_telemetry::TelemetryEndpoints; @@ -252,8 +252,9 @@ fn generate_genesis( evm: Default::default(), teebag: TeebagConfig { allow_sgx_debug_mode: true, - admin: Some(root_key), + admin: Some(root_key.clone()), mode: TeebagOperationalMode::Development, }, + bitacross: BitacrossConfig { admin: Some(root_key) }, } } diff --git a/pallets/bitacross-pallet/src/lib.rs b/pallets/bitacross-pallet/src/lib.rs deleted file mode 100644 index b33abf6a4f..0000000000 --- a/pallets/bitacross-pallet/src/lib.rs +++ /dev/null @@ -1,68 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! TODO: event/error handling -//! Currently the errors are synchronously emitted from this pallet itself, -//! meanwhile we have the `SomeError` **Event** which is callable from TEE -//! to represent any generic "error". -//! However, there are so many error cases in TEE that I'm not even sure -//! if it's a good idea to have a matching extrinsic for error propagation. -//! -//! The reasons that we don't use pallet_teerex::call_worker directly are: -//! - call teerex::call_worker inside IMP won't trigger the handler, because it's not called as -//! extrinsics so won't be scraped -//! - the origin is discarded in call_worker but we need it -//! - to simplify the F/E usage, we only need to encrypt the needed parameters (see e.g. -//! shield_funds) - -#![cfg_attr(not(feature = "std"), no_std)] -#![allow(unused_variables)] -#![allow(clippy::let_unit_value, deprecated)] - -pub use pallet::*; -#[frame_support::pallet] -pub mod pallet { - use frame_support::pallet_prelude::*; - use frame_system::pallet_prelude::*; - - #[pallet::pallet] - pub struct Pallet(_); - - #[pallet::config] - pub trait Config: frame_system::Config { - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - // some extrinsics should only be called by origins from TEE - type TEECallOrigin: EnsureOrigin; - // origin that is allowed to call extrinsics - type ExtrinsicWhitelistOrigin: EnsureOrigin; - } - - #[pallet::event] - pub enum Event {} - - #[pallet::error] - pub enum Error {} - - #[pallet::call] - impl Pallet { - /// add an account to the delegatees - #[pallet::call_index(0)] - #[pallet::weight(10000000)] - pub fn placeholder(origin: OriginFor, account: T::AccountId) -> DispatchResult { - Ok(()) - } - } -} diff --git a/pallets/bitacross-pallet/Cargo.toml b/pallets/bitacross/Cargo.toml similarity index 76% rename from pallets/bitacross-pallet/Cargo.toml rename to pallets/bitacross/Cargo.toml index d52a5f6541..baa1f201bf 100644 --- a/pallets/bitacross-pallet/Cargo.toml +++ b/pallets/bitacross/Cargo.toml @@ -6,38 +6,24 @@ name = "pallet-bitacross" repository = 'https://github.com/litentry/litentry-parachain' version = '0.1.0' -# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html - [dependencies] # third-party dependencies codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } -pallet-teerex = { path = "../teerex", default-features = false } scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } -# primitives +frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false, optional = true } +frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } +frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -# frame dependencies -frame-support = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -frame-system = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } - -# benchmarking -frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false, optional = true } -test-utils = { path = "../test-utils", default-features = false, optional = true } - -# local core-primitives = { path = "../../primitives/core", default-features = false } -teerex-primitives = { path = "../../primitives/teerex", default-features = false } [dev-dependencies] pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } -pallet-group = { path = "../../pallets/group" } -pallet-teerex = { path = "../teerex" } pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } -test-utils = { path = "../test-utils" } [features] default = ["std"] @@ -46,10 +32,6 @@ runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", "pallet-timestamp/runtime-benchmarks", - "test-utils", -] -skip-ias-check = [ - "pallet-teerex/skip-ias-check", ] std = [ "codec/std", @@ -62,7 +44,5 @@ std = [ "frame-system/std", "frame-benchmarking?/std", "core-primitives/std", - "teerex-primitives/std", - "pallet-teerex/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/bitacross/src/custodial_wallet.rs b/pallets/bitacross/src/custodial_wallet.rs new file mode 100644 index 0000000000..f20424124a --- /dev/null +++ b/pallets/bitacross/src/custodial_wallet.rs @@ -0,0 +1,36 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use codec::{Decode, Encode}; +use core_primitives::{Address20, Address33}; +use scale_info::TypeInfo; + +/// custodial wallet that each tee worker generates and holds +#[derive(Encode, Decode, Clone, Default, Debug, PartialEq, Eq, TypeInfo)] +pub struct CustodialWallet { + pub btc: Option, + pub eth: Option, +} + +impl CustodialWallet { + pub fn has_btc(&self) -> bool { + self.btc.is_some() + } + + pub fn has_eth(&self) -> bool { + self.eth.is_some() + } +} diff --git a/pallets/bitacross/src/lib.rs b/pallets/bitacross/src/lib.rs new file mode 100644 index 0000000000..11579d0ae6 --- /dev/null +++ b/pallets/bitacross/src/lib.rs @@ -0,0 +1,196 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use core_primitives::{Address20, Address33, Identity}; +use frame_support::{ + dispatch::{DispatchResult, DispatchResultWithPostInfo}, + ensure, + pallet_prelude::*, + traits::Get, +}; +use frame_system::pallet_prelude::*; + +pub use pallet::*; + +mod custodial_wallet; +pub use custodial_wallet::*; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(PhantomData); + + #[pallet::config] + pub trait Config: frame_system::Config { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + // some extrinsics should only be called by origins from TEE + type TEECallOrigin: EnsureOrigin; + // origin to manage Relayer Admin + type SetAdminOrigin: EnsureOrigin; + } + + #[pallet::storage] + #[pallet::getter(fn admin)] + pub type Admin = StorageValue<_, T::AccountId, OptionQuery>; + + // use `Identity` as key to keep the flexibility, however we do further check its type when + // adding them + #[pallet::storage] + #[pallet::getter(fn relayer)] + pub type Relayer = StorageMap<_, Blake2_128Concat, Identity, (), OptionQuery>; + + // `ValueQuery` is used as each field in CustodialWallet is optional already + // not using Option either as each field is set separately + #[pallet::storage] + #[pallet::getter(fn vault)] + pub type Vault = + StorageMap<_, Blake2_128Concat, T::AccountId, CustodialWallet, ValueQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + AdminSet { new_admin: Option }, + RelayerAdded { who: Identity }, + RelayerRemoved { who: Identity }, + BtcWalletGenerated { address: Address33 }, + EthWalletGenerated { address: Address20 }, + } + + #[pallet::error] + pub enum Error { + RequireAdminOrRoot, + RelayerNotExist, + UnsupportedRelayerType, + BtcWalletAlreadyExist, + EthWalletAlreadyExist, + } + + #[pallet::genesis_config] + pub struct GenesisConfig { + pub admin: Option, + } + + #[cfg(feature = "std")] + impl Default for GenesisConfig { + fn default() -> Self { + Self { admin: None } + } + } + + #[pallet::genesis_build] + impl GenesisBuild for GenesisConfig { + fn build(&self) { + if let Some(ref admin) = self.admin { + Admin::::put(admin); + } + } + } + + #[pallet::call] + impl Pallet { + /// Set the admin account + /// + /// Weights should be 2 DB writes: 1 for mode and 1 for event + #[pallet::call_index(0)] + #[pallet::weight((2 * T::DbWeight::get().write, DispatchClass::Normal, Pays::No))] + pub fn set_admin( + origin: OriginFor, + new_admin: T::AccountId, + ) -> DispatchResultWithPostInfo { + T::SetAdminOrigin::ensure_origin(origin)?; + Admin::::put(new_admin.clone()); + Self::deposit_event(Event::AdminSet { new_admin: Some(new_admin) }); + Ok(Pays::No.into()) + } + + #[pallet::call_index(1)] + #[pallet::weight({195_000_000})] + pub fn add_relayer(origin: OriginFor, account: Identity) -> DispatchResult { + Self::ensure_admin_or_root(origin)?; + ensure!(account.is_substrate() || account.is_evm(), Error::::UnsupportedRelayerType); + // we don't care if `account` already exists + Relayer::::insert(account.clone(), ()); + Self::deposit_event(Event::RelayerAdded { who: account }); + Ok(()) + } + + #[pallet::call_index(2)] + #[pallet::weight({195_000_000})] + pub fn remove_relayer(origin: OriginFor, account: Identity) -> DispatchResult { + Self::ensure_admin_or_root(origin)?; + ensure!(Relayer::::contains_key(&account), Error::::RelayerNotExist); + Relayer::::remove(account.clone()); + Self::deposit_event(Event::RelayerRemoved { who: account }); + Ok(()) + } + + /// --------------------------------------------------- + /// The following extrinsics are supposed to be called by TEE only + /// --------------------------------------------------- + #[pallet::call_index(30)] + #[pallet::weight(({195_000_000}, DispatchClass::Normal, Pays::No))] + pub fn btc_wallet_generated( + origin: OriginFor, + address: Address33, + ) -> DispatchResultWithPostInfo { + let tee_account = T::TEECallOrigin::ensure_origin(origin)?; + Vault::::try_mutate(tee_account, |v| { + ensure!(!v.has_btc(), Error::::BtcWalletAlreadyExist); + v.btc = Some(address); + Self::deposit_event(Event::BtcWalletGenerated { address }); + Ok(Pays::No.into()) + }) + } + + #[pallet::call_index(31)] + #[pallet::weight(({195_000_000}, DispatchClass::Normal, Pays::No))] + pub fn eth_wallet_generated( + origin: OriginFor, + address: Address20, + ) -> DispatchResultWithPostInfo { + let tee_account = T::TEECallOrigin::ensure_origin(origin)?; + Vault::::try_mutate(tee_account, |v| { + ensure!(!v.has_eth(), Error::::EthWalletAlreadyExist); + v.eth = Some(address); + Self::deposit_event(Event::EthWalletGenerated { address }); + Ok(Pays::No.into()) + }) + } + + // TODO: placeholder + #[pallet::call_index(32)] + #[pallet::weight(({195_000_000}, DispatchClass::Normal, Pays::No))] + pub fn task_complete(origin: OriginFor) -> DispatchResultWithPostInfo { + let _ = T::TEECallOrigin::ensure_origin(origin)?; + Ok(Pays::No.into()) + } + } +} + +impl Pallet { + fn ensure_admin_or_root(origin: OriginFor) -> DispatchResult { + ensure!( + ensure_root(origin.clone()).is_ok() || Some(ensure_signed(origin)?) == Self::admin(), + Error::::RequireAdminOrRoot + ); + Ok(()) + } +} diff --git a/primitives/core/src/identity.rs b/primitives/core/src/identity.rs index 77876b362a..499e58e7f0 100644 --- a/primitives/core/src/identity.rs +++ b/primitives/core/src/identity.rs @@ -59,7 +59,7 @@ impl Encode for IdentityString { } } -#[derive(Eq, PartialEq, Clone, MaxEncodedLen, Default)] +#[derive(Eq, PartialEq, Clone, MaxEncodedLen, Default, Ord, PartialOrd)] pub struct IdentityString { pub inner: IdentityInnerString, } @@ -91,7 +91,9 @@ impl Debug for IdentityString { } } -#[derive(Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive( + Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen, Ord, PartialOrd, +)] pub struct Address20([u8; 20]); impl AsRef<[u8; 20]> for Address20 { @@ -128,7 +130,9 @@ impl Debug for Address20 { } } -#[derive(Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive( + Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen, Ord, PartialOrd, +)] pub struct Address32([u8; 32]); impl AsRef<[u8; 32]> for Address32 { fn as_ref(&self) -> &[u8; 32] { @@ -197,7 +201,7 @@ impl Debug for Address32 { } // TODO: maybe use macros to reduce verbosity -#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen)] +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen, PartialOrd, Ord)] pub struct Address33([u8; 33]); impl AsRef<[u8; 33]> for Address33 { fn as_ref(&self) -> &[u8; 33] { @@ -261,7 +265,9 @@ impl Debug for Address33 { /// Web2 and Web3 Identity based on handle/public key /// We only include the network categories (substrate/evm) without concrete types /// see https://github.com/litentry/litentry-parachain/issues/1841 -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen, EnumIter)] +#[derive( + Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen, EnumIter, Ord, PartialOrd, +)] pub enum Identity { // web2 #[codec(index = 0)] diff --git a/runtime/rococo/Cargo.toml b/runtime/rococo/Cargo.toml index e8c0c0bd73..b2031d6872 100644 --- a/runtime/rococo/Cargo.toml +++ b/runtime/rococo/Cargo.toml @@ -88,7 +88,7 @@ frame-system-benchmarking = { git = "https://github.com/paritytech/substrate", d # Rococo pallets pallet-account-fix = { path = "../../pallets/account-fix", default-features = false } pallet-asset-manager = { path = "../../pallets/xcm-asset-manager", default-features = false } -pallet-bitacross = { path = "../../pallets/bitacross-pallet", default-features = false } +pallet-bitacross = { path = "../../pallets/bitacross", default-features = false } pallet-bridge = { path = "../../pallets/bridge", default-features = false } pallet-bridge-transfer = { path = "../../pallets/bridge-transfer", default-features = false } pallet-drop3 = { path = "../../pallets/drop3", default-features = false } diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 999043c0e9..fe51e24651 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -1040,11 +1040,10 @@ impl pallet_identity_management::Config for Runtime { type ExtrinsicWhitelistOrigin = IMPExtrinsicWhitelist; } -// NOTE: Use this for bitacross-pallet impl pallet_bitacross::Config for Runtime { type RuntimeEvent = RuntimeEvent; type TEECallOrigin = EnsureEnclaveSigner; - type ExtrinsicWhitelistOrigin = IMPExtrinsicWhitelist; + type SetAdminOrigin = EnsureRootOrAllCouncil; } impl pallet_group::Config for Runtime { @@ -1265,7 +1264,7 @@ construct_runtime! { VCManagement: pallet_vc_management = 66, IMPExtrinsicWhitelist: pallet_group:: = 67, VCMPExtrinsicWhitelist: pallet_group:: = 68, - BitAcross: pallet_bitacross = 70, + Bitacross: pallet_bitacross = 70, // TEE Teerex: pallet_teerex = 90, @@ -1369,7 +1368,8 @@ impl Contains for NormalModeFilter { // So no EVM pallet RuntimeCall::Ethereum(_) | // AccountFix - RuntimeCall::AccountFix(_) + RuntimeCall::AccountFix(_) | + RuntimeCall::Bitacross(_) ) } } diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index fdd91b00d3..f2890e6a49 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -8736,13 +8736,11 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "pallet-teerex", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] From aaccf7da504fedeb29c77a427b40714bef68ef3e Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Sun, 11 Feb 2024 23:35:39 +0100 Subject: [PATCH 24/64] Decouple woker_type and worker_mode (#2476) * add worker_mode * adjust test * update extrinsic param --- pallets/teebag/src/lib.rs | 20 ++++++++++++------- pallets/teebag/src/tests.rs | 15 +++++++++++++- pallets/teebag/src/types.rs | 16 +++++++++++---- tee-worker/Cargo.lock | 3 +++ .../core-primitives/settings/Cargo.toml | 6 ++++-- .../settings/src/worker_mode.rs | 7 +------ tee-worker/enclave-runtime/Cargo.lock | 3 +++ tee-worker/enclave-runtime/src/attestation.rs | 8 +++++++- tee-worker/litentry/primitives/src/lib.rs | 2 +- 9 files changed, 58 insertions(+), 22 deletions(-) diff --git a/pallets/teebag/src/lib.rs b/pallets/teebag/src/lib.rs index ecdc96deaa..7d60354b30 100644 --- a/pallets/teebag/src/lib.rs +++ b/pallets/teebag/src/lib.rs @@ -135,17 +135,18 @@ pub mod pallet { EnclaveIdentifierNotExist, /// The enclave identifier already exists. EnclaveIdentifierAlreadyExist, - /// when we try to re-register an existing enclave with a differnet worker type - WorkerTypeNotAllowed, + /// The worker type is unexpected, becuase e.g. when we try to re-register an + /// existing enclave with a differnet worker type + UnexpectedWorkerType, /// The shard doesn't match the enclave. WrongMrenclaveForShard, /// The worker url is too long. EnclaveUrlTooLong, /// The raw attestation data is too long. AttestationTooLong, - /// The worker type is unexpected, because e.g. a non-sidechain worker calls sidechain - /// specific extrinsic - UnexpectedWorkerType, + /// The worker mode is unexpected, because e.g. a non-sidechain worker calls + /// sidechain specific extrinsic + UnexpectedWorkerMode, /// Can not found the desired scheduled enclave. ScheduledEnclaveNotExist, /// Enclave not in the scheduled list, therefore unexpected. @@ -412,6 +413,7 @@ pub mod pallet { pub fn register_enclave( origin: OriginFor, worker_type: WorkerType, + worker_mode: WorkerMode, attestation: Vec, worker_url: Vec, shielding_pubkey: Option>, @@ -423,6 +425,7 @@ pub mod pallet { ensure!(worker_url.len() <= MAX_URL_LEN, Error::::EnclaveUrlTooLong); let mut enclave = Enclave::new(worker_type) + .with_worker_mode(worker_mode) .with_url(worker_url) .with_shielding_pubkey(shielding_pubkey) .with_vc_pubkey(vc_pubkey) @@ -564,7 +567,10 @@ pub mod pallet { Error::::WrongMrenclaveForShard ); - ensure!(sender_enclave.worker_type.is_sidechain(), Error::::UnexpectedWorkerType,); + ensure!( + sender_enclave.worker_mode == WorkerMode::Sidechain, + Error::::UnexpectedWorkerMode + ); sender_enclave.last_seen_timestamp = Self::now().saturated_into(); @@ -626,7 +632,7 @@ impl Pallet { match EnclaveRegistry::::get(sender) { Some(old_enclave) => ensure!( old_enclave.worker_type == enclave.worker_type, - Error::::WorkerTypeNotAllowed + Error::::UnexpectedWorkerType ), None => Self::add_enclave_identifier(enclave.worker_type, sender)?, }; diff --git a/pallets/teebag/src/tests.rs b/pallets/teebag/src/tests.rs index 4930c9d2fd..086ea4eb74 100644 --- a/pallets/teebag/src/tests.rs +++ b/pallets/teebag/src/tests.rs @@ -90,6 +90,7 @@ fn register_enclave_dev_works_with_no_scheduled_enclave() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(alice()), Default::default(), + Default::default(), TEST4_MRENCLAVE.to_vec(), URL.to_vec(), None, @@ -114,6 +115,7 @@ fn register_enclave_dev_works_with_sgx_build_mode_debug() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer4.clone()), Default::default(), + Default::default(), TEST4_CERT.to_vec(), URL.to_vec(), None, @@ -152,6 +154,7 @@ fn parentchain_block_processed_works() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer7.clone()), WorkerType::BitAcross, + Default::default(), TEST7_CERT.to_vec(), URL.to_vec(), None, @@ -195,6 +198,7 @@ fn register_dcap_enclave_works() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer.clone()), WorkerType::Identity, + Default::default(), TEST1_DCAP_QUOTE.to_vec(), URL.to_vec(), None, @@ -227,6 +231,7 @@ fn register_enclave_prod_works_with_sgx_build_mode_debug() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer4.clone()), Default::default(), + Default::default(), TEST4_CERT.to_vec(), URL.to_vec(), None, @@ -261,6 +266,7 @@ fn register_enclave_prod_works_with_sgx_build_mode_production() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer8.clone()), Default::default(), + Default::default(), TEST8_CERT.to_vec(), URL.to_vec(), None, @@ -287,6 +293,7 @@ fn register_enclave_prod_fails_with_wrong_attestation_type() { Teebag::register_enclave( RuntimeOrigin::signed(alice()), Default::default(), + Default::default(), TEST4_MRENCLAVE.to_vec(), URL.to_vec(), None, @@ -307,6 +314,7 @@ fn register_enclave_prod_fails_with_no_scheduled_enclave() { Teebag::register_enclave( RuntimeOrigin::signed(signer), Default::default(), + Default::default(), TEST4_CERT.to_vec(), URL.to_vec(), None, @@ -333,6 +341,7 @@ fn register_enclave_prod_fails_with_max_limit_reached() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer5.clone()), WorkerType::BitAcross, + Default::default(), TEST5_CERT.to_vec(), URL.to_vec(), None, @@ -344,6 +353,7 @@ fn register_enclave_prod_fails_with_max_limit_reached() { Teebag::register_enclave( RuntimeOrigin::signed(signer6.clone()), WorkerType::BitAcross, + Default::default(), TEST6_CERT.to_vec(), URL.to_vec(), None, @@ -359,13 +369,14 @@ fn register_enclave_prod_fails_with_max_limit_reached() { Teebag::register_enclave( RuntimeOrigin::signed(signer5.clone()), WorkerType::Identity, + Default::default(), TEST5_CERT.to_vec(), URL.to_vec(), None, None, AttestationType::Ias, ), - Error::::WorkerTypeNotAllowed + Error::::UnexpectedWorkerType ); // remove and re-register it should work @@ -374,6 +385,7 @@ fn register_enclave_prod_fails_with_max_limit_reached() { assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(signer5), WorkerType::Identity, + Default::default(), TEST5_CERT.to_vec(), URL.to_vec(), None, @@ -386,6 +398,7 @@ fn register_enclave_prod_fails_with_max_limit_reached() { Teebag::register_enclave( RuntimeOrigin::signed(signer6), WorkerType::Identity, + Default::default(), TEST6_CERT.to_vec(), URL.to_vec(), None, diff --git a/pallets/teebag/src/types.rs b/pallets/teebag/src/types.rs index c54421b546..9320f1d826 100644 --- a/pallets/teebag/src/types.rs +++ b/pallets/teebag/src/types.rs @@ -74,10 +74,12 @@ pub enum WorkerType { BitAcross, } -impl WorkerType { - pub fn is_sidechain(&self) -> bool { - self == &Self::Identity - } +#[derive(Encode, Decode, Clone, Copy, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] +pub enum WorkerMode { + #[default] + OffChainWorker, + Sidechain, + Teeracle, } #[derive(Encode, Decode, Copy, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] @@ -99,6 +101,7 @@ pub struct SidechainBlockConfirmation { #[derive(Encode, Decode, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] pub struct Enclave { pub worker_type: WorkerType, + pub worker_mode: WorkerMode, pub mrenclave: MrEnclave, pub last_seen_timestamp: u64, // unix epoch in milliseconds when it's last seen pub url: Vec, // utf8 encoded url @@ -113,6 +116,11 @@ impl Enclave { Enclave { worker_type, ..Default::default() } } + pub fn with_worker_mode(mut self, worker_mode: WorkerMode) -> Self { + self.worker_mode = worker_mode; + self + } + pub fn with_mrenclave(mut self, mrenclave: MrEnclave) -> Self { self.mrenclave = mrenclave; self diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index f2890e6a49..02b90b7393 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -5156,6 +5156,9 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" +dependencies = [ + "litentry-primitives", +] [[package]] name = "itp-sgx-crypto" diff --git a/tee-worker/core-primitives/settings/Cargo.toml b/tee-worker/core-primitives/settings/Cargo.toml index bf48cd4ec2..2d36658a7e 100644 --- a/tee-worker/core-primitives/settings/Cargo.toml +++ b/tee-worker/core-primitives/settings/Cargo.toml @@ -5,10 +5,12 @@ authors = ['Trust Computing GmbH ', 'Integritee AG WorkerMode; diff --git a/tee-worker/enclave-runtime/Cargo.lock b/tee-worker/enclave-runtime/Cargo.lock index f5cd1bddbc..9844b6a377 100644 --- a/tee-worker/enclave-runtime/Cargo.lock +++ b/tee-worker/enclave-runtime/Cargo.lock @@ -2379,6 +2379,9 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" +dependencies = [ + "litentry-primitives", +] [[package]] name = "itp-sgx-crypto" diff --git a/tee-worker/enclave-runtime/src/attestation.rs b/tee-worker/enclave-runtime/src/attestation.rs index 59810d8abd..7eaf912e40 100644 --- a/tee-worker/enclave-runtime/src/attestation.rs +++ b/tee-worker/enclave-runtime/src/attestation.rs @@ -47,7 +47,10 @@ use itp_node_api::metadata::{ Error as MetadataError, }; use itp_node_api_metadata::NodeMetadata; -use itp_settings::worker::MR_ENCLAVE_SIZE; +use itp_settings::{ + worker::MR_ENCLAVE_SIZE, + worker_mode::{ProvideWorkerMode, WorkerModeProvider}, +}; use itp_sgx_crypto::{ ed25519_derivation::DeriveEd25519, key_repository::AccessKey, Error as SgxCryptoError, }; @@ -332,6 +335,7 @@ pub fn generate_dcap_ra_extrinsic_from_quote_internal( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::Identity, + WorkerModeProvider::worker_mode(), quote, url, shielding_pubkey, @@ -361,6 +365,7 @@ pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::Identity, + WorkerModeProvider::worker_mode(), quote, url, shielding_pubkey, @@ -401,6 +406,7 @@ pub fn generate_ias_ra_extrinsic_from_der_cert_internal( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::Identity, + WorkerModeProvider::worker_mode(), cert_der, url, shielding_pubkey, diff --git a/tee-worker/litentry/primitives/src/lib.rs b/tee-worker/litentry/primitives/src/lib.rs index 82b9c77c64..f050262683 100644 --- a/tee-worker/litentry/primitives/src/lib.rs +++ b/tee-worker/litentry/primitives/src/lib.rs @@ -45,7 +45,7 @@ use litentry_hex_utils::hex_encode; use log::error; pub use pallet_teebag::{ decl_rsa_request, extract_tcb_info_from_raw_dcap_quote, AttestationType, Enclave, - EnclaveFingerprint, MrEnclave, ShardIdentifier, SidechainBlockNumber, WorkerType, + EnclaveFingerprint, MrEnclave, ShardIdentifier, SidechainBlockNumber, WorkerMode, WorkerType, }; pub use parentchain_primitives::{ all_bitcoin_web3networks, all_evm_web3networks, all_substrate_web3networks, all_web3networks, From 800cca78eb960f825287937fab3ffb521661fe6d Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Mon, 12 Feb 2024 10:13:20 +0100 Subject: [PATCH 25/64] fix ecdsa and schnorr tests (#2478) * fix ecdsa and schnorr tests * fmt --------- Co-authored-by: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> --- .../core-primitives/sgx/crypto/src/ecdsa.rs | 58 +++++++++++++------ .../core-primitives/sgx/crypto/src/lib.rs | 15 ++++- .../core-primitives/sgx/crypto/src/schnorr.rs | 57 +++++++++++++----- .../enclave-runtime/src/test/tests_main.rs | 8 +++ 4 files changed, 103 insertions(+), 35 deletions(-) diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index 3c06ebec8a..ee2a68c451 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -27,7 +27,7 @@ use std::{string::ToString, vec::Vec}; /// File name of the sealed seed file. pub const SEALED_SIGNER_SEED_FILE: &str = "ecdsa_key_sealed.bin"; -#[derive(Clone, PartialEq)] +#[derive(Clone, PartialEq, Eq)] pub struct Pair { pub public: PublicKey, private: SigningKey, @@ -88,18 +88,16 @@ pub mod sgx { self.base_path .join(self.key_file_prefix.clone() + "_" + SEALED_SIGNER_SEED_FILE) } - } - impl Seal { fn unseal_pair(&self) -> Result { self.unseal() } - fn exists(&self) -> bool { + pub fn exists(&self) -> bool { self.path().exists() } - fn init(&self) -> Result { + pub fn init(&self) -> Result { if !self.exists() { info!("Keyfile not found, creating new! {}", self.path().display()); let mut seed = [0u8; 32]; @@ -133,9 +131,16 @@ pub mod sgx { #[cfg(feature = "test")] pub mod sgx_tests { + use super::sgx::*; + use crate::{ + create_ecdsa_repository, key_repository::AccessKey, std::string::ToString, Pair, Seal, + }; + use itp_sgx_temp_dir::TempDir; + use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use sgx_tstd::path::PathBuf; - #[test] - pub fn creating_repository_with_same_path_and_prefix_results_in_same_key() { + pub fn ecdsa_creating_repository_with_same_path_and_prefix_results_in_same_key() { + //given let key_file_prefix = "test"; fn get_key_from_repo(path: PathBuf, prefix: &str) -> Pair { create_ecdsa_repository(path, prefix).unwrap().retrieve_key().unwrap() @@ -145,17 +150,19 @@ pub mod sgx_tests { ) .unwrap(); let temp_path = temp_dir.path().to_path_buf(); - assert_eq!( - get_key_from_repo(temp_path.clone(), key_file_prefix), - get_key_from_repo(temp_path.clone(), key_file_prefix) - ); + + //when + let first_key = get_key_from_repo(temp_path.clone(), key_file_prefix); + let second_key = get_key_from_repo(temp_path.clone(), key_file_prefix); + + //then + assert_eq!(first_key.public, second_key.public); } - #[test] - pub fn seal_init_should_create_new_key_if_not_present() { + pub fn ecdsa_seal_init_should_create_new_key_if_not_present() { //given let temp_dir = - TempDir::with_prefix("seal_init_should_create_new_key_if_not_present").unwrap(); + TempDir::with_prefix("ecdsa_seal_init_should_create_new_key_if_not_present").unwrap(); let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); assert!(!seal.exists()); @@ -166,10 +173,10 @@ pub mod sgx_tests { assert!(seal.exists()); } - #[test] - pub fn seal_init_should_not_change_key_if_exists() { + pub fn ecdsa_seal_init_should_not_change_key_if_exists() { //given - let temp_dir = TempDir::with_prefix("seal_init_should_not_change_key_if_exists").unwrap(); + let temp_dir = + TempDir::with_prefix("ecdsa_seal_init_should_not_change_key_if_exists").unwrap(); let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); let pair = seal.init().unwrap(); @@ -177,6 +184,21 @@ pub mod sgx_tests { let new_pair = seal.init().unwrap(); //then - assert!(pair, new_pair); + assert_eq!(pair.public, new_pair.public); + } + + pub fn ecdsa_sign_should_produce_valid_signature() { + //given + let temp_dir = TempDir::with_prefix("ecdsa_sign_should_produce_valid_signature").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + let pair = seal.init().unwrap(); + let message = [1; 32]; + + //when + let signature = Signature::from_slice(&pair.sign(&message).unwrap()).unwrap(); + + //then + let verifying_key = VerifyingKey::from(&pair.private); + assert!(verifying_key.verify(&message, &signature).is_ok()); } } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs index 5f18dd86c0..27b6b99028 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/lib.rs @@ -42,7 +42,7 @@ pub mod rsa3072; pub mod schnorr; pub mod traits; -pub use self::{aes::*, ed25519::*, rsa3072::*}; +pub use self::{aes::*, ecdsa::*, ed25519::*, rsa3072::*}; pub use error::*; pub use traits::*; @@ -62,4 +62,17 @@ pub mod tests { pub use super::aes::sgx_tests::{ aes_sealing_works, using_get_aes_repository_twice_initializes_key_only_once, }; + + pub use super::ecdsa::sgx_tests::{ + ecdsa_creating_repository_with_same_path_and_prefix_results_in_same_key, + ecdsa_seal_init_should_create_new_key_if_not_present, + ecdsa_seal_init_should_not_change_key_if_exists, ecdsa_sign_should_produce_valid_signature, + }; + + pub use super::schnorr::sgx_tests::{ + schnorr_creating_repository_with_same_path_and_prefix_results_in_same_key, + schnorr_seal_init_should_create_new_key_if_not_present, + schnorr_seal_init_should_not_change_key_if_exists, + schnorr_sign_should_produce_valid_signature, + }; } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs index 2be85eef82..11b0ecd7ae 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs @@ -92,11 +92,11 @@ pub mod sgx { self.unseal() } - fn exists(&self) -> bool { + pub fn exists(&self) -> bool { self.path().exists() } - fn init(&self) -> Result { + pub fn init(&self) -> Result { if !self.exists() { info!("Keyfile not found, creating new! {}", self.path().display()); let mut seed = [0u8; 32]; @@ -129,29 +129,39 @@ pub mod sgx { #[cfg(feature = "test")] pub mod sgx_tests { + use crate::{ + key_repository::AccessKey, + schnorr::{create_schnorr_repository, Pair, Seal}, + std::string::ToString, + }; + use itp_sgx_temp_dir::TempDir; + use k256::schnorr::{signature::Verifier, Signature, VerifyingKey}; + use std::path::PathBuf; - #[test] - pub fn creating_repository_with_same_path_and_prefix_results_in_same_key() { + pub fn schnorr_creating_repository_with_same_path_and_prefix_results_in_same_key() { + //given let key_file_prefix = "test"; fn get_key_from_repo(path: PathBuf, prefix: &str) -> Pair { create_schnorr_repository(path, prefix).unwrap().retrieve_key().unwrap() } let temp_dir = TempDir::with_prefix( - "creating_repository_with_same_path_and_prefix_results_in_same_key", + "schnorr_creating_repository_with_same_path_and_prefix_results_in_same_key", ) .unwrap(); let temp_path = temp_dir.path().to_path_buf(); - assert_eq!( - get_key_from_repo(temp_path.clone(), key_file_prefix), - get_key_from_repo(temp_path.clone(), key_file_prefix) - ); + + //when + let first_key = get_key_from_repo(temp_path.clone(), key_file_prefix); + let second_key = get_key_from_repo(temp_path.clone(), key_file_prefix); + + //then + assert_eq!(first_key.public, second_key.public); } - #[test] - pub fn seal_init_should_create_new_key_if_not_present() { + pub fn schnorr_seal_init_should_create_new_key_if_not_present() { //given let temp_dir = - TempDir::with_prefix("seal_init_should_create_new_key_if_not_present").unwrap(); + TempDir::with_prefix("schnorr_seal_init_should_create_new_key_if_not_present").unwrap(); let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); assert!(!seal.exists()); @@ -162,10 +172,10 @@ pub mod sgx_tests { assert!(seal.exists()); } - #[test] - pub fn seal_init_should_not_change_key_if_exists() { + pub fn schnorr_seal_init_should_not_change_key_if_exists() { //given - let temp_dir = TempDir::with_prefix("seal_init_should_not_change_key_if_exists").unwrap(); + let temp_dir = + TempDir::with_prefix("schnorr_seal_init_should_not_change_key_if_exists").unwrap(); let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); let pair = seal.init().unwrap(); @@ -173,6 +183,21 @@ pub mod sgx_tests { let new_pair = seal.init().unwrap(); //then - assert!(pair, new_pair); + assert_eq!(pair.public, new_pair.public); + } + + pub fn schnorr_sign_should_produce_valid_signature() { + //given + let temp_dir = TempDir::with_prefix("ecdsa_sign_should_produce_valid_signature").unwrap(); + let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); + let pair = seal.init().unwrap(); + let message = [1; 32]; + + //when + let signature = Signature::try_from(pair.sign(&message).unwrap().as_slice()).unwrap(); + + //then + let verifying_key = VerifyingKey::try_from(&pair.public).unwrap(); + assert!(verifying_key.verify(&message, &signature).is_ok()); } } diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index 21b9b0aad6..d5f7b167da 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -99,6 +99,14 @@ pub extern "C" fn test_main_entrance() -> size_t { itp_sgx_crypto::tests::using_get_ed25519_repository_twice_initializes_key_only_once, itp_sgx_crypto::tests::rsa3072_sealing_works, itp_sgx_crypto::tests::using_get_rsa3072_repository_twice_initializes_key_only_once, + itp_sgx_crypto::tests::ecdsa_creating_repository_with_same_path_and_prefix_results_in_same_key, + itp_sgx_crypto::tests::ecdsa_seal_init_should_create_new_key_if_not_present, + itp_sgx_crypto::tests::ecdsa_seal_init_should_not_change_key_if_exists, + itp_sgx_crypto::tests::ecdsa_sign_should_produce_valid_signature, + itp_sgx_crypto::tests::schnorr_creating_repository_with_same_path_and_prefix_results_in_same_key, + itp_sgx_crypto::tests::schnorr_seal_init_should_create_new_key_if_not_present, + itp_sgx_crypto::tests::schnorr_seal_init_should_not_change_key_if_exists, + itp_sgx_crypto::tests::schnorr_sign_should_produce_valid_signature, test_compose_block, test_submit_trusted_call_to_top_pool, test_submit_trusted_getter_to_top_pool, From 943345ca34a67ecfb65ea850778573ecca75026b Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Mon, 12 Feb 2024 12:42:25 +0100 Subject: [PATCH 26/64] Adjust bc-worker counterpart of pallet-teebag (#2479) * fix: add extrinsic and parsing for indirect executor * fix: seal unseal operations for GLOBAL RELAYER REGISTRY * fix: update indirect executor to update Relayer Registry * feat: add relayer account type * debug: misc * debug: fix event * debug: change pallet folder name * debug: fmt * debug: using existing struct * debug: fmt * pallet-bitacross skeleton * runtime integration * some more update * fix: update indirect call executor and registry to use Identity instead of AccountId * fix: clippy * refactor: remove accidental commit * refactor: remove error log * refactor: remove comments * init * init impl * compiles * fix compile error * clippy unreachable --------- Co-authored-by: felixfaisal Co-authored-by: Minqi Wang Co-authored-by: WMQ <46511820+wangminqi@users.noreply.github.com> --- bitacross-worker/Cargo.lock | 28 ++-- .../litentry/scheduled_enclave.rs | 27 +++- .../src/indirect_calls/mod.rs | 2 +- .../src/integritee/mod.rs | 39 ++--- .../app-libs/sgx-runtime/src/lib.rs | 3 +- .../app-libs/stf/src/trusted_call.rs | 18 +-- .../core/bc-relayer-registry/Cargo.toml | 2 - .../core/bc-relayer-registry/src/lib.rs | 1 - bitacross-worker/cli/lit_parentchain_nonce.sh | 2 +- .../cli/src/base_cli/commands/listen.rs | 73 ++++----- bitacross-worker/cli/src/base_cli/mod.rs | 42 +++-- .../core-primitives/enclave-api/Cargo.toml | 2 +- .../enclave-api/src/enclave_base.rs | 4 +- .../enclave-api/src/remote_attestation.rs | 4 +- .../node-api/api-client-extensions/src/lib.rs | 2 + .../src/pallet_teebag.rs | 134 +++++++++++++++ .../node-api/metadata/src/lib.rs | 14 +- .../node-api/metadata/src/metadata_mocks.rs | 123 +++++--------- .../node-api/metadata/src/pallet_teebag.rs | 71 ++++++++ .../core-primitives/settings/Cargo.toml | 6 +- .../settings/src/worker_mode.rs | 6 +- .../sgx-runtime-primitives/Cargo.toml | 5 - .../sgx-runtime-primitives/src/types.rs | 20 --- .../core-primitives/test/Cargo.toml | 4 +- .../test/src/mock/onchain_mock.rs | 43 +++-- .../core-primitives/types/src/lib.rs | 27 +--- .../indirect-calls-executor/src/executor.rs | 72 ++------- .../indirect-calls-executor/src/mock.rs | 6 +- bitacross-worker/core/rpc-client/Cargo.toml | 1 - .../core/rpc-client/src/direct_client.rs | 6 +- bitacross-worker/core/rpc-client/src/mock.rs | 2 +- bitacross-worker/enclave-runtime/Cargo.lock | 152 ++++++++++++++---- .../enclave-runtime/src/attestation.rs | 69 +++++--- .../parentchain/integritee_parachain.rs | 1 + .../parentchain/integritee_solochain.rs | 1 + .../parentchain/target_a_parachain.rs | 1 + .../parentchain/target_a_solochain.rs | 1 + .../parentchain/target_b_parachain.rs | 1 + .../parentchain/target_b_solochain.rs | 1 + bitacross-worker/enclave-runtime/src/lib.rs | 2 +- .../src/rpc/worker_api_direct.rs | 19 ++- .../src/test/fixtures/components.rs | 10 +- .../enclave-runtime/src/test/tests_main.rs | 1 + .../src/test/top_pool_tests.rs | 40 ++--- .../src/tls_ra/tls_ra_server.rs | 1 + .../litentry/core/teebag-storage/Cargo.toml | 18 +++ .../litentry/core/teebag-storage/src/lib.rs | 48 ++++++ .../litentry/primitives/Cargo.toml | 7 +- .../litentry/primitives/src/aes_request.rs | 2 +- .../litentry/primitives/src/lib.rs | 5 +- bitacross-worker/service/Cargo.toml | 2 - bitacross-worker/service/src/main_impl.rs | 141 ++++++---------- .../service/src/sidechain_setup.rs | 4 +- bitacross-worker/service/src/sync_state.rs | 24 +-- bitacross-worker/service/src/tests/mock.rs | 47 +++--- .../src/tests/mocks/enclave_api_mock.rs | 3 +- bitacross-worker/service/src/worker.rs | 7 +- .../sidechain/consensus/aura/src/lib.rs | 45 +++--- .../aura/src/test/block_importer_tests.rs | 8 +- .../consensus/aura/src/test/fixtures/mod.rs | 2 +- .../sidechain/consensus/aura/src/verifier.rs | 3 +- .../src/block_import_confirmation_handler.rs | 4 +- .../sidechain/consensus/slots/src/lib.rs | 64 ++------ .../sidechain/consensus/slots/src/mocks.rs | 11 +- .../sidechain/peer-fetch/Cargo.toml | 1 + .../peer-fetch/src/untrusted_peer_fetch.rs | 8 +- .../sidechain/validateer-fetch/Cargo.toml | 7 +- .../validateer-fetch/src/validateer.rs | 56 ++----- 68 files changed, 848 insertions(+), 758 deletions(-) create mode 100644 bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs create mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs create mode 100644 bitacross-worker/litentry/core/teebag-storage/Cargo.toml create mode 100644 bitacross-worker/litentry/core/teebag-storage/src/lib.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 730e62fd53..4a3f99453f 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -622,7 +622,6 @@ dependencies = [ "sp-std 5.0.0", "strum 0.26.1", "strum_macros 0.26.1", - "teerex-primitives", "thiserror 1.0.44", "thiserror 1.0.9", ] @@ -839,7 +838,6 @@ dependencies = [ "serde 1.0.193", "serde_derive 1.0.193", "serde_json 1.0.103", - "sgx-verify", "sgx_crypto_helper", "sgx_types", "sp-consensus-grandpa", @@ -847,7 +845,6 @@ dependencies = [ "sp-keyring", "sp-runtime", "substrate-api-client", - "teerex-primitives", "thiserror 1.0.44", "tokio", "warp", @@ -5013,7 +5010,6 @@ dependencies = [ "serde_json 1.0.103", "sgx_crypto_helper", "sp-core", - "teerex-primitives", "thiserror 1.0.44", "url 2.4.0", "ws", @@ -5182,6 +5178,7 @@ dependencies = [ "itp-storage", "itp-types", "log 0.4.20", + "pallet-teebag", "parity-scale-codec", "serde_json 1.0.103", "sgx_crypto_helper", @@ -5189,7 +5186,6 @@ dependencies = [ "sgx_urts", "sp-core", "sp-runtime", - "teerex-primitives", "thiserror 1.0.44", ] @@ -5340,6 +5336,9 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" +dependencies = [ + "litentry-primitives", +] [[package]] name = "itp-sgx-crypto" @@ -5390,7 +5389,6 @@ name = "itp-sgx-runtime-primitives" version = "0.9.0" dependencies = [ "frame-system", - "litentry-primitives", "pallet-balances", "sp-core", "sp-runtime", @@ -5538,10 +5536,10 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-teerex-storage", "itp-time-utils", "itp-types", "jsonrpc-core 18.0.0 (git+https://github.com/scs/jsonrpc?branch=no_std_v18)", + "lc-teebag-storage", "litentry-primitives", "log 0.4.20", "parity-scale-codec", @@ -5802,6 +5800,7 @@ dependencies = [ "itc-rpc-client", "itp-node-api", "itp-test", + "itp-types", "its-primitives", "its-rpc-handler", "its-storage", @@ -5918,12 +5917,11 @@ name = "its-validateer-fetch" version = "0.9.0" dependencies = [ "derive_more", - "frame-support", "itc-parentchain-test", "itp-ocall-api", - "itp-teerex-storage", "itp-test", "itp-types", + "lc-teebag-storage", "parity-scale-codec", "sp-core", "sp-runtime", @@ -6336,6 +6334,15 @@ dependencies = [ "thiserror 1.0.9", ] +[[package]] +name = "lc-teebag-storage" +version = "0.1.0" +dependencies = [ + "itp-storage", + "itp-types", + "sp-std 5.0.0", +] + [[package]] name = "libc" version = "0.2.147" @@ -6970,7 +6977,6 @@ dependencies = [ "bitcoin", "core-primitives", "hex 0.4.3", - "itp-settings", "itp-sgx-crypto", "itp-sgx-io", "itp-utils", @@ -6978,6 +6984,7 @@ dependencies = [ "litentry-hex-utils", "log 0.4.20", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "pallet-teebag", "parity-scale-codec", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "rand 0.7.3 (git+https://github.com/mesalock-linux/rand-sgx?tag=sgx_1.1.3)", @@ -6992,7 +6999,6 @@ dependencies = [ "sp-std 5.0.0", "strum 0.26.1", "strum_macros 0.26.1", - "teerex-primitives", "thiserror 1.0.44", "thiserror 1.0.9", ] diff --git a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs index f2ae695c9e..289e016e02 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/litentry/scheduled_enclave.rs @@ -21,29 +21,35 @@ use itc_parentchain_indirect_calls_executor::{ IndirectDispatch, }; use itp_stf_primitives::traits::IndirectExecutor; -use itp_types::{MrEnclave, SidechainBlockNumber}; +use itp_types::{MrEnclave, SidechainBlockNumber, WorkerType}; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; -use log::debug; +use log::*; #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct UpdateScheduledEnclaveArgs { +pub struct SetScheduledEnclaveArgs { + worker_type: WorkerType, sbn: codec::Compact, mrenclave: MrEnclave, } impl> - IndirectDispatch for UpdateScheduledEnclaveArgs + IndirectDispatch for SetScheduledEnclaveArgs { type Args = (); fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { - debug!("execute indirect call: UpdateScheduledEnclave, sidechain_block_number: {:?}, mrenclave: {:?}", self.sbn, self.mrenclave); - GLOBAL_SCHEDULED_ENCLAVE.update(self.sbn.into(), self.mrenclave)?; + debug!("execute indirect call: SetScheduledEnclave, worker_type: {:?}, sidechain_block_number: {:?}, mrenclave: {:?}", self.worker_type, self.sbn, self.mrenclave); + if self.worker_type == WorkerType::BitAcross { + GLOBAL_SCHEDULED_ENCLAVE.update(self.sbn.into(), self.mrenclave)?; + } else { + warn!("Ignore SetScheduledEnclave due to wrong worker_type"); + } Ok(()) } } #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub struct RemoveScheduledEnclaveArgs { + worker_type: WorkerType, sbn: codec::Compact, } @@ -53,10 +59,15 @@ impl> type Args = (); fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { debug!( - "execute indirect call: RemoveScheduledEnclave, sidechain_block_number: {:?}", + "execute indirect call: RemoveScheduledEnclave, worker_type: {:?}, sidechain_block_number: {:?}", + self.worker_type, self.sbn ); - GLOBAL_SCHEDULED_ENCLAVE.remove(self.sbn.into())?; + if self.worker_type == WorkerType::BitAcross { + GLOBAL_SCHEDULED_ENCLAVE.remove(self.sbn.into())?; + } else { + warn!("Ignore RemoveScheduledEnclave due to wrong worker_type"); + } Ok(()) } } diff --git a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs index 88fe8aab4d..826c551410 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/indirect_calls/mod.rs @@ -21,6 +21,6 @@ pub mod shield_funds; pub mod transfer_to_alice_shields_funds; pub use invoke::InvokeArgs; -pub use litentry::scheduled_enclave::{RemoveScheduledEnclaveArgs, UpdateScheduledEnclaveArgs}; +pub use litentry::scheduled_enclave::{RemoveScheduledEnclaveArgs, SetScheduledEnclaveArgs}; pub use shield_funds::ShieldFundsArgs; pub use transfer_to_alice_shields_funds::{TransferToAliceShieldsFundsArgs, ALICE_ACCOUNT_ID}; diff --git a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs index 3e79c8ac9e..fd3614c14b 100644 --- a/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs +++ b/bitacross-worker/app-libs/parentchain-interface/src/integritee/mod.rs @@ -21,7 +21,7 @@ mod extrinsic_parser; use crate::{ decode_and_log_error, - indirect_calls::{RemoveScheduledEnclaveArgs, UpdateScheduledEnclaveArgs}, + indirect_calls::{RemoveScheduledEnclaveArgs, SetScheduledEnclaveArgs}, integritee::extrinsic_parser::ParseExtrinsic, }; use bc_relayer_registry::{RelayerRegistryUpdater, GLOBAL_RELAYER_REGISTRY}; @@ -40,20 +40,17 @@ use itp_node_api::metadata::NodeMetadataTrait; use itp_stf_primitives::traits::IndirectExecutor; use litentry_primitives::Identity; use log::trace; -use sp_core::crypto::AccountId32; /// The default indirect call (extrinsic-triggered) of the Integritee-Parachain. #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub enum IndirectCall { #[codec(index = 0)] - BitAcross(BitAcrossArgs), + SetScheduledEnclave(SetScheduledEnclaveArgs), #[codec(index = 1)] - UpdateScheduledEnclave(UpdateScheduledEnclaveArgs), - #[codec(index = 2)] RemoveScheduledEnclave(RemoveScheduledEnclaveArgs), - #[codec(index = 3)] + #[codec(index = 2)] AddRelayer(AddRelayerArgs), - #[codec(index = 4)] + #[codec(index = 3)] RemoveRelayer(RemoveRelayerArgs), } @@ -64,9 +61,8 @@ impl> fn dispatch(&self, executor: &Executor, _args: Self::Args) -> Result<()> { trace!("dispatching indirect call {:?}", self); match self { - IndirectCall::BitAcross(bitacross_args) => bitacross_args.dispatch(executor, ()), - IndirectCall::UpdateScheduledEnclave(update_scheduled_enclave_args) => - update_scheduled_enclave_args.dispatch(executor, ()), + IndirectCall::SetScheduledEnclave(set_scheduled_enclave_args) => + set_scheduled_enclave_args.dispatch(executor, ()), IndirectCall::RemoveScheduledEnclave(remove_scheduled_enclave_args) => remove_scheduled_enclave_args.dispatch(executor, ()), IndirectCall::AddRelayer(add_relayer_args) => add_relayer_args.dispatch(executor, ()), @@ -76,21 +72,6 @@ impl> } } -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct BitAcrossArgs { - account_id: AccountId32, -} - -impl> - IndirectDispatch for BitAcrossArgs -{ - type Args = (); - fn dispatch(&self, _executor: &Executor, _args: Self::Args) -> Result<()> { - log::error!("Not yet implemented"); - Ok(()) - } -} - #[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub struct AddRelayerArgs { account_id: Identity, @@ -157,10 +138,10 @@ where let index = xt.call_index; let call_args = &mut &xt.call_args[..]; - if index == metadata.update_scheduled_enclave().ok()? { - let args = decode_and_log_error::(call_args)?; - Some(IndirectCall::UpdateScheduledEnclave(args)) - } else if index == metadata.remove_scheduled_enclave().ok()? { + if index == metadata.set_scheduled_enclave_call_indexes().ok()? { + let args = decode_and_log_error::(call_args)?; + Some(IndirectCall::SetScheduledEnclave(args)) + } else if index == metadata.remove_scheduled_enclave_call_indexes().ok()? { let args = decode_and_log_error::(call_args)?; Some(IndirectCall::RemoveScheduledEnclave(args)) } else if index == metadata.add_relayer_call_indexes().ok()? { diff --git a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs index 0a653c3ab1..89da384c87 100644 --- a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs +++ b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs @@ -53,8 +53,7 @@ use sp_version::RuntimeVersion; pub use itp_sgx_runtime_primitives::{ constants::SLOT_DURATION, types::{ - AccountData, AccountId, Address, Balance, BlockNumber, ConvertAccountId, Hash, Header, - Index, SgxParentchainTypeConverter, Signature, + AccountData, AccountId, Address, Balance, BlockNumber, Hash, Header, Index, Signature, }, }; diff --git a/bitacross-worker/app-libs/stf/src/trusted_call.rs b/bitacross-worker/app-libs/stf/src/trusted_call.rs index ace42c1f56..c920aad039 100644 --- a/bitacross-worker/app-libs/stf/src/trusted_call.rs +++ b/bitacross-worker/app-libs/stf/src/trusted_call.rs @@ -34,10 +34,7 @@ use frame_support::{ensure, traits::UnfilteredDispatchable}; use ita_sgx_runtime::{AddressMapping, HashedAddressMapping}; pub use ita_sgx_runtime::{Balance, Index, Runtime, System}; use itp_node_api::metadata::{provider::AccessNodeMetadata, NodeMetadataTrait}; -use itp_node_api_metadata::{ - pallet_balances::BalancesCallIndexes, pallet_proxy::ProxyCallIndexes, - pallet_teerex::TeerexCallIndexes, -}; +use itp_node_api_metadata::{pallet_balances::BalancesCallIndexes, pallet_proxy::ProxyCallIndexes}; use itp_stf_interface::{ExecuteCall, SHARD_VAULT_KEY}; pub use itp_stf_primitives::{ error::{StfError, StfResult}, @@ -59,7 +56,6 @@ use sp_core::{ crypto::{AccountId32, UncheckedFrom}, ed25519, }; -use sp_io::hashing::blake2_256; use sp_runtime::MultiAddress; use std::{format, prelude::v1::*, sync::Arc}; @@ -265,7 +261,6 @@ where node_metadata_repo: Arc, ) -> Result { let sender = self.call.sender_identity().clone(); - let call_hash = blake2_256(&self.call.encode()); let account_id: AccountId = sender.to_account_id().ok_or(Self::Error::InvalidAccount)?; let system_nonce = System::account_nonce(&account_id); ensure!(self.nonce == system_nonce, Self::Error::InvalidNonce(self.nonce, system_nonce)); @@ -412,16 +407,7 @@ where debug!("balance_shield({}, {})", account_id_to_string(&who), value); shield_funds(who, value)?; - // Send proof of execution on chain. - calls.push(ParentchainCall::Litentry(OpaqueCall::from_tuple(&( - node_metadata_repo - .get_from_metadata(|m| m.publish_hash_call_indexes()) - .map_err(|_| StfError::InvalidMetadata)? - .map_err(|_| StfError::InvalidMetadata)?, - call_hash, - Vec::::new(), - b"shielded some funds!".to_vec(), - )))); + // Litentry: we don't have publish_hash call in teebag Ok(TrustedCallResult::Empty) }, #[cfg(feature = "evm")] diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml index 6750739b39..dc34df7d55 100644 --- a/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml @@ -40,7 +40,6 @@ itp-sgx-io = { path = "../../../core-primitives/sgx/io", default-features = fals itp-utils = { path = "../../../core-primitives/utils", default-features = false } litentry-hex-utils = { path = "../../../../primitives/hex", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } -teerex-primitives = { path = "../../../../primitives/teerex", default-features = false } # litentry primities litentry-primitives = { path = "../../../litentry/primitives", default-features = false } @@ -72,7 +71,6 @@ std = [ "sp-runtime/std", "ring/std", "parentchain-primitives/std", - "teerex-primitives/std", "rand", "log/std", "bitcoin/std", diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs index a7d5eefff5..eff6895977 100644 --- a/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs @@ -13,7 +13,6 @@ use lazy_static::lazy_static; use litentry_primitives::Identity; use log::error; use std::{collections::BTreeMap, path::PathBuf, sync::Arc}; -pub use teerex_primitives::{decl_rsa_request, ShardIdentifier, SidechainBlockNumber}; #[cfg(feature = "std")] use std::sync::RwLock; diff --git a/bitacross-worker/cli/lit_parentchain_nonce.sh b/bitacross-worker/cli/lit_parentchain_nonce.sh index 505469f11b..a8b0b0d446 100755 --- a/bitacross-worker/cli/lit_parentchain_nonce.sh +++ b/bitacross-worker/cli/lit_parentchain_nonce.sh @@ -62,7 +62,7 @@ ${CLIENT} trusted --mrenclave $MRENCLAVE --direct send-erroneous-parentchain-cal echo "" sleep 20 -# wait for 10 `ProcessedParentchainBlock` events, which should take around 2 min (1 worker) +# wait for 10 `ParentchainBlockProcessed` events, which should take around 2 min (1 worker) # if the incoming parentchain extrinsic is blocked (due to the wrong nonce), there won't be # such many events. set -e diff --git a/bitacross-worker/cli/src/base_cli/commands/listen.rs b/bitacross-worker/cli/src/base_cli/commands/listen.rs index 27a9b15811..734ffb9af8 100644 --- a/bitacross-worker/cli/src/base_cli/commands/listen.rs +++ b/bitacross-worker/cli/src/base_cli/commands/listen.rs @@ -73,70 +73,55 @@ impl ListenCommand { }, } }, - RuntimeEvent::Teerex(ee) => { - println!(">>>>>>>>>> integritee teerex event: {:?}", ee); + RuntimeEvent::Teebag(ee) => { + println!(">>>>>>>>>> litentry teebag event: {:?}", ee); count += 1; match &ee { - my_node_runtime::pallet_teerex::Event::AddedEnclave( - accountid, + my_node_runtime::pallet_teebag::Event::EnclaveAdded { + who, + worker_type, url, - ) => { + } => { println!( - "AddedEnclave: {:?} at url {}", - accountid, + "EnclaveAdded: {:?} [{:?}] at url {}", + who, + worker_type, String::from_utf8(url.to_vec()) .unwrap_or_else(|_| "error".to_string()) ); }, - my_node_runtime::pallet_teerex::Event::RemovedEnclave( - accountid, - ) => { - println!("RemovedEnclave: {:?}", accountid); + my_node_runtime::pallet_teebag::Event::EnclaveRemoved { + who, + } => { + println!("EnclaveRemoved: {:?}", who); }, - my_node_runtime::pallet_teerex::Event::Forwarded(shard) => { + my_node_runtime::pallet_teebag::Event::OpaqueTaskPosted { shard } => { println!( - "Forwarded request for shard {}", + "OpaqueTaskPosted for shard {}", shard.encode().to_base58() ); }, - my_node_runtime::pallet_teerex::Event::ProcessedParentchainBlock( - accountid, - block_hash, - merkle_root, + my_node_runtime::pallet_teebag::Event::ParentchainBlockProcessed { + who, block_number, - ) => { + block_hash, + task_merkle_root, + } => { println!( - "ProcessedParentchainBlock from {} with hash {:?}, number {} and merkle root {:?}", - accountid, block_hash, merkle_root, block_number + "ParentchainBlockProcessed from {} with hash {:?}, number {} and merkle root {:?}", + who, block_hash, block_number, task_merkle_root ); }, - my_node_runtime::pallet_teerex::Event::ShieldFunds( - incognito_account, - ) => { - println!("ShieldFunds for {:?}", incognito_account); - }, - my_node_runtime::pallet_teerex::Event::UnshieldedFunds( - public_account, - ) => { - println!("UnshieldFunds for {:?}", public_account); - }, - _ => debug!("ignoring unsupported teerex event: {:?}", ee), - } - }, - RuntimeEvent::Sidechain(ee) => { - println!(">>>>>>>>>> integritee sidechain event: {:?}", ee); - count += 1; - match &ee { - my_node_runtime::pallet_sidechain::Event::ProposedSidechainBlock( - accountid, - block_hash, - ) => { + my_node_runtime::pallet_teebag::Event::SidechainBlockFinalized { + who, + sidechain_block_number, + } => { println!( - "ProposedSidechainBlock from {} with hash {:?}", - accountid, block_hash + "SidechainBlockFinalized from {} with number {:?}", + who, sidechain_block_number ); }, - _ => debug!("ignoring unsupported sidechain event: {:?}", ee), + _ => debug!("ignoring unsupported teebag event: {:?}", ee), } }, _ => debug!("ignoring unsupported module event: {:?}", evr.event), diff --git a/bitacross-worker/cli/src/base_cli/mod.rs b/bitacross-worker/cli/src/base_cli/mod.rs index 9ba67f94f7..a2ec00e64c 100644 --- a/bitacross-worker/cli/src/base_cli/mod.rs +++ b/bitacross-worker/cli/src/base_cli/mod.rs @@ -29,7 +29,8 @@ use base58::ToBase58; use chrono::{DateTime, Utc}; use clap::Subcommand; use itc_rpc_client::direct_client::DirectApi; -use itp_node_api::api_client::PalletTeerexApi; +use itp_node_api::api_client::PalletTeebagApi; +use itp_types::WorkerType; use sp_core::crypto::Ss58Codec; use sp_keystore::Keystore; use std::{ @@ -164,29 +165,22 @@ fn print_sgx_metadata_raw(cli: &Cli) -> CliResult { fn list_workers(cli: &Cli) -> CliResult { let api = get_chain_api(cli); - let wcount = api.enclave_count(None).unwrap(); - println!("number of workers registered: {}", wcount); - - let mut mr_enclaves = Vec::with_capacity(wcount as usize); - - for w in 1..=wcount { - let enclave = api.enclave(w, None).unwrap(); - if enclave.is_none() { - println!("error reading enclave data"); - continue - }; - let enclave = enclave.unwrap(); - let timestamp = - DateTime::::from(UNIX_EPOCH + Duration::from_millis(enclave.timestamp)); - let mr_enclave = enclave.mr_enclave.to_base58(); - println!("Enclave {}", w); - println!(" AccountId: {}", enclave.pubkey.to_ss58check()); - println!(" MRENCLAVE: {}", mr_enclave); - println!(" RA timestamp: {}", timestamp); - println!(" URL: {}", enclave.url); - - mr_enclaves.push(mr_enclave); - } + let enclaves = api.all_enclaves(WorkerType::BitAcross, None).unwrap(); + println!("number of enclaves registered: {}", enclaves.len()); + + let mr_enclaves = enclaves + .iter() + .map(|enclave| { + println!("Enclave"); + println!(" MRENCLAVE: {}", enclave.mrenclave.to_base58()); + let timestamp = DateTime::::from( + UNIX_EPOCH + Duration::from_millis(enclave.last_seen_timestamp), + ); + println!(" Last seen: {}", timestamp); + println!(" URL: {}", String::from_utf8_lossy(enclave.url.as_slice())); + enclave.mrenclave.to_base58() + }) + .collect(); Ok(CliResultOk::MrEnclaveBase58 { mr_enclaves }) } diff --git a/bitacross-worker/core-primitives/enclave-api/Cargo.toml b/bitacross-worker/core-primitives/enclave-api/Cargo.toml index c9dfaa9dff..b0541b1136 100644 --- a/bitacross-worker/core-primitives/enclave-api/Cargo.toml +++ b/bitacross-worker/core-primitives/enclave-api/Cargo.toml @@ -26,7 +26,7 @@ itp-storage = { path = "../storage" } itp-types = { path = "../types" } # litentry -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } +pallet-teebag = { path = "../../../pallets/teebag", default-features = false } [features] default = [] diff --git a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs index 4e79a6f902..b0f9dcb047 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs @@ -21,9 +21,9 @@ use codec::Decode; use core::fmt::Debug; use itc_parentchain::primitives::{ParentchainId, ParentchainInitParams}; use itp_types::ShardIdentifier; +use pallet_teebag::EnclaveFingerprint; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sp_core::ed25519; -use teerex_primitives::EnclaveFingerprint; /// Trait for base/common Enclave API functions pub trait EnclaveBase: Send + Sync + 'static { @@ -97,10 +97,10 @@ mod impl_ffi { }; use itp_types::ShardIdentifier; use log::*; + use pallet_teebag::EnclaveFingerprint; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sgx_types::*; use sp_core::ed25519; - use teerex_primitives::EnclaveFingerprint; impl EnclaveBase for Enclave { fn init( diff --git a/bitacross-worker/core-primitives/enclave-api/src/remote_attestation.rs b/bitacross-worker/core-primitives/enclave-api/src/remote_attestation.rs index 9aa32cb631..30a765da32 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/remote_attestation.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/remote_attestation.rs @@ -18,8 +18,8 @@ use crate::EnclaveResult; use itp_types::ShardIdentifier; +use pallet_teebag::Fmspc; use sgx_types::*; -use teerex_primitives::Fmspc; /// Struct that unites all relevant data reported by the QVE pub struct QveReport { @@ -128,8 +128,8 @@ mod impl_ffi { use itp_settings::worker::EXTRINSIC_MAX_SIZE; use itp_types::ShardIdentifier; use log::*; + use pallet_teebag::Fmspc; use sgx_types::*; - use teerex_primitives::Fmspc; const OS_SYSTEM_PATH: &str = "/usr/lib/x86_64-linux-gnu/"; const C_STRING_ENDING: &str = "\0"; diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index f56b639e19..878d384af8 100644 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -21,10 +21,12 @@ pub use substrate_api_client::{api::Error as ApiClientError, rpc::TungsteniteRpc pub mod account; pub mod chain; +pub mod pallet_teebag; pub mod pallet_teerex; pub use account::*; pub use chain::*; +pub use pallet_teebag::*; pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs new file mode 100644 index 0000000000..e243091ca5 --- /dev/null +++ b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teebag.rs @@ -0,0 +1,134 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::ApiResult; +use itp_api_client_types::{traits::GetStorage, Api, Config, Request}; +use itp_types::{AccountId, Enclave, ShardIdentifier, WorkerType}; + +pub const TEEBAG: &str = "Teebag"; + +/// ApiClient extension that enables communication with the `teebag` pallet. +pub trait PalletTeebagApi { + type Hash; + + fn enclave( + &self, + account: &AccountId, + at_block: Option, + ) -> ApiResult>; + fn enclave_count( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult; + fn primary_enclave_identifier_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult>; + fn primary_enclave_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult>; + fn all_enclaves( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult>; +} + +impl PalletTeebagApi for Api +where + RuntimeConfig: Config, + Client: Request, +{ + type Hash = RuntimeConfig::Hash; + + fn enclave( + &self, + account: &AccountId, + at_block: Option, + ) -> ApiResult> { + self.get_storage_map(TEEBAG, "EnclaveRegistry", account, at_block) + } + + fn enclave_count( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult { + // Vec<> and BoundedVec<> have the same encoding, thus they are used interchangeably + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + Ok(identifiers.len() as u64) + } + + // please note we don't use dedicated on-chain storage for this (like the upstream `WorkerForShard`) + // so this API will always return the "first" registered and qualified enclave. + // Wheter it meets our needs needs to be further evaluated + fn primary_enclave_identifier_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + let mut maybe_account: Option = None; + for account in identifiers { + match self.enclave(&account, at_block)? { + Some(e) => + if e.mrenclave == shard.as_ref() { + maybe_account = Some(account.clone()); + break + }, + None => continue, + } + } + Ok(maybe_account) + } + + fn primary_enclave_for_shard( + &self, + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { + self.primary_enclave_identifier_for_shard(worker_type, shard, at_block)? + .map_or_else(|| Ok(None), |account| self.enclave(&account, at_block)) + } + + fn all_enclaves( + &self, + worker_type: WorkerType, + at_block: Option, + ) -> ApiResult> { + let identifiers: Vec = self + .get_storage_map(TEEBAG, "EnclaveIdentifier", worker_type, at_block)? + .unwrap_or_default(); + + let enclaves = identifiers + .into_iter() + .filter_map(|account| self.enclave(&account, at_block).ok()?) + .collect(); + Ok(enclaves) + } +} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs index a1bc01d865..3f8073c569 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/lib.rs @@ -21,9 +21,8 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, + pallet_proxy::ProxyCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teebag::TeebagCallIndexes, pallet_utility::UtilityCallIndexes, }; use codec::{Decode, Encode}; use sp_core::storage::StorageKey; @@ -35,9 +34,8 @@ pub mod error; pub mod pallet_balances; pub mod pallet_bitacross; pub mod pallet_proxy; -pub mod pallet_sidechain; pub mod pallet_system; -pub mod pallet_teerex; +pub mod pallet_teebag; pub mod pallet_utility; pub mod runtime_call; @@ -45,8 +43,7 @@ pub mod runtime_call; pub mod metadata_mocks; pub trait NodeMetadataTrait: - TeerexCallIndexes - + SidechainCallIndexes + TeebagCallIndexes + SystemSs58Prefix + UtilityCallIndexes + ProxyCallIndexes @@ -55,8 +52,7 @@ pub trait NodeMetadataTrait: { } impl< - T: TeerexCallIndexes - + SidechainCallIndexes + T: TeebagCallIndexes + SystemSs58Prefix + UtilityCallIndexes + ProxyCallIndexes diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index 2ddc45ad6a..3235c73ca5 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -17,9 +17,9 @@ use crate::{ error::Result, pallet_balances::BalancesCallIndexes, pallet_bitacross::BitAcrossCallIndexes, - pallet_proxy::ProxyCallIndexes, pallet_sidechain::SidechainCallIndexes, - pallet_system::SystemSs58Prefix, pallet_teerex::TeerexCallIndexes, - pallet_utility::UtilityCallIndexes, runtime_call::RuntimeCall, + pallet_proxy::ProxyCallIndexes, pallet_system::SystemSs58Prefix, + pallet_teebag::TeebagCallIndexes, pallet_utility::UtilityCallIndexes, + runtime_call::RuntimeCall, }; use codec::{Decode, Encode}; @@ -35,22 +35,17 @@ impl TryFrom for Metadata { #[derive(Default, Encode, Decode, Debug, Clone)] pub struct NodeMetadataMock { - teerex_module: u8, + // teebag + teebag_module: u8, + set_scheduled_enclave: u8, + remove_scheduled_enclave: u8, register_enclave: u8, - unregister_sovereign_enclave: u8, - unregister_proxied_enclave: u8, + unregister_enclave: u8, register_quoting_enclave: u8, register_tcb_info: u8, - update_scheduled_enclave: u8, - remove_scheduled_enclave: u8, - enclave_bridge_module: u8, - invoke: u8, - confirm_processed_parentchain_block: u8, - shield_funds: u8, - unshield_funds: u8, - publish_hash: u8, - update_shard_config: u8, - sidechain_module: u8, + post_opaque_task: u8, + parentchain_block_processed: u8, + sidechain_block_imported: u8, utility_module: u8, utility_batch: u8, @@ -78,22 +73,16 @@ pub struct NodeMetadataMock { impl NodeMetadataMock { pub fn new() -> Self { NodeMetadataMock { - teerex_module: 50u8, - register_enclave: 0u8, - unregister_sovereign_enclave: 1u8, - unregister_proxied_enclave: 2u8, - register_quoting_enclave: 3, - register_tcb_info: 4, - update_scheduled_enclave: 5, - remove_scheduled_enclave: 6, - enclave_bridge_module: 54u8, - invoke: 0u8, - confirm_processed_parentchain_block: 1u8, - shield_funds: 2u8, - unshield_funds: 3u8, - publish_hash: 4u8, - update_shard_config: 5u8, - sidechain_module: 53u8, + teebag_module: 50u8, + set_scheduled_enclave: 0u8, + remove_scheduled_enclave: 1u8, + register_enclave: 2u8, + unregister_enclave: 3u8, + register_quoting_enclave: 4u8, + register_tcb_info: 5u8, + post_opaque_task: 6u8, + parentchain_block_processed: 7u8, + sidechain_block_imported: 8u8, utility_module: 80u8, utility_batch: 0u8, @@ -114,69 +103,39 @@ impl NodeMetadataMock { runtime_transaction_version: 4, bitacross_module: 69u8, - bitacross_add_relayer: 71u8, - bitacross_remove_relayer: 72u8, + bitacross_add_relayer: 0u8, + bitacross_remove_relayer: 1u8, } } } -impl TeerexCallIndexes for NodeMetadataMock { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_enclave]) +impl TeebagCallIndexes for NodeMetadataMock { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.set_scheduled_enclave]) } - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unregister_sovereign_enclave]) + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.remove_scheduled_enclave]) } - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unregister_proxied_enclave]) + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.register_enclave]) + } + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.unregister_enclave]) } - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_quoting_enclave]) + Ok([self.teebag_module, self.register_quoting_enclave]) } - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.register_tcb_info]) - } - - fn invoke_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.invoke]) - } - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.confirm_processed_parentchain_block]) - } - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.shield_funds]) - } - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.unshield_funds]) - } - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.publish_hash]) + Ok([self.teebag_module, self.register_tcb_info]) } - - // fn update_shard_config_call_indexes(&self) -> Result<[u8; 2]> { - // Ok([self.teerex_module, self.update_shard_config]) - // } - - fn update_scheduled_enclave(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.update_scheduled_enclave]) + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.post_opaque_task]) } - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]> { - Ok([self.teerex_module, self.remove_scheduled_enclave]) + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.parentchain_block_processed]) } -} - -impl SidechainCallIndexes for NodeMetadataMock { - fn confirm_imported_sidechain_block_indexes(&self) -> Result<[u8; 2]> { - Ok([self.sidechain_module, self.imported_sidechain_block]) + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]> { + Ok([self.teebag_module, self.sidechain_block_imported]) } } diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs new file mode 100644 index 0000000000..b748dd632f --- /dev/null +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teebag.rs @@ -0,0 +1,71 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use crate::{error::Result, NodeMetadata}; + +/// Pallet' name: +pub const TEEBAG: &str = "Teebag"; + +// we only list the extrinsics that we care +pub trait TeebagCallIndexes { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]>; + + fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]>; + + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]>; + + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]>; + + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]>; +} + +impl TeebagCallIndexes for NodeMetadata { + fn set_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "set_scheduled_enclave") + } + fn remove_scheduled_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "remove_scheduled_enclave") + } + fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_enclave") + } + fn unregister_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "unregister_enclave") + } + fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_quoting_enclave") + } + fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "register_tcb_info") + } + fn post_opaque_task_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "post_opaque_task") + } + fn parentchain_block_processed_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "parentchain_block_processed") + } + fn sidechain_block_imported_call_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(TEEBAG, "sidechain_block_imported") + } +} diff --git a/bitacross-worker/core-primitives/settings/Cargo.toml b/bitacross-worker/core-primitives/settings/Cargo.toml index f430a521be..3aadf1e5e5 100644 --- a/bitacross-worker/core-primitives/settings/Cargo.toml +++ b/bitacross-worker/core-primitives/settings/Cargo.toml @@ -5,9 +5,11 @@ authors = ['Trust Computing GmbH ', 'Integritee AG WorkerMode; diff --git a/bitacross-worker/core-primitives/sgx-runtime-primitives/Cargo.toml b/bitacross-worker/core-primitives/sgx-runtime-primitives/Cargo.toml index 8ec87045fa..5290e8da32 100644 --- a/bitacross-worker/core-primitives/sgx-runtime-primitives/Cargo.toml +++ b/bitacross-worker/core-primitives/sgx-runtime-primitives/Cargo.toml @@ -12,9 +12,6 @@ pallet-balances = { default-features = false, git = "https://github.com/parityte sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -# litentry -litentry-primitives = { path = "../../litentry/primitives", default-features = false } - [features] default = ["std"] std = [ @@ -22,6 +19,4 @@ std = [ "pallet-balances/std", "sp-core/std", "sp-runtime/std", - # litentry - "litentry-primitives/std", ] diff --git a/bitacross-worker/core-primitives/sgx-runtime-primitives/src/types.rs b/bitacross-worker/core-primitives/sgx-runtime-primitives/src/types.rs index bad667791e..035ae982b8 100644 --- a/bitacross-worker/core-primitives/sgx-runtime-primitives/src/types.rs +++ b/bitacross-worker/core-primitives/sgx-runtime-primitives/src/types.rs @@ -21,8 +21,6 @@ use sp_runtime::{ MultiSignature, OpaqueExtrinsic, }; -use litentry_primitives::ParentchainAccountId; - /// The address format for describing accounts. pub type Address = sp_runtime::MultiAddress; /// Block header type as expected by this sgx-runtime. @@ -66,21 +64,3 @@ pub type Block = BlockG; pub type SignedBlock = SignedBlockG; pub type BlockHash = sp_core::H256; pub type ShardIdentifier = sp_core::H256; - -// litentry -pub trait ConvertAccountId { - type Input; - type Output; - fn convert(input: Self::Input) -> Self::Output; -} - -pub struct SgxParentchainTypeConverter; - -impl ConvertAccountId for SgxParentchainTypeConverter { - type Input = AccountId; - type Output = ParentchainAccountId; - fn convert(a: AccountId) -> ParentchainAccountId { - // it's an identity converter - a as ParentchainAccountId - } -} diff --git a/bitacross-worker/core-primitives/test/Cargo.toml b/bitacross-worker/core-primitives/test/Cargo.toml index ff82183e77..92466e8615 100644 --- a/bitacross-worker/core-primitives/test/Cargo.toml +++ b/bitacross-worker/core-primitives/test/Cargo.toml @@ -29,12 +29,12 @@ itp-stf-interface = { path = "../stf-interface", default-features = false } itp-stf-primitives = { path = "../stf-primitives", default-features = false } itp-stf-state-handler = { path = "../stf-state-handler", default-features = false } itp-storage = { path = "../storage", default-features = false } -itp-teerex-storage = { path = "../teerex-storage", default-features = false } itp-time-utils = { path = "../time-utils", default-features = false } itp-types = { path = "../types", default-features = false, features = ["test"] } # litentry hex = { version = "0.4.3", default-features = false } +lc-teebag-storage = { path = "../../litentry/core/teebag-storage", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } [features] @@ -50,7 +50,6 @@ std = [ "itp-stf-primitives/std", "itp-stf-state-handler/std", "itp-storage/std", - "itp-teerex-storage/std", "itp-time-utils/std", "itp-types/std", "log/std", @@ -59,6 +58,7 @@ std = [ "sp-runtime/std", "sp-std/std", "litentry-primitives/std", + "lc-teebag-storage/std", ] sgx = [ "itp-node-api/sgx", diff --git a/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs b/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs index 22744289b5..86c0d12a35 100644 --- a/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs +++ b/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs @@ -23,14 +23,14 @@ use itp_ocall_api::{ EnclaveSidechainOCallApi, }; use itp_storage::Error::StorageValueUnavailable; -use itp_teerex_storage::{TeeRexStorage, TeerexStorageKeys}; use itp_types::{ - parentchain::ParentchainId, storage::StorageEntryVerified, BlockHash, Enclave, ShardIdentifier, - WorkerRequest, WorkerResponse, + parentchain::ParentchainId, storage::StorageEntryVerified, AccountId, BlockHash, + ShardIdentifier, WorkerRequest, WorkerResponse, WorkerType, }; +use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; use sgx_types::*; use sp_core::H256; -use sp_runtime::{traits::Header as HeaderTrait, AccountId32, OpaqueExtrinsic}; +use sp_runtime::{traits::Header as HeaderTrait, OpaqueExtrinsic}; use sp_std::prelude::*; use std::{collections::HashMap, string::String}; @@ -55,11 +55,15 @@ impl OnchainMock { pub fn add_validateer_set>( mut self, header: &Header, - set: Option>, + set: Option>, ) -> Self { - let set = set.unwrap_or_else(validateer_set); - self.insert_at_header(header, TeeRexStorage::enclave_count(), (set.len() as u64).encode()); - self.with_storage_entries_at_header(header, into_key_value_storage(set)) + let set: Vec = set.unwrap_or_else(validateer_set); + self.insert_at_header( + header, + TeebagStorage::enclave_identifier(WorkerType::BitAcross), + set.encode(), + ); + self } pub fn with_mr_enclave(mut self, mr_enclave: [u8; SGX_HASH_SIZE]) -> Self { @@ -224,20 +228,11 @@ impl EnclaveOnChainOCallApi for OnchainMock { } } -pub fn validateer_set() -> Vec { - let default_enclave = Enclave::new( - AccountId32::from([0; 32]), - Default::default(), - Default::default(), - Default::default(), - ); - vec![default_enclave.clone(), default_enclave.clone(), default_enclave.clone(), default_enclave] -} - -fn into_key_value_storage(validateers: Vec) -> Vec<(Vec, Enclave)> { - validateers - .into_iter() - .enumerate() - .map(|(i, e)| (TeeRexStorage::enclave(i as u64 + 1), e)) - .collect() +pub fn validateer_set() -> Vec { + vec![ + AccountId::from([0; 32]), + AccountId::from([1; 32]), + AccountId::from([2; 32]), + AccountId::from([3; 32]), + ] } diff --git a/bitacross-worker/core-primitives/types/src/lib.rs b/bitacross-worker/core-primitives/types/src/lib.rs index cdaeaa5685..cc5d26ea9c 100644 --- a/bitacross-worker/core-primitives/types/src/lib.rs +++ b/bitacross-worker/core-primitives/types/src/lib.rs @@ -37,12 +37,12 @@ pub type PalletString = Vec; pub type PalletString = String; pub use itp_sgx_runtime_primitives::types::*; -pub use litentry_primitives::DecryptableRequest; +pub use litentry_primitives::{ + AttestationType, DecryptableRequest, Enclave, EnclaveFingerprint, MrEnclave, WorkerType, +}; pub use sp_core::{crypto::AccountId32 as AccountId, H256}; pub type IpfsHash = [u8; 46]; -pub type MrEnclave = [u8; 32]; - pub type CallIndex = [u8; 2]; // pallet teerex @@ -50,11 +50,9 @@ pub type ConfirmCallFn = (CallIndex, ShardIdentifier, H256, Vec); pub type ShieldFundsFn = (CallIndex, Vec, Balance, ShardIdentifier); pub type CallWorkerFn = (CallIndex, RsaRequest); -pub type UpdateScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); +pub type SetScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); pub type RemoveScheduledEnclaveFn = (CallIndex, SidechainBlockNumber); -pub type Enclave = EnclaveGen; - /// Simple blob to hold an encoded call #[derive(Debug, PartialEq, Eq, Clone, Default)] pub struct OpaqueCall(pub Vec); @@ -94,23 +92,6 @@ impl DecryptableRequest for RsaRequest { } } -// Todo: move this improved enclave definition into a primitives crate in the pallet_teerex repo. -#[derive(Encode, Decode, Clone, PartialEq, sp_core::RuntimeDebug)] -pub struct EnclaveGen { - pub pubkey: AccountId, - // FIXME: this is redundant information - pub mr_enclave: [u8; 32], - pub timestamp: u64, - // unix epoch in milliseconds - pub url: PalletString, // utf8 encoded url -} - -impl EnclaveGen { - pub fn new(pubkey: AccountId, mr_enclave: [u8; 32], timestamp: u64, url: PalletString) -> Self { - Self { pubkey, mr_enclave, timestamp, url } - } -} - #[derive(Debug, Clone, PartialEq, Encode, Decode, Eq)] pub enum DirectRequestStatus { /// Direct request was successfully executed diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs index 0c2dbcf74c..7132b0bdeb 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs @@ -29,7 +29,7 @@ use binary_merkle_tree::merkle_root; use codec::{Decode, Encode}; use core::marker::PhantomData; use itp_node_api::metadata::{ - pallet_teerex::TeerexCallIndexes, provider::AccessNodeMetadata, NodeMetadataTrait, + pallet_teebag::TeebagCallIndexes, provider::AccessNodeMetadata, NodeMetadataTrait, }; use itp_sgx_crypto::{key_repository::AccessKey, ShieldingCryptoDecrypt, ShieldingCryptoEncrypt}; use itp_stf_executor::traits::{StfEnclaveSigning, StfShardVaultQuery}; @@ -217,10 +217,10 @@ impl< ParentchainBlock: ParentchainBlockTrait, { let call = self.node_meta_data_provider.get_from_metadata(|meta_data| { - meta_data.confirm_processed_parentchain_block_call_indexes() + meta_data.parentchain_block_processed_call_indexes() })??; let root: H256 = merkle_root::(extrinsics); - trace!("prepared confirm_processed_parentchain_block() call for block {:?} with index {:?} and merkle root {}", block_number, call, root); + trace!("prepared parentchain_block_processed() call for block {:?} with index {:?} and merkle root {}", block_number, call, root); // Litentry: we don't include `shard` in the extrinsic parameter to be backwards compatible, // however, we should not forget it in case we need it later Ok(OpaqueCall::from_tuple(&(call, block_hash, block_number, root))) @@ -366,41 +366,6 @@ mod test { assert_eq!(1, top_pool_author.pending_tops(shard_id()).unwrap().len()); } - #[test] - fn shielding_call_can_be_added_to_pool_successfully() { - let _ = env_logger::builder().is_test(true).try_init(); - - let mr_enclave = [33u8; 32]; - let (indirect_calls_executor, top_pool_author, shielding_key_repo) = - test_fixtures(mr_enclave.clone(), NodeMetadataMock::new()); - let shielding_key = shielding_key_repo.retrieve_key().unwrap(); - - let opaque_extrinsic = OpaqueExtrinsic::from_bytes( - shield_funds_unchecked_extrinsic(&shielding_key).encode().as_slice(), - ) - .unwrap(); - - let parentchain_block = ParentchainBlockBuilder::default() - .with_extrinsics(vec![opaque_extrinsic]) - .build(); - - indirect_calls_executor - .execute_indirect_calls_in_extrinsics(&parentchain_block, &Vec::new()) - .unwrap(); - - assert_eq!(1, top_pool_author.pending_tops(shard_id()).unwrap().len()); - let submitted_extrinsic = - top_pool_author.pending_tops(shard_id()).unwrap().first().cloned().unwrap(); - let decrypted_extrinsic = shielding_key.decrypt(&submitted_extrinsic).unwrap(); - let decoded_operation = TrustedOperation::::decode( - &mut decrypted_extrinsic.as_slice(), - ) - .unwrap(); - assert_matches!(decoded_operation, TrustedOperation::indirect_call(_)); - let trusted_call_signed = decoded_operation.to_call().unwrap(); - assert!(trusted_call_signed.verify_signature(&mr_enclave, &shard_id())); - } - #[test] fn ensure_empty_extrinsic_vec_triggers_zero_filled_merkle_root() { // given @@ -409,11 +374,10 @@ mod test { let block_hash = H256::from([1; 32]); let extrinsics = Vec::new(); - let confirm_processed_parentchain_block_indexes = - dummy_metadata.confirm_processed_parentchain_block_call_indexes().unwrap(); + let parentchain_block_processed_call_indexes = + dummy_metadata.parentchain_block_processed_call_indexes().unwrap(); let expected_call = - (confirm_processed_parentchain_block_indexes, block_hash, 1u32, H256::default()) - .encode(); + (parentchain_block_processed_call_indexes, block_hash, 1u32, H256::default()).encode(); // when let call = indirect_calls_executor @@ -432,12 +396,11 @@ mod test { let block_hash = H256::from([1; 32]); let extrinsics = vec![H256::from([4; 32]), H256::from([9; 32])]; - let confirm_processed_parentchain_block_indexes = - dummy_metadata.confirm_processed_parentchain_block_call_indexes().unwrap(); + let parentchain_block_processed_call_indexes = + dummy_metadata.parentchain_block_processed_call_indexes().unwrap(); let zero_root_call = - (confirm_processed_parentchain_block_indexes, block_hash, 1u32, H256::default()) - .encode(); + (parentchain_block_processed_call_indexes, block_hash, 1u32, H256::default()).encode(); // when let call = indirect_calls_executor @@ -448,25 +411,10 @@ mod test { assert_ne!(call.0, zero_root_call); } - fn shield_funds_unchecked_extrinsic( - shielding_key: &ShieldingCryptoMock, - ) -> ParentchainUncheckedExtrinsic { - let target_account = shielding_key.encrypt(&AccountId::new([2u8; 32]).encode()).unwrap(); - let dummy_metadata = NodeMetadataMock::new(); - - let shield_funds_indexes = dummy_metadata.shield_funds_call_indexes().unwrap(); - ParentchainUncheckedExtrinsic::::new_signed( - (shield_funds_indexes, target_account, 1000u128, shard_id()), - MultiAddress::Address32([1u8; 32]), - MultiSignature::Ed25519(default_signature()), - default_extrinsic_params().signed_extra(), - ) - } - fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { let request = RsaRequest::new(shard_id(), vec![1u8, 2u8]); let dummy_metadata = NodeMetadataMock::new(); - let call_worker_indexes = dummy_metadata.invoke_call_indexes().unwrap(); + let call_worker_indexes = dummy_metadata.post_opaque_task_call_indexes().unwrap(); ParentchainUncheckedExtrinsic::::new_signed( (call_worker_indexes, request), diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/src/mock.rs b/bitacross-worker/core/parentchain/indirect-calls-executor/src/mock.rs index 38189f44d8..57c1645cc2 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/src/mock.rs +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/src/mock.rs @@ -58,11 +58,7 @@ where "[ShieldFundsAndInvokeFilter] attempting to execute indirect call with index {:?}", index ); - if index == metadata.shield_funds_call_indexes().ok()? { - log::debug!("executing shield funds call"); - let args = ShieldFundsArgs::decode(call_args).unwrap(); - Some(IndirectCall::ShieldFunds(args)) - } else if index == metadata.invoke_call_indexes().ok()? { + if index == metadata.post_opaque_task_call_indexes().ok()? { log::debug!("executing invoke call"); let args = InvokeArgs::decode(call_args).unwrap(); Some(IndirectCall::Invoke(args)) diff --git a/bitacross-worker/core/rpc-client/Cargo.toml b/bitacross-worker/core/rpc-client/Cargo.toml index fc06593ed3..1f2b3ff104 100644 --- a/bitacross-worker/core/rpc-client/Cargo.toml +++ b/bitacross-worker/core/rpc-client/Cargo.toml @@ -31,7 +31,6 @@ itp-utils = { path = "../../core-primitives/utils" } ita-stf = { path = "../../app-libs/stf" } itp-stf-primitives = { path = "../../core-primitives/stf-primitives" } litentry-primitives = { path = "../../litentry/primitives", default-features = false } -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } [dev-dependencies] env_logger = "0.9.0" diff --git a/bitacross-worker/core/rpc-client/src/direct_client.rs b/bitacross-worker/core/rpc-client/src/direct_client.rs index 5f7acab959..142ecdb897 100644 --- a/bitacross-worker/core/rpc-client/src/direct_client.rs +++ b/bitacross-worker/core/rpc-client/src/direct_client.rs @@ -17,6 +17,7 @@ //! Interface for direct access to a workers rpc. +pub use crate::error::{Error, Result}; use crate::ws_client::{WsClient, WsClientControl}; use base58::ToBase58; use codec::{Decode, Encode}; @@ -25,7 +26,7 @@ use ita_stf::Getter; use itp_api_client_types::Metadata; use itp_rpc::{Id, RpcRequest, RpcResponse, RpcReturnValue}; use itp_stf_primitives::types::{AccountId, ShardIdentifier}; -use itp_types::{DirectRequestStatus, RsaRequest}; +use itp_types::{DirectRequestStatus, MrEnclave, RsaRequest}; use itp_utils::{FromHexPrefixed, ToHexPrefixed}; use log::*; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; @@ -37,9 +38,6 @@ use std::{ thread, thread::JoinHandle, }; -use teerex_primitives::MrEnclave; - -pub use crate::error::{Error, Result}; #[derive(Clone)] pub struct DirectClient { diff --git a/bitacross-worker/core/rpc-client/src/mock.rs b/bitacross-worker/core/rpc-client/src/mock.rs index d61290c035..cbbda5a7a4 100644 --- a/bitacross-worker/core/rpc-client/src/mock.rs +++ b/bitacross-worker/core/rpc-client/src/mock.rs @@ -23,10 +23,10 @@ use frame_metadata::RuntimeMetadataPrefixed; use ita_stf::H256; use itp_api_client_types::Metadata; use itp_stf_primitives::types::{AccountId, ShardIdentifier}; +use itp_types::MrEnclave; use litentry_primitives::Identity; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use std::{sync::mpsc::Sender as MpscSender, thread::JoinHandle}; -use teerex_primitives::MrEnclave; #[derive(Clone, Default)] pub struct DirectClientMock { diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 81ad46a745..b83357c121 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -277,7 +277,6 @@ dependencies = [ "sp-std", "strum", "strum_macros", - "teerex-primitives 0.1.0", "thiserror", ] @@ -604,6 +603,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ "num-traits 0.2.16", + "serde 1.0.193", ] [[package]] @@ -796,6 +796,17 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2e66c9d817f1720209181c316d28635c050fa304f9c79e47a520882661b7308" +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "der_derive", + "flagset", +] + [[package]] name = "der" version = "0.7.8" @@ -806,6 +817,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der_derive" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ef71ddb5b3a1f53dee24817c8f70dfa1cb29e804c18d88c228d4bc9c86ee3b9" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote 1.0.33", + "syn 1.0.109", +] + [[package]] name = "derive-syn-parse" version = "0.1.5" @@ -866,7 +889,7 @@ version = "0.16.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" dependencies = [ - "der", + "der 0.7.8", "digest 0.10.7", "elliptic-curve", "rfc6979", @@ -994,7 +1017,7 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "teerex-primitives 0.1.0 (git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42)", + "teerex-primitives", "webpki", ] @@ -1230,6 +1253,12 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "flagset" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d52a7e408202050813e6f1d9addadcaafef3dca7530c7ddfb005d4081cce6779" + [[package]] name = "fnv" version = "1.0.6" @@ -2359,6 +2388,9 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" +dependencies = [ + "litentry-primitives", +] [[package]] name = "itp-sgx-crypto" @@ -2407,7 +2439,6 @@ name = "itp-sgx-runtime-primitives" version = "0.9.0" dependencies = [ "frame-system", - "litentry-primitives", "pallet-balances", "sp-core", "sp-runtime", @@ -2525,14 +2556,6 @@ dependencies = [ "thiserror", ] -[[package]] -name = "itp-teerex-storage" -version = "0.9.0" -dependencies = [ - "itp-storage", - "sp-std", -] - [[package]] name = "itp-test" version = "0.9.0" @@ -2547,10 +2570,10 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-teerex-storage", "itp-time-utils", "itp-types", "jsonrpc-core", + "lc-teebag-storage", "litentry-primitives", "log", "parity-scale-codec", @@ -2836,10 +2859,9 @@ name = "its-validateer-fetch" version = "0.9.0" dependencies = [ "derive_more", - "frame-support", "itp-ocall-api", - "itp-teerex-storage", "itp-types", + "lc-teebag-storage", "parity-scale-codec", "sp-core", "sp-runtime", @@ -2933,6 +2955,15 @@ dependencies = [ "thiserror", ] +[[package]] +name = "lc-teebag-storage" +version = "0.1.0" +dependencies = [ + "itp-storage", + "itp-types", + "sp-std", +] + [[package]] name = "libc" version = "0.2.152" @@ -3010,7 +3041,6 @@ dependencies = [ "bitcoin", "core-primitives", "hex 0.4.3", - "itp-settings", "itp-sgx-crypto", "itp-sgx-io", "itp-utils", @@ -3018,6 +3048,7 @@ dependencies = [ "litentry-hex-utils", "log", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "pallet-teebag", "parity-scale-codec", "rand 0.7.3", "ring 0.16.20", @@ -3031,7 +3062,6 @@ dependencies = [ "sp-std", "strum", "strum_macros", - "teerex-primitives 0.1.0", "thiserror", ] @@ -3373,6 +3403,31 @@ dependencies = [ "sp-std", ] +[[package]] +name = "pallet-teebag" +version = "0.1.0" +dependencies = [ + "base64 0.13.1", + "chrono 0.4.31", + "der 0.6.1", + "frame-support", + "frame-system", + "hex 0.4.3", + "log", + "pallet-timestamp", + "parity-scale-codec", + "ring 0.16.20", + "rustls-webpki", + "scale-info", + "serde 1.0.193", + "serde_json 1.0.107", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "x509-cert", +] + [[package]] name = "pallet-timestamp" version = "4.0.0-dev" @@ -3751,7 +3806,7 @@ dependencies = [ "cc", "sgx_tstd", "spin", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -3764,7 +3819,7 @@ dependencies = [ "libc", "once_cell 1.18.0", "spin", - "untrusted", + "untrusted 0.7.1", "web-sys", "winapi", ] @@ -3854,6 +3909,22 @@ dependencies = [ "webpki", ] +[[package]] +name = "rustls-pki-types" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47003264dea418db67060fa420ad16d0d2f8f0a0360d825c00e177ac52cb5d8" + +[[package]] +name = "rustls-webpki" +version = "0.102.0-alpha.3" +source = "git+https://github.com/rustls/webpki?rev=da923ed#da923edaab56f599971e58773617fb574cd019dc" +dependencies = [ + "ring 0.16.20", + "rustls-pki-types", + "untrusted 0.9.0", +] + [[package]] name = "rustversion" version = "1.0.14" @@ -3980,7 +4051,7 @@ source = "git+https://github.com/mesalock-linux/sct.rs?branch=mesalock_sgx#c4d85 dependencies = [ "ring 0.16.19", "sgx_tstd", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -3990,7 +4061,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" dependencies = [ "base16ct", - "der", + "der 0.7.8", "generic-array 0.14.7", "subtle", "zeroize", @@ -4795,6 +4866,15 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "der 0.6.1", +] + [[package]] name = "ss58-registry" version = "1.43.0" @@ -4926,16 +5006,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "teerex-primitives" -version = "0.1.0" -dependencies = [ - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-std", -] - [[package]] name = "teerex-primitives" version = "0.1.0" @@ -5211,6 +5281,12 @@ version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.1.1" @@ -5313,7 +5389,7 @@ source = "git+https://github.com/mesalock-linux/webpki?branch=mesalock_sgx#8dbe6 dependencies = [ "ring 0.16.19", "sgx_tstd", - "untrusted", + "untrusted 0.7.1", ] [[package]] @@ -5374,6 +5450,18 @@ dependencies = [ "tap", ] +[[package]] +name = "x509-cert" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d224a125dec5adda27d0346b9cae9794830279c4f9c27e4ab0b6c408d54012" +dependencies = [ + "const-oid", + "der 0.6.1", + "flagset", + "spki", +] + [[package]] name = "yasna" version = "0.3.1" diff --git a/bitacross-worker/enclave-runtime/src/attestation.rs b/bitacross-worker/enclave-runtime/src/attestation.rs index 7702cd8f0f..895a37a511 100644 --- a/bitacross-worker/enclave-runtime/src/attestation.rs +++ b/bitacross-worker/enclave-runtime/src/attestation.rs @@ -42,16 +42,19 @@ use itp_attestation_handler::{AttestationHandler, RemoteAttestationType, SgxQlQv use itp_component_container::ComponentGetter; use itp_extrinsics_factory::CreateExtrinsics; use itp_node_api::metadata::{ - pallet_teerex::TeerexCallIndexes, + pallet_teebag::TeebagCallIndexes, provider::{AccessNodeMetadata, Error as MetadataProviderError}, Error as MetadataError, }; use itp_node_api_metadata::NodeMetadata; -use itp_settings::worker::MR_ENCLAVE_SIZE; +use itp_settings::{ + worker::MR_ENCLAVE_SIZE, + worker_mode::{ProvideWorkerMode, WorkerModeProvider}, +}; use itp_sgx_crypto::{ ed25519_derivation::DeriveEd25519, key_repository::AccessKey, Error as SgxCryptoError, }; -use itp_types::OpaqueCall; +use itp_types::{AttestationType, OpaqueCall, WorkerType}; use itp_utils::write_slice_and_whitespace_pad; use log::*; use sgx_types::*; @@ -142,7 +145,8 @@ pub unsafe extern "C" fn generate_ias_ra_extrinsic( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -178,7 +182,8 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -204,7 +209,7 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic( } pub fn generate_dcap_ra_extrinsic_internal( - url: String, + url: Vec, skip_ra: bool, quoting_enclave_target_info: Option<&sgx_target_info_t>, quote_size: Option<&u32>, @@ -287,7 +292,8 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic_from_quote( } let mut url_slice = slice::from_raw_parts(w_url, w_url_size as usize); let url = match String::decode(&mut url_slice) { - Ok(url) => url, + // Litentry: the teebag extrinsic expects an URL with plain utf8 encoded Vec, not string scale-encoded + Ok(url) => url.as_bytes().to_vec(), Err(_) => return EnclaveError::Other("Could not decode url slice to a valid String".into()).into(), }; @@ -311,7 +317,7 @@ pub unsafe extern "C" fn generate_dcap_ra_extrinsic_from_quote( } pub fn generate_dcap_ra_extrinsic_from_quote_internal( - url: String, + url: Vec, quote: &[u8], ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -324,15 +330,24 @@ pub fn generate_dcap_ra_extrinsic_from_quote_internal( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; + let attestation_type = AttestationType::Dcap(Default::default()); // skip_ra should be false here already - let call = OpaqueCall::from_tuple(&(call_ids, quote, url, shielding_pubkey, vc_pubkey)); - + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::BitAcross, + WorkerModeProvider::worker_mode(), + quote, + url, + shielding_pubkey, + vc_pubkey, + attestation_type, + )); info!(" [Enclave] Compose register enclave got extrinsic, returning"); create_extrinsics(call) } pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( - url: String, + url: Vec, quote: &[u8], ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -346,25 +361,34 @@ pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; - let call = OpaqueCall::from_tuple(&(call_ids, quote, url, shielding_pubkey, vc_pubkey)); - + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::BitAcross, + WorkerModeProvider::worker_mode(), + quote, + url, + shielding_pubkey, + vc_pubkey, + AttestationType::Ignore, + )); info!(" [Enclave] Compose register enclave (skip-ra) got extrinsic, returning"); create_extrinsics(call) } fn generate_ias_ra_extrinsic_internal( - url: String, + url: Vec, skip_ra: bool, ) -> EnclaveResult { let attestation_handler = GLOBAL_ATTESTATION_HANDLER_COMPONENT.get()?; let cert_der = attestation_handler.generate_ias_ra_cert(skip_ra)?; - generate_ias_ra_extrinsic_from_der_cert_internal(url, &cert_der) + generate_ias_ra_extrinsic_from_der_cert_internal(url, &cert_der, skip_ra) } pub fn generate_ias_ra_extrinsic_from_der_cert_internal( - url: String, + url: Vec, cert_der: &[u8], + skip_ra: bool, ) -> EnclaveResult { let node_metadata_repo = get_node_metadata_repository_from_integritee_solo_or_parachain()?; @@ -375,9 +399,18 @@ pub fn generate_ias_ra_extrinsic_from_der_cert_internal( let shielding_pubkey = get_shielding_pubkey()?; let vc_pubkey = get_vc_pubkey()?; + let attestation_type = if skip_ra { AttestationType::Ignore } else { AttestationType::Ias }; - let call = OpaqueCall::from_tuple(&(call_ids, cert_der, url, shielding_pubkey, vc_pubkey)); - + let call = OpaqueCall::from_tuple(&( + call_ids, + WorkerType::BitAcross, + WorkerModeProvider::worker_mode(), + cert_der, + url, + shielding_pubkey, + vc_pubkey, + attestation_type, + )); create_extrinsics(call) } diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs index aeeec12a5b..bc9b9efe52 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs @@ -100,6 +100,7 @@ impl IntegriteeParachainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs index 207115d47f..07f3801d3f 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs @@ -99,6 +99,7 @@ impl IntegriteeSolochainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs index e4a08cce6d..0d278eeef5 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs @@ -104,6 +104,7 @@ impl TargetAParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs index e26ce6833d..ef9f6c07ed 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs @@ -97,6 +97,7 @@ impl TargetASolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs index 36d83a0e06..d44910c478 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs @@ -104,6 +104,7 @@ impl TargetBParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs index 015ff2cea6..ab4278ce79 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs @@ -97,6 +97,7 @@ impl TargetBSolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index d5b0a39c93..03461047f0 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -24,9 +24,9 @@ #![cfg_attr(not(target_env = "sgx"), no_std)] #![cfg_attr(target_env = "sgx", feature(rustc_private))] #![allow(clippy::missing_safety_doc)] +#![allow(clippy::unreachable)] #![warn( clippy::unwrap_used, - clippy::unreachable, /* comment out for the moment. There are some upstream code `unimplemented` */ // clippy::unimplemented, // clippy::panic_in_result_fn, diff --git a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs index 93b9370d57..e02f775f0d 100644 --- a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs +++ b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs @@ -339,8 +339,8 @@ where if_not_production!({ use itp_types::{MrEnclave, SidechainBlockNumber}; - // state_updateScheduledEnclave, params: sidechainBlockNumber, hex encoded mrenclave - io.add_sync_method("state_updateScheduledEnclave", move |params: Params| { + // state_setScheduledEnclave, params: sidechainBlockNumber, hex encoded mrenclave + io.add_sync_method("state_setScheduledEnclave", move |params: Params| { match params.parse::<(SidechainBlockNumber, String)>() { Ok((bn, mrenclave)) => return match hex::decode(&mrenclave) { @@ -492,8 +492,11 @@ fn forward_dcap_quote_inner(params: Params) -> Result { litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); - let ext = generate_dcap_ra_extrinsic_from_quote_internal(url, &encoded_quote_to_forward) - .map_err(|e| format!("{:?}", e))?; + let ext = generate_dcap_ra_extrinsic_from_quote_internal( + url.as_bytes().to_vec(), + &encoded_quote_to_forward, + ) + .map_err(|e| format!("{:?}", e))?; let validator_access = get_validator_accessor_from_integritee_solo_or_parachain() .map_err(|e| format!("{:?}", e))?; @@ -522,8 +525,12 @@ fn attesteer_forward_ias_attestation_report_inner( litentry_hex_utils::decode_hex(param).map_err(|e| format!("{:?}", e))?; let url = String::new(); - let ext = generate_ias_ra_extrinsic_from_der_cert_internal(url, &ias_attestation_report) - .map_err(|e| format!("{:?}", e))?; + let ext = generate_ias_ra_extrinsic_from_der_cert_internal( + url.as_bytes().to_vec(), + &ias_attestation_report, + false, + ) + .map_err(|e| format!("{:?}", e))?; let validator_access = get_validator_accessor_from_integritee_solo_or_parachain() .map_err(|e| format!("{:?}", e))?; diff --git a/bitacross-worker/enclave-runtime/src/test/fixtures/components.rs b/bitacross-worker/enclave-runtime/src/test/fixtures/components.rs index dd1237672d..34f3606010 100644 --- a/bitacross-worker/enclave-runtime/src/test/fixtures/components.rs +++ b/bitacross-worker/enclave-runtime/src/test/fixtures/components.rs @@ -27,7 +27,7 @@ use itp_stf_primitives::{ }; use itp_top_pool::pool::Options as PoolOptions; use itp_top_pool_author::api::SidechainApi; -use itp_types::{Block as ParentchainBlock, Enclave, ShardIdentifier}; +use itp_types::{Block as ParentchainBlock, ShardIdentifier}; use sp_core::{ed25519, Pair, H256}; use sp_runtime::traits::Header as HeaderTrait; use std::{boxed::Box, sync::Arc, vec::Vec}; @@ -41,13 +41,7 @@ pub(crate) fn create_ocall_api>( header: &Header, signer: &TestSigner, ) -> Arc { - let enclave_validateer = Enclave::new( - signer.public().into(), - Default::default(), - Default::default(), - Default::default(), - ); - Arc::new(TestOCallApi::default().add_validateer_set(header, Some(vec![enclave_validateer]))) + Arc::new(TestOCallApi::default().add_validateer_set(header, Some(vec![signer.public().into()]))) } pub(crate) fn encrypt_trusted_operation( diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index d5f7b167da..e24dd7198d 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -151,6 +151,7 @@ pub extern "C" fn test_main_entrance() -> size_t { sidechain_aura_tests::produce_sidechain_block_and_import_it, sidechain_event_tests::ensure_events_get_reset_upon_block_proposal, top_pool_tests::process_indirect_call_in_top_pool, + // TODO: Litentry disables it for now (P-494) // top_pool_tests::submit_shielding_call_to_top_pool, // tls_ra unit tests tls_ra::seal_handler::test::seal_shielding_key_works, diff --git a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs index 236b437d27..cb4e5e7a1d 100644 --- a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -22,21 +22,16 @@ use crate::test::{ create_ocall_api, create_top_pool, encrypt_trusted_operation, sign_trusted_call, }, initialize_test_state::init_state, - test_setup::TestStf, }, mocks::types::{ TestShieldingKey, TestShieldingKeyRepo, TestSigner, TestStateHandler, TestTopPoolAuthor, }, }; use codec::Encode; -use ita_parentchain_interface::integritee; use ita_stf::{ test_genesis::{endowed_account, unendowed_account}, Getter, TrustedCall, TrustedCallSigned, }; -use itc_parentchain::indirect_calls_executor::{ - mock::TestEventCreator, ExecuteIndirectCalls, IndirectCallsExecutor, -}; use itc_parentchain_test::{ parentchain_block_builder::ParentchainBlockBuilder, parentchain_header_builder::ParentchainHeaderBuilder, @@ -46,22 +41,18 @@ use itp_node_api::{ ExtrinsicParams, ParentchainAdditionalParams, ParentchainExtrinsicParams, ParentchainUncheckedExtrinsic, }, - metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}, + metadata::metadata_mocks::NodeMetadataMock, }; -use itp_node_api_metadata::pallet_teerex::TeerexCallIndexes; +use itp_node_api_metadata::pallet_teebag::TeebagCallIndexes; use itp_ocall_api::EnclaveAttestationOCallApi; use itp_sgx_crypto::ShieldingCryptoEncrypt; -use itp_stf_executor::enclave_signer::StfEnclaveSigner; -use itp_stf_primitives::{traits::TrustedCallVerification, types::TrustedOperation}; -use itp_stf_state_observer::mock::ObserveStateMock; +use itp_stf_primitives::types::TrustedOperation; use itp_test::mock::metrics_ocall_mock::MetricsOCallMock; use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, traits::AuthorApi, }; -use itp_types::{ - parentchain::Address, AccountId, Block, RsaRequest, ShardIdentifier, ShieldFundsFn, H256, -}; +use itp_types::{parentchain::Address, Block, CallWorkerFn, RsaRequest, ShardIdentifier, H256}; use jsonrpc_core::futures::executor; use litentry_primitives::Identity; use log::*; @@ -107,6 +98,9 @@ pub fn process_indirect_call_in_top_pool() { assert_eq!(1, top_pool_author.get_pending_trusted_calls(shard_id).len()); } +/* +// TODO: use our trusted call for testing - see P-494 + pub fn submit_shielding_call_to_top_pool() { let _ = env_logger::builder().is_test(true).try_init(); @@ -172,6 +166,8 @@ pub fn submit_shielding_call_to_top_pool() { assert!(trusted_call.verify_signature(&mr_enclave.m, &shard_id)); } +*/ + fn encrypted_indirect_call< AttestationApi: EnclaveAttestationOCallApi, ShieldingKey: ShieldingCryptoEncrypt, @@ -194,11 +190,10 @@ fn encrypted_indirect_call< encrypt_trusted_operation(shielding_key, &trusted_operation) } -fn create_shielding_call_extrinsic( - shard: ShardIdentifier, - shielding_key: &ShieldingKey, +fn create_opaque_call_extrinsic( + _shard: ShardIdentifier, + _shielding_key: &ShieldingKey, ) -> Block { - let target_account = shielding_key.encrypt(&AccountId::new([2u8; 32]).encode()).unwrap(); let test_signer = ed25519::Pair::from_seed(b"33345678901234567890123456789012"); let signature = test_signer.sign(&[0u8]); @@ -212,15 +207,10 @@ fn create_shielding_call_extrinsic( let dummy_node_metadata = NodeMetadataMock::new(); - let shield_funds_indexes = dummy_node_metadata.shield_funds_call_indexes().unwrap(); + let call_index = dummy_node_metadata.post_opaque_task_call_indexes().unwrap(); let opaque_extrinsic = OpaqueExtrinsic::from_bytes( - ParentchainUncheckedExtrinsic::::new_signed( - ( - shield_funds_indexes, - target_account, - ita_stf::test_genesis::SECOND_ENDOWED_ACC_FUNDS, - shard, - ), + ParentchainUncheckedExtrinsic::::new_signed( + (call_index, RsaRequest::default()), Address::Address32([1u8; 32]), MultiSignature::Ed25519(signature), default_extra_for_test.signed_extra(), diff --git a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs index e5fbed0a09..abf5cd230a 100644 --- a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs +++ b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs @@ -57,6 +57,7 @@ impl From for ProvisioningPayload { match m { WorkerMode::OffChainWorker => ProvisioningPayload::ShieldingKeyAndLightClient, WorkerMode::Sidechain => ProvisioningPayload::Everything, + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), } } } diff --git a/bitacross-worker/litentry/core/teebag-storage/Cargo.toml b/bitacross-worker/litentry/core/teebag-storage/Cargo.toml new file mode 100644 index 0000000000..c68b9e0d5e --- /dev/null +++ b/bitacross-worker/litentry/core/teebag-storage/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "lc-teebag-storage" +version = "0.1.0" +authors = ['Trust Computing GmbH '] +edition = "2021" + +[dependencies] +itp-storage = { path = "../../../core-primitives/storage", default-features = false } +itp-types = { path = "../../../core-primitives/types", default-features = false } +sp-std = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42", default-features = false } + +[features] +default = ["std"] +std = [ + "sp-std/std", + "itp-storage/std", + "itp-types/std", +] diff --git a/bitacross-worker/litentry/core/teebag-storage/src/lib.rs b/bitacross-worker/litentry/core/teebag-storage/src/lib.rs new file mode 100644 index 0000000000..3f931d9f7e --- /dev/null +++ b/bitacross-worker/litentry/core/teebag-storage/src/lib.rs @@ -0,0 +1,48 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#![cfg_attr(not(feature = "std"), no_std)] + +use itp_storage::{storage_map_key, StorageHasher}; +use itp_types::WorkerType; +use sp_std::prelude::Vec; + +pub struct TeebagStorage; + +pub trait StoragePrefix { + fn prefix() -> &'static str; +} + +impl StoragePrefix for TeebagStorage { + fn prefix() -> &'static str { + "Teebag" + } +} + +pub trait TeebagStorageKeys { + fn enclave_identifier(worker_type: WorkerType) -> Vec; +} + +impl TeebagStorageKeys for S { + fn enclave_identifier(worker_type: WorkerType) -> Vec { + storage_map_key( + Self::prefix(), + "EnclaveIdentifier", + &worker_type, + &StorageHasher::Blake2_128Concat, + ) + } +} diff --git a/bitacross-worker/litentry/primitives/Cargo.toml b/bitacross-worker/litentry/primitives/Cargo.toml index 6de056d426..23cf074100 100644 --- a/bitacross-worker/litentry/primitives/Cargo.toml +++ b/bitacross-worker/litentry/primitives/Cargo.toml @@ -25,21 +25,18 @@ strum = { version = "0.26", default-features = false } strum_macros = { version = "0.26", default-features = false } thiserror = { version = "1.0.26", optional = true } - # sgx dependencies base64_sgx = { package = "base64", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base64-sgx", optional = true } sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true, features = ["net", "thread"] } thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } - # internal dependencies -itp-settings = { path = "../../core-primitives/settings", default-features = false } itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } itp-sgx-io = { path = "../../core-primitives/sgx/io", default-features = false } itp-utils = { path = "../../core-primitives/utils", default-features = false } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } +pallet-teebag = { path = "../../../pallets/teebag", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../primitives/core", default-features = false } -teerex-primitives = { path = "../../../primitives/teerex", default-features = false } [dev-dependencies] base64 = { version = "0.13", features = ["alloc"] } @@ -68,7 +65,7 @@ std = [ "sp-runtime/std", "ring/std", "parentchain-primitives/std", - "teerex-primitives/std", + "pallet-teebag/std", "rand", "log/std", "bitcoin/std", diff --git a/bitacross-worker/litentry/primitives/src/aes_request.rs b/bitacross-worker/litentry/primitives/src/aes_request.rs index 998d642837..e15c88e40b 100644 --- a/bitacross-worker/litentry/primitives/src/aes_request.rs +++ b/bitacross-worker/litentry/primitives/src/aes_request.rs @@ -17,7 +17,7 @@ #[cfg(all(not(feature = "std"), feature = "sgx"))] extern crate sgx_tstd as std; -/// A morphling of itp_types::RsaRequest which stems from teerex_primitives::RsaRequest +/// A morphling of itp_types::RsaRequest which stems from teebag::RsaRequest /// /// Instead of encrypting the TrustedCallSigned with the TEE's shielding key, we encrypt /// it with a 32-byte ephemeral AES key which is generated from the client's side, and diff --git a/bitacross-worker/litentry/primitives/src/lib.rs b/bitacross-worker/litentry/primitives/src/lib.rs index 9ac4ac0dd1..6acab463cc 100644 --- a/bitacross-worker/litentry/primitives/src/lib.rs +++ b/bitacross-worker/litentry/primitives/src/lib.rs @@ -43,6 +43,10 @@ use codec::{Decode, Encode, MaxEncodedLen}; use itp_sgx_crypto::ShieldingCryptoDecrypt; use litentry_hex_utils::hex_encode; use log::error; +pub use pallet_teebag::{ + decl_rsa_request, extract_tcb_info_from_raw_dcap_quote, AttestationType, Enclave, + EnclaveFingerprint, MrEnclave, ShardIdentifier, SidechainBlockNumber, WorkerMode, WorkerType, +}; pub use parentchain_primitives::{ identity::*, AccountId as ParentchainAccountId, Balance as ParentchainBalance, BlockNumber as ParentchainBlockNumber, ErrorDetail, ErrorString, Hash as ParentchainHash, @@ -57,7 +61,6 @@ use sp_io::{ }; use sp_runtime::traits::Verify; use std::string::{String, ToString}; -pub use teerex_primitives::{decl_rsa_request, ShardIdentifier, SidechainBlockNumber}; #[cfg(feature = "std")] use serde::{Deserialize, Serialize}; diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index e56b810cb3..1c3315b960 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -75,8 +75,6 @@ litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-macros = { path = "../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } -sgx-verify = { path = "../../pallets/teerex/sgx-verify", default-features = false } -teerex-primitives = { path = "../../primitives/teerex", default-features = false } [features] default = [] diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 36f24fcd9d..2072418dc2 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -35,7 +35,7 @@ use itp_enclave_api::{ sidechain::Sidechain, }; use itp_node_api::{ - api_client::{AccountApi, PalletTeerexApi, ParentchainApi}, + api_client::{AccountApi, PalletTeebagApi, ParentchainApi}, metadata::NodeMetadata, node_api_factory::{CreateNodeApi, NodeApiFactory}, }; @@ -46,6 +46,7 @@ use its_peer_fetch::{ use its_primitives::types::block::SignedBlock as SignedSidechainBlock; use its_storage::{interface::FetchBlocks, BlockPruner, SidechainStorageLock}; use litentry_macros::if_production_or; +use litentry_primitives::{Enclave as TeebagEnclave, ShardIdentifier, WorkerType}; use log::*; use my_node_runtime::{Hash, Header, RuntimeEvent}; use regex::Regex; @@ -56,10 +57,9 @@ use substrate_api_client::{ ac_primitives::serde_impls::StorageKey, api::XtStatus, rpc::HandleSubscription, storage_key, GetChainInfo, GetStorage, SubmitAndWatch, SubscribeChain, SubscribeEvents, }; -use teerex_primitives::{Enclave as TeerexEnclave, ShardIdentifier}; #[cfg(feature = "dcap")] -use sgx_verify::extract_tcb_info_from_raw_dcap_quote; +use litentry_primitives::extract_tcb_info_from_raw_dcap_quote; use itc_parentchain::primitives::ParentchainId; use itp_enclave_api::Enclave; @@ -492,86 +492,51 @@ fn start_worker( #[cfg(feature = "dcap")] let register_xt = move || enclave2.generate_dcap_ra_extrinsic(&trusted_url2, skip_ra).unwrap(); - let mut register_enclave_xt_header: Option
= None; - let mut we_are_primary_validateer: bool = false; - let send_register_xt = move || { println!("[+] Send register enclave extrinsic"); send_extrinsic(register_xt(), &node_api2, &tee_accountid2, is_development_mode) }; - // litentry: check if the enclave is already registered - // TODO: revisit the registration process (P-10) - match litentry_rpc_api.get_keys(storage_key("Teerex", "EnclaveRegistry"), None) { - Ok(Some(keys)) => { - let trusted_url = trusted_url.as_bytes().to_vec(); - let mrenclave = mrenclave.0.to_vec(); - let mut found = false; - for key in keys { - let key = if key.starts_with("0x") { - let bytes = &key.as_bytes()[b"0x".len()..]; - hex::decode(bytes).unwrap() - } else { - hex::decode(key.as_bytes()).unwrap() - }; - match litentry_rpc_api.get_storage_by_key::>>( - StorageKey(key.clone()), - None, - ) { - Ok(Some(value)) => { - if value.mr_enclave.to_vec() == mrenclave && value.url == trusted_url { - // After calling the perform_ra function, the nonce will be incremented by 1, - // so enclave is already registered, we should reset the nonce_cache - let nonce = - litentry_rpc_api.get_account_next_index(&tee_accountid).unwrap(); - enclave - .set_nonce(nonce, ParentchainId::Litentry) - .expect("Could not set nonce of enclave. Returning here..."); - found = true; - info!("fond enclave: {:?}", value); - break - } - }, - Ok(None) => { - warn!("not found from key: {:?}", key); - }, - Err(_) => {}, - } - } - if !found { - // Todo: Can't unwrap here because the extrinsic is for some reason not found in the block - // even if it was successful: https://github.com/scs/substrate-api-client/issues/624. - let register_enclave_block_hash = send_register_xt(); - let api_register_enclave_xt_header = - litentry_rpc_api.get_header(register_enclave_block_hash).unwrap().unwrap(); - - // TODO: #1451: Fix api-client type hacks - // TODO(Litentry): keep an eye on it - it's a hacky way to convert `SubstrateHeader` to `Header` - let header = - Header::decode(&mut api_register_enclave_xt_header.encode().as_slice()) - .expect("Can decode previously encoded header; qed"); - - println!( - "[+] Enclave registered at block number: {:?}, hash: {:?}", - header.number(), - header.hash() - ); + // Litentry: send the registration extrinsic regardless of being registered or not, + // the reason is the mrenclave could change in between, so we rely on the + // on-chain logic to handle everything. + // this is the same behavior as upstream + let register_enclave_block_hash = + send_register_xt().expect("enclave RA registration must be successful to continue"); - register_enclave_xt_header = Some(header); - } - }, - _ => panic!("unknown error"), - } + let api_register_enclave_xt_header = + litentry_rpc_api.get_header(Some(register_enclave_block_hash)).unwrap().unwrap(); - if let Some(register_enclave_xt_header) = register_enclave_xt_header.clone() { - we_are_primary_validateer = - we_are_primary_worker(&litentry_rpc_api, ®ister_enclave_xt_header).unwrap(); - } + // TODO: #1451: Fix api-client type hacks + let register_enclave_xt_header = + Header::decode(&mut api_register_enclave_xt_header.encode().as_slice()) + .expect("Can decode previously encoded header; qed"); - if we_are_primary_validateer { - println!("[+] We are the primary worker"); + println!( + "[+] Enclave registered at block number: {:?}, hash: {:?}", + register_enclave_xt_header.number(), + register_enclave_xt_header.hash() + ); + + // double-check + let my_enclave = litentry_rpc_api + .enclave(&tee_accountid, None) + .unwrap() + .expect("our enclave should be registered at this point"); + trace!("verified that our enclave is registered: {:?}", my_enclave); + + let is_primary_enclave = match litentry_rpc_api + .primary_enclave_identifier_for_shard(WorkerType::BitAcross, shard, None) + .unwrap() + { + Some(account) => account == tee_accountid, + None => false, + }; + + if is_primary_enclave { + println!("[+] We are the primary enclave"); } else { - println!("[+] We are NOT the primary worker"); + println!("[+] We are NOT the primary enclave"); } initialization_handler.registered_on_parentchain(); @@ -606,7 +571,7 @@ fn start_worker( let last_synced_header = sidechain_init_block_production( enclave.clone(), register_enclave_xt_header, - we_are_primary_validateer, + is_primary_enclave, parentchain_handler.clone(), sidechain_storage, &last_synced_header, @@ -618,10 +583,11 @@ fn start_worker( start_parentchain_header_subscription_thread(parentchain_handler, last_synced_header); - init_provided_shard_vault(shard, &enclave, we_are_primary_validateer); + init_provided_shard_vault(shard, &enclave, is_primary_enclave); spawn_worker_for_shard_polling(shard, litentry_rpc_api.clone(), initialization_handler); }, + WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), } if let Some(url) = config.target_a_parentchain_rpc_endpoint() { @@ -661,14 +627,14 @@ fn start_worker( fn init_provided_shard_vault( shard: &ShardIdentifier, enclave: &Arc, - we_are_primary_validateer: bool, + is_primary_enclave: bool, ) { if let Ok(shard_vault) = enclave.get_ecc_vault_pubkey(shard) { println!( "[Litentry] shard vault account is already initialized in state: {}", shard_vault.to_ss58check() ); - } else if we_are_primary_validateer { + } else if is_primary_enclave { println!("[Litentry] initializing proxied shard vault account now"); enclave.init_proxied_shard_vault(shard, &ParentchainId::Litentry).unwrap(); println!( @@ -777,9 +743,8 @@ where } /// Start polling loop to wait until we have a worker for a shard registered on -/// the parentchain (TEEREX WorkerForShard). This is the pre-requisite to be +/// the parentchain (TEEBAG EnclaveIdentifier). This is the pre-requisite to be /// considered initialized and ready for the next worker to start (in sidechain mode only). -/// considered initialized and ready for the next worker to start. fn spawn_worker_for_shard_polling( shard: &ShardIdentifier, node_api: ParentchainApi, @@ -793,7 +758,11 @@ fn spawn_worker_for_shard_polling( loop { info!("Polling for worker for shard ({} seconds interval)", POLL_INTERVAL_SECS); - if let Ok(Some(_enclave)) = node_api.worker_for_shard(&shard_for_initialized, None) { + if let Ok(Some(_account)) = node_api.primary_enclave_identifier_for_shard( + WorkerType::BitAcross, + &shard_for_initialized, + None, + ) { // Set that the service is initialized. initialization_handler.worker_for_shard_registered(); println!("[+] Found `WorkerForShard` on parentchain state",); @@ -1006,13 +975,3 @@ pub fn enclave_account(enclave_api: &E) -> AccountId32 { trace!("[+] Got ed25519 account of TEE = {}", tee_public.to_ss58check()); AccountId32::from(*tee_public.as_array_ref()) } - -/// Checks if we are the first validateer to register on the parentchain. -fn we_are_primary_worker( - node_api: &ParentchainApi, - register_enclave_xt_header: &Header, -) -> Result { - let enclave_count_of_previous_block = - node_api.enclave_count(Some(*register_enclave_xt_header.parent_hash()))?; - Ok(enclave_count_of_previous_block == 0) -} diff --git a/bitacross-worker/service/src/sidechain_setup.rs b/bitacross-worker/service/src/sidechain_setup.rs index a499c85fed..55a17cdbe1 100644 --- a/bitacross-worker/service/src/sidechain_setup.rs +++ b/bitacross-worker/service/src/sidechain_setup.rs @@ -57,7 +57,7 @@ pub(crate) fn sidechain_start_untrusted_rpc_server( #[allow(clippy::too_many_arguments)] pub(crate) fn sidechain_init_block_production( enclave: Arc, - register_enclave_xt_header: Option
, + register_enclave_xt_header: Header, we_are_primary_validateer: bool, parentchain_handler: Arc, sidechain_storage: Arc, @@ -80,7 +80,7 @@ where ); updated_header = Some(parentchain_handler.sync_and_import_parentchain_until( last_synced_header, - ®ister_enclave_xt_header.unwrap(), + ®ister_enclave_xt_header, overriden_start_block, )?); } diff --git a/bitacross-worker/service/src/sync_state.rs b/bitacross-worker/service/src/sync_state.rs index 21d2d4d7e0..e17545d770 100644 --- a/bitacross-worker/service/src/sync_state.rs +++ b/bitacross-worker/service/src/sync_state.rs @@ -26,15 +26,15 @@ use itp_enclave_api::{ enclave_base::EnclaveBase, remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, }; -use itp_node_api::api_client::PalletTeerexApi; +use itp_node_api::api_client::PalletTeebagApi; use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; -use itp_types::ShardIdentifier; +use itp_types::{ShardIdentifier, WorkerType}; use sgx_types::sgx_quote_sign_type_t; use std::string::String; pub(crate) fn sync_state< E: TlsRemoteAttestation + EnclaveBase + RemoteAttestation, - NodeApi: PalletTeerexApi, + NodeApi: PalletTeebagApi, WorkerModeProvider: ProvideWorkerMode, >( node_api: &NodeApi, @@ -70,30 +70,32 @@ pub(crate) fn sync_state< /// Note: The sidechainblock author will only change whenever a new parentchain block is /// produced. And even then, it might be the same as the last block. So if several workers /// are started in a timely manner, they will all get the same url. -async fn get_author_url_of_last_finalized_sidechain_block( +async fn get_author_url_of_last_finalized_sidechain_block( node_api: &NodeApi, shard: &ShardIdentifier, ) -> Result { let enclave = node_api - .worker_for_shard(shard, None)? + .primary_enclave_for_shard(WorkerType::BitAcross, shard, None)? .ok_or_else(|| Error::NoWorkerForShardFound(*shard))?; - let worker_api_direct = DirectWorkerApi::new(enclave.url); + let worker_api_direct = + DirectWorkerApi::new(String::from_utf8_lossy(enclave.url.as_slice()).to_string()); Ok(worker_api_direct.get_mu_ra_url()?) } /// Returns the url of the first Enclave that matches our own MRENCLAVE. /// /// This should be run before we register ourselves as enclave, to ensure we don't get our own url. -async fn get_enclave_url_of_first_registered( +async fn get_enclave_url_of_first_registered( node_api: &NodeApi, enclave_api: &EnclaveApi, ) -> Result { - let self_mr_enclave = enclave_api.get_fingerprint()?; + let self_mrenclave = enclave_api.get_fingerprint()?; let first_enclave = node_api - .all_enclaves(None)? + .all_enclaves(WorkerType::BitAcross, None)? .into_iter() - .find(|e| e.mr_enclave == self_mr_enclave.to_fixed_bytes()) + .find(|e| e.mrenclave == self_mrenclave.to_fixed_bytes()) .ok_or(Error::NoPeerWorkerFound)?; - let worker_api_direct = DirectWorkerApi::new(first_enclave.url); + let worker_api_direct = + DirectWorkerApi::new(String::from_utf8_lossy(first_enclave.url.as_slice()).to_string()); Ok(worker_api_direct.get_mu_ra_url()?) } diff --git a/bitacross-worker/service/src/tests/mock.rs b/bitacross-worker/service/src/tests/mock.rs index 0587669dc4..0f96dc3f1e 100644 --- a/bitacross-worker/service/src/tests/mock.rs +++ b/bitacross-worker/service/src/tests/mock.rs @@ -15,8 +15,8 @@ */ -use itp_node_api::api_client::{ApiResult, PalletTeerexApi}; -use itp_types::{Enclave, MrEnclave, ShardIdentifier, H256 as Hash}; +use itp_node_api::api_client::{ApiResult, PalletTeebagApi}; +use itp_types::{AccountId, Enclave, MrEnclave, ShardIdentifier, WorkerType, H256 as Hash}; use std::collections::HashSet; pub struct TestNodeApi; @@ -26,43 +26,44 @@ pub const W2_URL: &str = "127.0.0.1:33333"; pub fn enclaves() -> Vec { vec![ - Enclave::new([0; 32].into(), [1; 32], 1, format!("wss://{}", W1_URL)), - Enclave::new([2; 32].into(), [3; 32], 2, format!("wss://{}", W2_URL)), + Enclave::new(WorkerType::BitAcross).with_url(W1_URL.into()), + Enclave::new(WorkerType::BitAcross).with_url(W2_URL.into()), ] } -impl PalletTeerexApi for TestNodeApi { +impl PalletTeebagApi for TestNodeApi { type Hash = Hash; - fn enclave(&self, index: u64, _at_block: Option) -> ApiResult> { - Ok(Some(enclaves().remove(index as usize))) + fn enclave(&self, _account: &AccountId, _at_block: Option) -> ApiResult> { + unreachable!() } - fn enclave_count(&self, _at_block: Option) -> ApiResult { + fn enclave_count(&self, _worker_type: WorkerType, _at_block: Option) -> ApiResult { unreachable!() } - fn all_enclaves(&self, _at_block: Option) -> ApiResult> { + fn all_enclaves( + &self, + _worker_type: WorkerType, + _at_block: Option, + ) -> ApiResult> { Ok(enclaves()) } - fn worker_for_shard( + fn primary_enclave_identifier_for_shard( &self, - _: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { unreachable!() } - fn latest_ipfs_hash( + + fn primary_enclave_for_shard( &self, - _: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { + worker_type: WorkerType, + shard: &ShardIdentifier, + at_block: Option, + ) -> ApiResult> { unreachable!() } - - fn all_scheduled_mrenclaves(&self, _at_block: Option) -> ApiResult> { - let enclaves = enclaves(); - let mr_enclaves: HashSet<_> = enclaves.into_iter().map(|e| e.mr_enclave).collect(); - Ok(mr_enclaves.into_iter().collect()) - } } diff --git a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs index af27dd3fae..e9b5d4c884 100644 --- a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs +++ b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs @@ -24,10 +24,9 @@ use itc_parentchain::primitives::{ use itp_enclave_api::{enclave_base::EnclaveBase, sidechain::Sidechain, EnclaveResult}; use itp_settings::worker::MR_ENCLAVE_SIZE; use itp_storage::StorageProof; -use itp_types::ShardIdentifier; +use itp_types::{EnclaveFingerprint, ShardIdentifier}; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sp_core::ed25519; -use teerex_primitives::EnclaveFingerprint; /// mock for EnclaveBase - use in tests pub struct EnclaveMock; diff --git a/bitacross-worker/service/src/worker.rs b/bitacross-worker/service/src/worker.rs index 638e4f081b..db638ec998 100644 --- a/bitacross-worker/service/src/worker.rs +++ b/bitacross-worker/service/src/worker.rs @@ -25,13 +25,14 @@ use async_trait::async_trait; use codec::{Decode, Encode}; use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; use itp_enclave_api::enclave_base::EnclaveBase; -use itp_node_api::{api_client::PalletTeerexApi, node_api_factory::CreateNodeApi}; +use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; use its_primitives::types::SignedBlock as SignedSidechainBlock; use its_rpc_handler::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; use jsonrpsee::{ types::{to_json_value, traits::Client}, ws_client::WsClientBuilder, }; +use litentry_primitives::WorkerType; use log::*; use std::{ collections::HashSet, @@ -187,11 +188,11 @@ where .node_api_factory .create_api() .map_err(|e| Error::Custom(format!("Failed to create NodeApi: {:?}", e).into()))?; - let enclaves = node_api.all_enclaves(None)?; + let enclaves = node_api.all_enclaves(WorkerType::BitAcross, None)?; let mut peer_urls = HashSet::::new(); for enclave in enclaves { // FIXME: This is temporary only, as block broadcasting should be moved to trusted ws server. - let enclave_url = enclave.url.clone(); + let enclave_url = String::from_utf8_lossy(enclave.url.as_slice()).to_string(); let worker_api_direct = DirectWorkerApi::new(enclave_url.clone()); match worker_api_direct.get_untrusted_worker_url() { Ok(untrusted_worker_url) => { diff --git a/bitacross-worker/sidechain/consensus/aura/src/lib.rs b/bitacross-worker/sidechain/consensus/aura/src/lib.rs index dbbe099e4e..4114852c73 100644 --- a/bitacross-worker/sidechain/consensus/aura/src/lib.rs +++ b/bitacross-worker/sidechain/consensus/aura/src/lib.rs @@ -47,7 +47,7 @@ use its_primitives::{ use its_validateer_fetch::ValidateerFetch; use lc_scheduled_enclave::ScheduledEnclaveUpdater; use litentry_hex_utils::hex_encode; -use sp_core::ByteArray; +use sp_core::crypto::UncheckedFrom; use sp_runtime::{ app_crypto::{sp_core::H256, Pair}, generic::SignedBlock as SignedParentchainBlock, @@ -189,6 +189,7 @@ impl< StateHandler, > where AuthorityPair: Pair, + AuthorityPair::Public: UncheckedFrom<[u8; 32]>, // todo: Relax hash trait bound, but this needs a change to some other parts in the code. ParentchainBlock: ParentchainBlockTrait, E: Environment, @@ -211,10 +212,6 @@ impl< type ScheduledEnclave = ScheduledEnclave; type StateHandler = StateHandler; - fn logging_target(&self) -> &'static str { - "aura" - } - fn get_scheduled_enclave(&mut self) -> Arc { self.scheduled_enclave.clone() } @@ -226,7 +223,6 @@ impl< fn epoch_data( &self, header: &ParentchainBlock::Header, - _shard: ShardIdentifierFor, _slot: Slot, ) -> Result { authorities::<_, AuthorityPair, ParentchainBlock::Header>(&self.ocall_api, header) @@ -246,15 +242,12 @@ impl< let expected_author = slot_author::(slot, epoch_data)?; if expected_author == &self.authority_pair.public() { - log::info!(target: self.logging_target(), "Claiming slot ({})", *slot); + log::info!("Claiming slot ({})", *slot); return Some(self.authority_pair.public()) } if self.claim_strategy == SlotClaimStrategy::Always { - log::debug!( - target: self.logging_target(), - "Not our slot but we still claim it." - ); + log::debug!("Not our slot but we still claim it."); return Some(self.authority_pair.public()) } @@ -281,7 +274,10 @@ impl< &self, parentchain_header_hash: &::Hash, ) -> Result, ConsensusError> { - log::trace!(target: self.logging_target(), "import Integritee blocks until {}", hex_encode(parentchain_header_hash.encode().as_ref())); + log::trace!( + "import Integritee blocks until {}", + hex_encode(parentchain_header_hash.encode().as_ref()) + ); let maybe_parentchain_block = self .parentchain_integritee_import_trigger .import_until(|parentchain_block| { @@ -296,7 +292,10 @@ impl< &self, parentchain_header_hash: &::Hash, ) -> Result, ConsensusError> { - log::trace!(target: self.logging_target(), "import TargetA blocks until {}", hex_encode(parentchain_header_hash.encode().as_ref())); + log::trace!( + "import TargetA blocks until {}", + hex_encode(parentchain_header_hash.encode().as_ref()) + ); let maybe_parentchain_block = self .maybe_parentchain_target_a_import_trigger .clone() @@ -313,7 +312,10 @@ impl< &self, parentchain_header_hash: &::Hash, ) -> Result, ConsensusError> { - log::trace!(target: self.logging_target(), "import TargetB blocks until {}", hex_encode(parentchain_header_hash.encode().as_ref())); + log::trace!( + "import TargetB blocks until {}", + hex_encode(parentchain_header_hash.encode().as_ref()) + ); let maybe_parentchain_block = self .maybe_parentchain_target_b_import_trigger .clone() @@ -389,13 +391,14 @@ fn authorities( where ValidateerFetcher: ValidateerFetch + EnclaveOnChainOCallApi, P: Pair, + P::Public: UncheckedFrom<[u8; 32]>, ParentchainHeader: ParentchainHeaderTrait, { Ok(ocall_api - .current_validateers(header) + .current_validateers::(header) .map_err(|e| ConsensusError::CouldNotGetAuthorities(e.to_string()))? - .into_iter() - .filter_map(|e| AuthorityId::

::from_slice(e.pubkey.as_ref()).ok()) + .iter() + .map(|account| P::Public::unchecked_from(*account.as_ref())) .collect()) } @@ -409,14 +412,14 @@ pub enum AnyImportTrigger { mod tests { use super::*; use crate::test::{ - fixtures::{types::TestAura, validateer, SLOT_DURATION}, + fixtures::{types::TestAura, SLOT_DURATION}, mocks::environment_mock::{EnvironmentMock, OutdatedBlockEnvironmentMock}, }; use itc_parentchain_block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; use itc_parentchain_test::{ParentchainBlockBuilder, ParentchainHeaderBuilder}; use itp_test::mock::{handle_state_mock::HandleStateMock, onchain_mock::OnchainMock}; use itp_types::{ - Block as ParentchainBlock, Enclave, Header as ParentchainHeader, ShardIdentifier, + AccountId, Block as ParentchainBlock, Header as ParentchainHeader, ShardIdentifier, SignedBlock as SignedParentchainBlock, }; use its_consensus_slots::PerShardSlotWorkerScheduler; @@ -481,8 +484,8 @@ mod tests { vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()] } - fn create_validateer_set_from_publics(authorities: Vec) -> Vec { - authorities.iter().map(|a| validateer(a.clone().into())).collect() + fn create_validateer_set_from_publics(authorities: Vec) -> Vec { + authorities.iter().map(|a| AccountId::from(a.clone())).collect() } fn onchain_mock( diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs b/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs index 447aa18c62..be23004cf0 100644 --- a/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs +++ b/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs @@ -87,10 +87,10 @@ fn test_fixtures( ) -> (TestBlockImporter, Arc, Arc) { let state_handler = Arc::new(HandleStateMock::from_shard(shard()).unwrap()); let top_pool_author = Arc::new(TestTopPoolAuthor::default()); - let ocall_api = Arc::new(OnchainMock::default().add_validateer_set( - parentchain_header, - Some(vec![validateer(Keyring::Alice.public().into())]), - )); + let ocall_api = Arc::new( + OnchainMock::default() + .add_validateer_set(parentchain_header, Some(vec![Keyring::Alice.public().into()])), + ); let state_key_repository = Arc::new(TestStateKeyRepo::new(state_key())); let peer_updater_mock = Arc::new(PeerUpdaterMock {}); diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs b/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs index 54d47324fa..de85405549 100644 --- a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs +++ b/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs @@ -23,5 +23,5 @@ use std::time::Duration; pub const SLOT_DURATION: Duration = Duration::from_millis(300); pub fn validateer(account: AccountId) -> Enclave { - Enclave::new(account, Default::default(), Default::default(), Default::default()) + Enclave::default() } diff --git a/bitacross-worker/sidechain/consensus/aura/src/verifier.rs b/bitacross-worker/sidechain/consensus/aura/src/verifier.rs index 0c1f64b138..121cef58eb 100644 --- a/bitacross-worker/sidechain/consensus/aura/src/verifier.rs +++ b/bitacross-worker/sidechain/consensus/aura/src/verifier.rs @@ -24,6 +24,7 @@ use its_primitives::{ types::block::BlockHash, }; use its_validateer_fetch::ValidateerFetch; +use sp_core::crypto::UncheckedFrom; use sp_runtime::{app_crypto::Pair, traits::Block as ParentchainBlockTrait}; use std::{fmt::Debug, time::Duration}; @@ -57,7 +58,7 @@ impl for AuraVerifier where AuthorityPair: Pair, - AuthorityPair::Public: Debug, + AuthorityPair::Public: Debug + UncheckedFrom<[u8; 32]>, // todo: Relax hash trait bound, but this needs a change to some other parts in the code. ParentchainBlock: ParentchainBlockTrait, SignedSidechainBlock: SignedSidechainBlockTrait + 'static, diff --git a/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs b/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs index be93feb51c..ee2b5f3ede 100644 --- a/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs +++ b/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs @@ -20,7 +20,7 @@ use itc_parentchain_light_client::{ concurrent_access::ValidatorAccess, BlockNumberOps, ExtrinsicSender, NumberFor, }; use itp_extrinsics_factory::CreateExtrinsics; -use itp_node_api_metadata::{pallet_sidechain::SidechainCallIndexes, NodeMetadataTrait}; +use itp_node_api_metadata::{pallet_teebag::TeebagCallIndexes, NodeMetadataTrait}; use itp_node_api_metadata_provider::AccessNodeMetadata; use itp_settings::worker::BLOCK_NUMBER_FINALIZATION_DIFF; use itp_types::{OpaqueCall, ShardIdentifier}; @@ -102,7 +102,7 @@ impl< fn confirm_import(&self, header: &SidechainHeader, shard: &ShardIdentifier) -> Result<()> { let call = self .metadata_repository - .get_from_metadata(|m| m.confirm_imported_sidechain_block_indexes()) + .get_from_metadata(|m| m.sidechain_block_imported_call_indexes()) .map_err(|e| Error::Other(e.into()))? .map_err(|e| Error::Other(format!("{:?}", e).into()))?; diff --git a/bitacross-worker/sidechain/consensus/slots/src/lib.rs b/bitacross-worker/sidechain/consensus/slots/src/lib.rs index 14ed2b6d21..370abc7702 100644 --- a/bitacross-worker/sidechain/consensus/slots/src/lib.rs +++ b/bitacross-worker/sidechain/consensus/slots/src/lib.rs @@ -195,9 +195,6 @@ pub trait SimpleSlotWorker { /// State handler context for authoring type StateHandler: HandleState; - /// The logging target to use when logging messages. - fn logging_target(&self) -> &'static str; - /// Get scheduled enclave fn get_scheduled_enclave(&mut self) -> Arc; @@ -209,7 +206,6 @@ pub trait SimpleSlotWorker { fn epoch_data( &self, header: &ParentchainBlock::Header, - shard: ShardIdentifierFor, slot: Slot, ) -> Result; @@ -281,15 +277,10 @@ pub trait SimpleSlotWorker { is_single_worker: bool, ) -> Option> { let (_timestamp, slot) = (slot_info.timestamp, slot_info.slot); - let logging_target = self.logging_target(); - let remaining_duration = self.proposing_remaining_duration(&slot_info); if remaining_duration == Duration::default() { - debug!( - target: logging_target, - "Skipping proposal slot {} since there's no time left to propose", *slot, - ); + debug!("Skipping proposal slot {} since there's no time left to propose", *slot,); return None } @@ -299,15 +290,11 @@ pub trait SimpleSlotWorker { Ok(Some(peeked_header)) => peeked_header, Ok(None) => slot_info.last_imported_integritee_parentchain_head.clone(), Err(e) => { - warn!( - target: logging_target, - "Failed to peek latest Integritee parentchain block header: {:?}", e - ); + warn!("Failed to peek latest Integritee parentchain block header: {:?}", e); return None }, }; trace!( - target: logging_target, "on_slot: a priori latest Integritee block number: {:?}", latest_integritee_parentchain_header.number() ); @@ -317,15 +304,11 @@ pub trait SimpleSlotWorker { Ok(Some(peeked_header)) => Some(peeked_header), Ok(None) => slot_info.maybe_last_imported_target_a_parentchain_head.clone(), Err(e) => { - debug!( - target: logging_target, - "Failed to peek latest target_a_parentchain block header: {:?}", e - ); + debug!("Failed to peek latest target_a_parentchain block header: {:?}", e); None }, }; trace!( - target: logging_target, "on_slot: a priori latest TargetA block number: {:?}", maybe_latest_target_a_parentchain_header.clone().map(|h| *h.number()) ); @@ -335,24 +318,19 @@ pub trait SimpleSlotWorker { Ok(Some(peeked_header)) => Some(peeked_header), Ok(None) => slot_info.maybe_last_imported_target_b_parentchain_head.clone(), Err(e) => { - debug!( - target: logging_target, - "Failed to peek latest target_a_parentchain block header: {:?}", e - ); + debug!("Failed to peek latest target_a_parentchain block header: {:?}", e); None }, }; trace!( - target: logging_target, "on_slot: a priori latest TargetB block number: {:?}", maybe_latest_target_b_parentchain_header.clone().map(|h| *h.number()) ); - let epoch_data = match self.epoch_data(&latest_integritee_parentchain_header, shard, slot) { + let epoch_data = match self.epoch_data(&latest_integritee_parentchain_header, slot) { Ok(epoch_data) => epoch_data, Err(e) => { warn!( - target: logging_target, "Unable to fetch epoch data at block {:?}: {:?}", latest_integritee_parentchain_header.hash(), e, @@ -365,10 +343,7 @@ pub trait SimpleSlotWorker { let authorities_len = self.authorities_len(&epoch_data); if !authorities_len.map(|a| a > 0).unwrap_or(false) { - debug!( - target: logging_target, - "Skipping proposal slot. Authorities len {:?}", authorities_len - ); + debug!("Skipping proposal slot. Authorities len {:?}", authorities_len); } // Return early if MRENCLAVE doesn't match - it implies that the enclave should be updated @@ -380,7 +355,6 @@ pub trait SimpleSlotWorker { if !scheduled_enclave.is_mrenclave_matching(next_sidechain_number) { warn!( - target: logging_target, "Skipping sidechain block {} due to mismatch MRENCLAVE, current: {:?}, expect: {:?}", next_sidechain_number, scheduled_enclave.get_current_mrenclave().map(hex::encode), @@ -417,7 +391,6 @@ pub trait SimpleSlotWorker { Ok(h) => h, Err(e) => { debug!( - target: logging_target, "Failed to import Integritee blocks until nr{:?}: {:?}", latest_integritee_parentchain_header.number(), e @@ -426,7 +399,6 @@ pub trait SimpleSlotWorker { }, }; trace!( - target: logging_target, "on_slot: a posteriori latest Integritee block number: {:?}", last_imported_integritee_header.clone().map(|h| *h.number()) ); @@ -438,7 +410,6 @@ pub trait SimpleSlotWorker { Ok(None) => None, Err(e) => { debug!( - target: logging_target, "Failed to import TargetA blocks until nr{:?}: {:?}", header.number(), e @@ -450,7 +421,6 @@ pub trait SimpleSlotWorker { None }; trace!( - target: logging_target, "on_slot: a posteriori latest TargetA block number: {:?}", maybe_last_imported_target_a_header.map(|h| *h.number()) ); @@ -462,7 +432,6 @@ pub trait SimpleSlotWorker { Ok(None) => None, Err(e) => { debug!( - target: logging_target, "Failed to import TargetB blocks until nr{:?}: {:?}", header.number(), e @@ -475,7 +444,6 @@ pub trait SimpleSlotWorker { }; trace!( - target: logging_target, "on_slot: a posteriori latest TargetB block number: {:?}", maybe_last_imported_target_b_header.map(|h| *h.number()) ); @@ -483,7 +451,7 @@ pub trait SimpleSlotWorker { let proposer = match self.proposer(latest_integritee_parentchain_header.clone(), shard) { Ok(p) => p, Err(e) => { - warn!(target: logging_target, "Could not create proposer: {:?}", e); + warn!("Could not create proposer: {:?}", e); return None }, }; @@ -491,7 +459,7 @@ pub trait SimpleSlotWorker { let proposing = match proposer.propose(remaining_duration) { Ok(p) => p, Err(e) => { - warn!(target: logging_target, "Could not propose: {:?}", e); + warn!("Could not propose: {:?}", e); return None }, }; @@ -500,7 +468,6 @@ pub trait SimpleSlotWorker { warn!("Running as single worker, skipping timestamp within slot check") } else if !timestamp_within_slot(&slot_info, &proposing.block) { warn!( - target: logging_target, "⌛️ Discarding proposal for slot {}, block number {}; block production took too long", *slot, proposing.block.block().header().block_number(), ); @@ -558,8 +525,6 @@ impl, is_single_worker: bool, ) -> Self::Output { - let logging_target = SimpleSlotWorker::logging_target(self); - let mut remaining_shards = shards.len(); let mut slot_results = Vec::with_capacity(remaining_shards); @@ -572,10 +537,7 @@ impl { slot_results.push(res); debug!( - target: logging_target, - "on_slot: produced block for slot: {:?} in shard {:?}", shard_slot, shard + "on_slot: produced block for slot: {:?} in shard {:?}", + shard_slot, shard ) }, None => info!( - target: logging_target, - "Did not propose a block for slot {} in shard {:?}", *slot_info.slot, shard + "Did not propose a block for slot {} in shard {:?}", + *slot_info.slot, shard ), } diff --git a/bitacross-worker/sidechain/consensus/slots/src/mocks.rs b/bitacross-worker/sidechain/consensus/slots/src/mocks.rs index 409fb41987..72d6f9fee8 100644 --- a/bitacross-worker/sidechain/consensus/slots/src/mocks.rs +++ b/bitacross-worker/sidechain/consensus/slots/src/mocks.rs @@ -62,10 +62,6 @@ where type StateHandler = HandleStateMock; - fn logging_target(&self) -> &'static str { - "test" - } - fn get_scheduled_enclave(&mut self) -> Arc { todo!() } @@ -74,12 +70,7 @@ where todo!() } - fn epoch_data( - &self, - _header: &B::Header, - _shard: ShardIdentifierFor, - _slot: Slot, - ) -> Result { + fn epoch_data(&self, _header: &B::Header, _slot: Slot) -> Result { todo!() } diff --git a/bitacross-worker/sidechain/peer-fetch/Cargo.toml b/bitacross-worker/sidechain/peer-fetch/Cargo.toml index 63e2612d91..66c10302c9 100644 --- a/bitacross-worker/sidechain/peer-fetch/Cargo.toml +++ b/bitacross-worker/sidechain/peer-fetch/Cargo.toml @@ -16,6 +16,7 @@ thiserror = { version = "1.0" } # local itc-rpc-client = { path = "../../core/rpc-client" } itp-node-api = { path = "../../core-primitives/node-api" } +itp-types = { path = "../../core-primitives/types" } its-primitives = { path = "../primitives" } its-rpc-handler = { path = "../rpc-handler" } its-storage = { path = "../storage" } diff --git a/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs b/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs index 7ff9434103..b87b0ca5c1 100644 --- a/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs +++ b/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs @@ -17,7 +17,8 @@ use crate::error::{Error, Result}; use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; -use itp_node_api::{api_client::PalletTeerexApi, node_api_factory::CreateNodeApi}; +use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; +use itp_types::WorkerType; use its_primitives::types::ShardIdentifier; use std::sync::Arc; @@ -50,10 +51,11 @@ where let node_api = self.node_api_factory.create_api()?; let validateer = node_api - .worker_for_shard(shard, None)? + .primary_enclave_for_shard(WorkerType::BitAcross, shard, None)? .ok_or_else(|| Error::NoPeerFoundForShard(*shard))?; - let trusted_worker_client = DirectWorkerApi::new(validateer.url); + let trusted_worker_client = + DirectWorkerApi::new(String::from_utf8_lossy(validateer.url.as_slice()).to_string()); Ok(trusted_worker_client.get_untrusted_worker_url()?) } } diff --git a/bitacross-worker/sidechain/validateer-fetch/Cargo.toml b/bitacross-worker/sidechain/validateer-fetch/Cargo.toml index 7aca2dbf7a..2df88400d5 100644 --- a/bitacross-worker/sidechain/validateer-fetch/Cargo.toml +++ b/bitacross-worker/sidechain/validateer-fetch/Cargo.toml @@ -15,11 +15,8 @@ sp-std = { default-features = false, git = "https://github.com/paritytech/substr # local deps itp-ocall-api = { path = "../../core-primitives/ocall-api", default-features = false } -itp-teerex-storage = { path = "../../core-primitives/teerex-storage", default-features = false } itp-types = { path = "../../core-primitives/types", default-features = false } - -# litentry -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } +lc-teebag-storage = { path = "../../litentry/core/teebag-storage", default-features = false } [features] default = ["std"] @@ -30,7 +27,7 @@ std = [ "sp-std/std", "itp-types/std", "itp-ocall-api/std", - "frame-support/std", + "lc-teebag-storage/std", ] [dev-dependencies] diff --git a/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs b/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs index 4af8d86274..e61c7ab5ee 100644 --- a/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs +++ b/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs @@ -16,10 +16,9 @@ */ use crate::error::{Error, Result}; -use frame_support::ensure; use itp_ocall_api::EnclaveOnChainOCallApi; -use itp_teerex_storage::{TeeRexStorage, TeerexStorageKeys}; -use itp_types::{parentchain::ParentchainId, Enclave}; +use itp_types::{parentchain::ParentchainId, AccountId, WorkerType}; +use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; use sp_core::H256; use sp_runtime::traits::Header as HeaderT; use sp_std::prelude::Vec; @@ -28,7 +27,8 @@ pub trait ValidateerFetch { fn current_validateers>( &self, latest_header: &Header, - ) -> Result>; + ) -> Result>; + fn validateer_count>(&self, latest_header: &Header) -> Result; } @@ -37,31 +37,21 @@ impl ValidateerFetch for OnchainStorage fn current_validateers>( &self, header: &Header, - ) -> Result> { - let count = self.validateer_count(header)?; - - let mut hashes = Vec::with_capacity(count as usize); - for i in 1..=count { - hashes.push(TeeRexStorage::enclave(i)) - } - - let enclaves: Vec = self - .get_multiple_storages_verified(hashes, header, &ParentchainId::Litentry)? - .into_iter() - .filter_map(|e| e.into_tuple().1) - .collect(); - ensure!( - enclaves.len() == count as usize, - Error::Other("Found less validateers onchain than validateer count") - ); - Ok(enclaves) + ) -> Result> { + let identifiers = self + .get_storage_verified( + TeebagStorage::enclave_identifier(WorkerType::BitAcross), + header, + &ParentchainId::Litentry, + )? + .into_tuple() + .1 + .ok_or_else(|| Error::Other("Could not get validateer list from chain"))?; + Ok(identifiers) } fn validateer_count>(&self, header: &Header) -> Result { - self.get_storage_verified(TeeRexStorage::enclave_count(), header, &ParentchainId::Litentry)? - .into_tuple() - .1 - .ok_or_else(|| Error::Other("Could not get validateer count from chain")) + Ok(self.current_validateers::

(header)?.len() as u64) } } @@ -84,21 +74,7 @@ mod tests { pub fn get_validateer_set_works() { let header = ParentchainHeaderBuilder::default().build(); let mock = OnchainMock::default().add_validateer_set(&header, None); - let validateers = validateer_set(); - assert_eq!(mock.current_validateers(&header).unwrap(), validateers); } - - #[test] - pub fn if_validateer_count_bigger_than_returned_validateers_return_err() { - let header = ParentchainHeaderBuilder::default().build(); - let mut mock = OnchainMock::default().add_validateer_set(&header, None); - mock.insert_at_header(&header, TeeRexStorage::enclave_count(), 5u64.encode()); - - assert_eq!( - mock.current_validateers(&header).unwrap_err().to_string(), - "Found less validateers onchain than validateer count".to_string() - ); - } } From 551968296a1cf311ed418e6e33bdcfef3eb701d7 Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Mon, 12 Feb 2024 20:40:58 +0530 Subject: [PATCH 27/64] fix: add block numbers and assertion text (#2481) --- .../core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs index 4e3f8480f2..cf6485dd39 100644 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs +++ b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs @@ -194,9 +194,14 @@ where ) })?; + credential.parachain_block_number = self.req.parachain_block_number; + credential.sidechain_block_number = self.req.sidechain_block_number; + credential.credential_subject.endpoint = self.context.data_provider_config.credential_endpoint.to_string(); + credential.credential_subject.assertion_text = format!("{:?}", self.req.assertion); + credential.issuer.id = Identity::Substrate(enclave_account.into()).to_did().map_err(|e| { VCMPError::RequestVCFailed( From 2936b393a20a8b9f3229082c298a2e4e9a3abc69 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Tue, 13 Feb 2024 10:26:42 +0200 Subject: [PATCH 28/64] Add request-vc-direct ts-test (#2480) * Add request_vc_direct ts-test --- .../integration-tests/common/di-utils.ts | 25 +++++------ .../common/utils/assertion.ts | 41 ------------------- .../ts-tests/integration-tests/di_vc.test.ts | 40 ++++++++++++++++++ 3 files changed, 50 insertions(+), 56 deletions(-) diff --git a/tee-worker/ts-tests/integration-tests/common/di-utils.ts b/tee-worker/ts-tests/integration-tests/common/di-utils.ts index 4242dfc1c6..af673e5ca8 100644 --- a/tee-worker/ts-tests/integration-tests/common/di-utils.ts +++ b/tee-worker/ts-tests/integration-tests/common/di-utils.ts @@ -94,20 +94,13 @@ export const createSignedTrustedCall = async ( payload = u8aConcat(stringToU8a(''), payload, stringToU8a('')); } - let signature; - // for bitcoin signature, we expect a hex-encoded `string` without `0x` prefix - // TODO: any better idiomatic way? - if (signer.type() === 'bitcoin') { - const payloadStr = u8aToHex(payload).substring(2); - signature = parachainApi.createType('LitentryMultiSignature', { - [signer.type()]: u8aToHex(await signer.sign(payloadStr)), - }); - } else { - signature = parachainApi.createType('LitentryMultiSignature', { - [signer.type()]: u8aToHex(await signer.sign(payload)), - }); - } + const signature = parachainApi.createType('LitentryMultiSignature', { + [signer.type()]: u8aToHex( + await signer.sign(signer.type() === 'bitcoin' ? u8aToHex(payload).substring(2) : payload) + ), + }); + return parachainApi.createType('TrustedCallSigned', { call: call, index: nonce, @@ -221,6 +214,7 @@ export async function createSignedTrustedCallRequestVc( [primeIdentity.toHuman(), primeIdentity.toHuman(), assertion, aesKey, hash] ); } + export async function createSignedTrustedCallDeactivateIdentity( parachainApi: ApiPromise, mrenclave: string, @@ -305,7 +299,8 @@ export const getIdGraphHash = async ( export const sendRequestFromTrustedCall = async ( context: IntegrationTestContext, teeShieldingKey: KeyObject, - call: TrustedCallSigned + call: TrustedCallSigned, + isVcDirect = false ) => { // construct trusted operation const trustedOperation = context.api.createType('TrustedOperation', { direct_call: call }); @@ -320,7 +315,7 @@ export const sendRequestFromTrustedCall = async ( trustedOperation.toU8a() ); const request = createJsonRpcRequest( - 'author_submitAndWatchAesRequest', + isVcDirect ? 'author_requestVc' : 'author_submitAndWatchAesRequest', [u8aToHex(requestParam)], nextRequestId(context) ); diff --git a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts index 7a28ebf5a4..7062d0503e 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts @@ -134,47 +134,6 @@ export async function checkErrorDetail(events: Event[], expectedDetail: string) }); } -export async function verifySignature(data: any, index: HexString, proofJson: any, api: ApiPromise) { - const enclaveIdentifier = api.createType('Vec', await api.query.teebag.enclaveIdentifier('Identity')); - const primaryEnclave = ( - await api.query.teebag.enclaveRegistry(enclaveIdentifier[0]) - ).toHuman() as unknown as PalletTeebagEnclave; - - // Check vc index - expect(index).to.be.eq(data.id); - const signature = Buffer.from(hexToU8a(`0x${proofJson.proofValue}`)); - const message = Buffer.from(data.issuer.mrenclave); - const vcPubkeyBytes = api.createType('Option', primaryEnclave.vcPubkey).unwrap(); - const vcPubkey = Buffer.from(hexToU8a(vcPubkeyBytes.toHex())); - - const isValid = await ed.verify(signature, message, vcPubkey); - - expect(isValid).to.be.true; - return true; -} - -export async function checkVc(vcObj: any, index: HexString, proof: any, api: ApiPromise): Promise { - const vc = JSON.parse(JSON.stringify(vcObj)); - delete vc.proof; - const signatureValid = await verifySignature(vc, index, proof, api); - expect(signatureValid).to.be.true; - - const jsonValid = await checkJson(vcObj, proof); - expect(jsonValid).to.be.true; - return true; -} - -// Check VC json fields -export async function checkJson(vc: any, proofJson: any): Promise { - //check jsonSchema - const ajv = new Ajv(); - const validate = ajv.compile(jsonSchema); - const isValid = validate(vc); - expect(isValid).to.be.true; - expect(vc.type[0] === 'VerifiableCredential' && proofJson.type === 'Ed25519Signature2020').to.be.true; - return true; -} - // for IdGraph mutation, assert the corresponding event is emitted for the given signer and the id_graph_hash matches export async function assertIdGraphMutationEvent( context: IntegrationTestContext, diff --git a/tee-worker/ts-tests/integration-tests/di_vc.test.ts b/tee-worker/ts-tests/integration-tests/di_vc.test.ts index 9403bafbd8..36e5575ada 100644 --- a/tee-worker/ts-tests/integration-tests/di_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_vc.test.ts @@ -141,6 +141,7 @@ describe('Test Vc (direct invocation)', function () { } }); + // Testing VCs with request_vc call - might be obsolete in future due to replacement with request_vc_direct defaultAssertions.forEach(({ description, assertion }) => { step(`request vc ${Object.keys(assertion)[0]} (alice)`, async function () { let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); @@ -176,6 +177,45 @@ describe('Test Vc (direct invocation)', function () { await assertVc(context, aliceSubstrateIdentity, res.value); }); }); + + // Testing VCs with request_vc_direct call + defaultAssertions.forEach(({ description, assertion }) => { + step(`request vc direct ${Object.keys(assertion)[0]} (alice)`, async function () { + let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); + const getNextNonce = () => currentNonce++; + const nonce = getNextNonce(); + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + console.log( + `request vc direct ${Object.keys(assertion)[0]} for Alice ... Assertion description: ${description}` + ); + const eventsPromise = subscribeToEventsWithExtHash(requestIdentifier, context); + + const requestVcCall = await createSignedTrustedCallRequestVc( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + new PolkadotSigner(context.substrateWallet.alice), + aliceSubstrateIdentity, + context.api.createType('Assertion', assertion).toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier + ); + + const isVcDirect = true; + const res = await sendRequestFromTrustedCall(context, teeShieldingKey, requestVcCall, isVcDirect); + const events = await eventsPromise; + const vcIssuedEvents = events + .map(({ event }) => event) + .filter(({ section, method }) => section === 'vcManagement' && method === 'VCIssued'); + + assert.equal( + vcIssuedEvents.length, + 1, + `vcIssuedEvents.length != 1, please check the ${Object.keys(assertion)[0]} call` + ); + await assertVc(context, aliceSubstrateIdentity, res.value); + }); + }); unconfiguredAssertions.forEach(({ description, assertion }) => { it(`request vc ${Object.keys(assertion)[0]} (alice)`, async function () { let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); From ee67522239cb83705b0a19aaa69e85ec175166d4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 13 Feb 2024 10:10:49 +0100 Subject: [PATCH 29/64] Bump chrono from 0.4.33 to 0.4.34 (#2484) Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.33 to 0.4.34. - [Release notes](https://github.com/chronotope/chrono/releases) - [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md) - [Commits](https://github.com/chronotope/chrono/compare/v0.4.33...v0.4.34) --- updated-dependencies: - dependency-name: chrono dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 44bdec2c59..fb4cb29b08 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -961,9 +961,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.33" +version = "0.4.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f13690e35a5e4ace198e7beea2895d29f3a9cc55015fcebe6336bd2010af9eb" +checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" dependencies = [ "android-tzdata", "iana-time-zone", From 8df865018e762849f2988bf999e406ef36338152 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Tue, 13 Feb 2024 19:25:24 +0200 Subject: [PATCH 30/64] Refactor the way we run ts tests (#2487) * Refactor way we run ts tests --- .github/workflows/ci.yml | 1 - tee-worker/cli/lit_ts_integration_test.sh | 2 +- tee-worker/cli/lit_ts_worker_test.sh | 2 +- tee-worker/docker/lit-data-provider-test.yml | 2 +- .../docker/lit-di-bitcoin-identity-test.yml | 2 +- .../lit-di-evm-identity-multiworker-test.yml | 2 +- .../docker/lit-di-evm-identity-test.yml | 2 +- ...di-substrate-identity-multiworker-test.yml | 2 +- .../docker/lit-di-substrate-identity-test.yml | 2 +- .../docker/lit-di-vc-multiworker-test.yml | 2 +- tee-worker/docker/lit-di-vc-test.yml | 2 +- .../docker/lit-ii-batch-test-multiworker.yml | 2 +- tee-worker/docker/lit-ii-batch-test.yml | 2 +- .../lit-ii-identity-multiworker-test.yml | 2 +- tee-worker/docker/lit-ii-identity-test.yml | 2 +- .../docker/lit-ii-vc-multiworker-test.yml | 2 +- tee-worker/docker/lit-ii-vc-test.yml | 2 +- tee-worker/docker/lit-resume-worker.yml | 4 ++-- tee-worker/ts-tests/README.md | 16 +++++++++----- .../integration-tests/common/config.js | 2 +- .../ts-tests/integration-tests/package.json | 22 ++++--------------- tee-worker/ts-tests/stress/package.json | 9 ++++---- tee-worker/ts-tests/worker/package.json | 7 +++--- 23 files changed, 40 insertions(+), 53 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9eb17c948f..d463da5369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -701,7 +701,6 @@ jobs: - test_name: lit-di-vc-test - test_name: lit-parentchain-nonce - test_name: lit-ii-batch-test - - test_name: lit-test-stress-script steps: - uses: actions/checkout@v4 diff --git a/tee-worker/cli/lit_ts_integration_test.sh b/tee-worker/cli/lit_ts_integration_test.sh index 4549d32048..eee140cbf5 100755 --- a/tee-worker/cli/lit_ts_integration_test.sh +++ b/tee-worker/cli/lit_ts_integration_test.sh @@ -68,4 +68,4 @@ pnpm run build cd /ts-tests pnpm install -pnpm --filter integration-tests run $TEST:staging +NODE_ENV=staging pnpm --filter integration-tests run $TEST diff --git a/tee-worker/cli/lit_ts_worker_test.sh b/tee-worker/cli/lit_ts_worker_test.sh index a4983a99ae..ed96e23518 100755 --- a/tee-worker/cli/lit_ts_worker_test.sh +++ b/tee-worker/cli/lit_ts_worker_test.sh @@ -22,4 +22,4 @@ echo "Using node endpoint: $NODE_ENDPOINT" cd /ts-tests pnpm install -pnpm --filter worker run $TEST:staging +NODE_ENV=staging pnpm --filter worker run $TEST diff --git a/tee-worker/docker/lit-data-provider-test.yml b/tee-worker/docker/lit-data-provider-test.yml index 2af04c8784..469f617193 100644 --- a/tee-worker/docker/lit-data-provider-test.yml +++ b/tee-worker/docker/lit-data-provider-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-data-provider 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh data-provider.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-bitcoin-identity-test.yml b/tee-worker/docker/lit-di-bitcoin-identity-test.yml index 9cc7e41c35..22a1e0473c 100644 --- a/tee-worker/docker/lit-di-bitcoin-identity-test.yml +++ b/tee-worker/docker/lit-di-bitcoin-identity-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-bitcoin-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_bitcoin_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-evm-identity-multiworker-test.yml b/tee-worker/docker/lit-di-evm-identity-multiworker-test.yml index 4649e93fdb..f5bb805edc 100644 --- a/tee-worker/docker/lit-di-evm-identity-multiworker-test.yml +++ b/tee-worker/docker/lit-di-evm-identity-multiworker-test.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-evm-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_evm_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-evm-identity-test.yml b/tee-worker/docker/lit-di-evm-identity-test.yml index 7938ca67a8..b0c0152f79 100644 --- a/tee-worker/docker/lit-di-evm-identity-test.yml +++ b/tee-worker/docker/lit-di-evm-identity-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-evm-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_evm_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-substrate-identity-multiworker-test.yml b/tee-worker/docker/lit-di-substrate-identity-multiworker-test.yml index f5647624b9..e0bb7320b6 100644 --- a/tee-worker/docker/lit-di-substrate-identity-multiworker-test.yml +++ b/tee-worker/docker/lit-di-substrate-identity-multiworker-test.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-substrate-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_substrate_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-substrate-identity-test.yml b/tee-worker/docker/lit-di-substrate-identity-test.yml index 3f1c8250c7..9c194728d8 100644 --- a/tee-worker/docker/lit-di-substrate-identity-test.yml +++ b/tee-worker/docker/lit-di-substrate-identity-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-substrate-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_substrate_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-vc-multiworker-test.yml b/tee-worker/docker/lit-di-vc-multiworker-test.yml index 08bb708212..fc0eebca3b 100644 --- a/tee-worker/docker/lit-di-vc-multiworker-test.yml +++ b/tee-worker/docker/lit-di-vc-multiworker-test.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-vc 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_vc.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-di-vc-test.yml b/tee-worker/docker/lit-di-vc-test.yml index a41ebfc457..532e856b40 100644 --- a/tee-worker/docker/lit-di-vc-test.yml +++ b/tee-worker/docker/lit-di-vc-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-di-vc 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh di_vc.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-batch-test-multiworker.yml b/tee-worker/docker/lit-ii-batch-test-multiworker.yml index 6d26ecea5d..7b6ce161db 100644 --- a/tee-worker/docker/lit-ii-batch-test-multiworker.yml +++ b/tee-worker/docker/lit-ii-batch-test-multiworker.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-batch 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_batch.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-batch-test.yml b/tee-worker/docker/lit-ii-batch-test.yml index f0a6ee9fbf..cb8225857b 100644 --- a/tee-worker/docker/lit-ii-batch-test.yml +++ b/tee-worker/docker/lit-ii-batch-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-batch 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_batch.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-identity-multiworker-test.yml b/tee-worker/docker/lit-ii-identity-multiworker-test.yml index 684f832367..bdcb0d9e28 100644 --- a/tee-worker/docker/lit-ii-identity-multiworker-test.yml +++ b/tee-worker/docker/lit-ii-identity-multiworker-test.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-identity-test.yml b/tee-worker/docker/lit-ii-identity-test.yml index 874b3dad67..66055f0280 100644 --- a/tee-worker/docker/lit-ii-identity-test.yml +++ b/tee-worker/docker/lit-ii-identity-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-identity 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_identity.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-vc-multiworker-test.yml b/tee-worker/docker/lit-ii-vc-multiworker-test.yml index f84a6e52a2..60a3ee7fcf 100644 --- a/tee-worker/docker/lit-ii-vc-multiworker-test.yml +++ b/tee-worker/docker/lit-ii-vc-multiworker-test.yml @@ -21,7 +21,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-vc 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_vc.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-ii-vc-test.yml b/tee-worker/docker/lit-ii-vc-test.yml index f530e499f8..d72f80d719 100644 --- a/tee-worker/docker/lit-ii-vc-test.yml +++ b/tee-worker/docker/lit-ii-vc-test.yml @@ -17,7 +17,7 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh test-ii-vc 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh ii_vc.test.ts 2>&1' " restart: "no" networks: litentry-test-network: diff --git a/tee-worker/docker/lit-resume-worker.yml b/tee-worker/docker/lit-resume-worker.yml index 319d426d2f..2ee8532ab2 100644 --- a/tee-worker/docker/lit-resume-worker.yml +++ b/tee-worker/docker/lit-resume-worker.yml @@ -15,8 +15,8 @@ services: condition: service_healthy networks: - litentry-test-network - entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_worker_test.sh test-resuming-worker 2>&1' " + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_worker_test.sh resuming_worker.test.ts 2>&1' " restart: "no" networks: litentry-test-network: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/tee-worker/ts-tests/README.md b/tee-worker/ts-tests/README.md index e989771f35..bb69030767 100644 --- a/tee-worker/ts-tests/README.md +++ b/tee-worker/ts-tests/README.md @@ -29,14 +29,18 @@ pnpm install ## Usage(ts-tests folder) -II identity test: `pnpm --filter integration-tests run test-ii-identity:local` +``` +pnpm --filter integration-tests run test your-testfile.test.ts +``` + +II identity test: `pnpm --filter integration-tests run test ii_identity.test.ts` -II vc test: `pnpm --filter integration-tests run test-ii-vc:local` +II vc test: `pnpm --filter integration-tests run test ii_vc.test.ts` -II batch identity test: `pnpm --filter integration-tests run test-ii-batch:local` +II batch identity test: `pnpm --filter integration-tests run test ii_batch.test.ts` -Direct invocation substrate identity test: `pnpm --filter integration-tests run test-di-substrate-identity:local` +Direct invocation substrate identity test: `pnpm --filter integration-tests run test di_substrate_identity.test.ts` -Direct invocation evm identity test: `pnpm --filter integration-tests run test-di-evm-identity:local` +Direct invocation evm identity test: `pnpm --filter integration-tests run test di_evm_identity.test.ts` -Direct invocation vc test: `pnpm --filter integration-tests run test-di-vc:local` \ No newline at end of file +Direct invocation vc test: `pnpm --filter integration-tests run test di_vc.test.ts` diff --git a/tee-worker/ts-tests/integration-tests/common/config.js b/tee-worker/ts-tests/integration-tests/common/config.js index 3226f64181..3dbb229aa4 100644 --- a/tee-worker/ts-tests/integration-tests/common/config.js +++ b/tee-worker/ts-tests/integration-tests/common/config.js @@ -1,4 +1,4 @@ import dotenv from 'dotenv'; // eslint-disable-next-line @typescript-eslint/no-var-requires, no-undef -dotenv.config({ path: `.env.${process.env.NODE_ENV}` }); +dotenv.config({ path: `.env.${process.env.NODE_ENV || 'local'}` }); diff --git a/tee-worker/ts-tests/integration-tests/package.json b/tee-worker/ts-tests/integration-tests/package.json index 991658cd0f..0bf78b86df 100644 --- a/tee-worker/ts-tests/integration-tests/package.json +++ b/tee-worker/ts-tests/integration-tests/package.json @@ -3,24 +3,10 @@ "license": "ISC", "type": "module", "scripts": { - "check-format": "pnpm exec prettier --check .", - "format": "pnpm exec prettier --write .", - "test-ii-identity:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_identity.test.ts'", - "test-ii-identity:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm --loader=ts-node/esm 'ii_identity.test.ts'", - "test-di-substrate-identity:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_substrate_identity.test.ts'", - "test-di-substrate-identity:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_substrate_identity.test.ts'", - "test-di-evm-identity:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_evm_identity.test.ts'", - "test-di-evm-identity:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_evm_identity.test.ts'", - "test-di-bitcoin-identity:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_bitcoin_identity.test.ts'", - "test-di-bitcoin-identity:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_bitcoin_identity.test.ts'", - "test-di-vc:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_vc.test.ts'", - "test-di-vc:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'di_vc.test.ts'", - "test-ii-vc:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_vc.test.ts'", - "test-ii-vc:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_vc.test.ts'", - "test-ii-batch:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_batch.test.ts'", - "test-ii-batch:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'ii_batch.test.ts'", - "test-data-provider:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-provider.test.ts'", - "test-data-provider:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'data-provider.test.ts'" + "check-format": "prettier --check .", + "format": "prettier --write .", + "pretest": "eslint .", + "test": "mocha --exit --sort -r ts-node/register --loader=ts-node/esm" }, "dependencies": { "@noble/ed25519": "^1.7.3", diff --git a/tee-worker/ts-tests/stress/package.json b/tee-worker/ts-tests/stress/package.json index 1173ee80c6..7626051262 100644 --- a/tee-worker/ts-tests/stress/package.json +++ b/tee-worker/ts-tests/stress/package.json @@ -3,11 +3,10 @@ "license": "ISC", "type": "module", "scripts": { - "format": "pnpm exec prettier --write .", - "check-format": "pnpm exec prettier --check .", - "run-script": "NODE_TLS_REJECT_UNAUTHORIZED=0 pnpm exec ts-node bin/stress.ts", - "test-stress-script:local": "pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'test/test.ts'", - "test-stress-script:staging": "pnpm exec cross-env NODE_ENV=staging mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'test/test.ts'" + "format": "prettier --write .", + "check-format": "prettier --check .", + "run-script": "NODE_TLS_REJECT_UNAUTHORIZED=0 ts-node bin/stress.ts", + "test": "mocha --exit --sort -r ts-node/register --loader=ts-node/esm" }, "dependencies": { "@noble/ed25519": "^1.7.3", diff --git a/tee-worker/ts-tests/worker/package.json b/tee-worker/ts-tests/worker/package.json index 2113d20611..03268303cf 100644 --- a/tee-worker/ts-tests/worker/package.json +++ b/tee-worker/ts-tests/worker/package.json @@ -3,10 +3,9 @@ "license": "ISC", "type": "module", "scripts": { - "format": "pnpm exec prettier --write .", - "check-format": "pnpm exec prettier --check .", - "test-resuming-worker:staging": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=staging mocha --trace-warnings --full-trace --exit --sort -r ts-node/register --loader=ts-node/esm 'resuming_worker.test.ts'", - "test-resuming-worker:local": "pnpm exec eslint . && pnpm exec cross-env NODE_ENV=local mocha --exit --sort -r ts-node/register --loader=ts-node/esm 'resuming_worker.test.ts'" + "format": "prettier --write .", + "check-format": "prettier --check .", + "test": "mocha --trace-warnings --full-trace --exit --sort -r ts-node/register --loader=ts-node/esm" }, "dependencies": { "chai": "^4.3.6", From ce56ef0519a2f63d9ff36a408bae29d54d6cebd7 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Wed, 14 Feb 2024 08:59:58 +0200 Subject: [PATCH 31/64] Split direct request and invocation VC tests (#2489) --- .github/workflows/ci.yml | 2 + .../docker/lit-dr-vc-multiworker-test.yml | 28 +++ tee-worker/docker/lit-dr-vc-test.yml | 24 ++ .../ts-tests/integration-tests/di_vc.test.ts | 39 ---- .../ts-tests/integration-tests/dr_vc.test.ts | 206 ++++++++++++++++++ 5 files changed, 260 insertions(+), 39 deletions(-) create mode 100644 tee-worker/docker/lit-dr-vc-multiworker-test.yml create mode 100644 tee-worker/docker/lit-dr-vc-test.yml create mode 100644 tee-worker/ts-tests/integration-tests/dr_vc.test.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d463da5369..0453c4e7fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -699,6 +699,7 @@ jobs: - test_name: lit-di-evm-identity-test - test_name: lit-di-bitcoin-identity-test - test_name: lit-di-vc-test + - test_name: lit-dr-vc-test - test_name: lit-parentchain-nonce - test_name: lit-ii-batch-test steps: @@ -780,6 +781,7 @@ jobs: - test_name: lit-di-evm-identity-multiworker-test - test_name: lit-di-substrate-identity-multiworker-test - test_name: lit-di-vc-multiworker-test + - test_name: lit-dr-vc-multiworker-test - test_name: lit-ii-batch-test-multiworker - test_name: lit-ii-identity-multiworker-test - test_name: lit-ii-vc-multiworker-test diff --git a/tee-worker/docker/lit-dr-vc-multiworker-test.yml b/tee-worker/docker/lit-dr-vc-multiworker-test.yml new file mode 100644 index 0000000000..8bafe6680a --- /dev/null +++ b/tee-worker/docker/lit-dr-vc-multiworker-test.yml @@ -0,0 +1,28 @@ +services: + lit-dr-vc-multiworker-test: + image: litentry/litentry-cli:latest + container_name: litentry-dr-vc-test + volumes: + - ../ts-tests:/ts-tests + - ../client-api:/client-api + - ../cli:/usr/local/worker-cli + build: + context: .. + dockerfile: build.Dockerfile + target: deployed-client + depends_on: + litentry-node: + condition: service_healthy + litentry-worker-1: + condition: service_healthy + litentry-worker-2: + condition: service_healthy + litentry-worker-3: + condition: service_healthy + networks: + - litentry-test-network + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh dr_vc.test.ts 2>&1' " + restart: "no" +networks: + litentry-test-network: + driver: bridge diff --git a/tee-worker/docker/lit-dr-vc-test.yml b/tee-worker/docker/lit-dr-vc-test.yml new file mode 100644 index 0000000000..40624dbb40 --- /dev/null +++ b/tee-worker/docker/lit-dr-vc-test.yml @@ -0,0 +1,24 @@ +services: + lit-dr-vc-test: + image: litentry/litentry-cli:latest + container_name: litentry-dr-vc-test + volumes: + - ../ts-tests:/ts-tests + - ../client-api:/client-api + - ../cli:/usr/local/worker-cli + build: + context: .. + dockerfile: build.Dockerfile + target: deployed-client + depends_on: + litentry-node: + condition: service_healthy + litentry-worker-1: + condition: service_healthy + networks: + - litentry-test-network + entrypoint: "bash -c '/usr/local/worker-cli/lit_ts_integration_test.sh dr_vc.test.ts 2>&1' " + restart: "no" +networks: + litentry-test-network: + driver: bridge diff --git a/tee-worker/ts-tests/integration-tests/di_vc.test.ts b/tee-worker/ts-tests/integration-tests/di_vc.test.ts index 36e5575ada..f0c72d0f49 100644 --- a/tee-worker/ts-tests/integration-tests/di_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_vc.test.ts @@ -141,7 +141,6 @@ describe('Test Vc (direct invocation)', function () { } }); - // Testing VCs with request_vc call - might be obsolete in future due to replacement with request_vc_direct defaultAssertions.forEach(({ description, assertion }) => { step(`request vc ${Object.keys(assertion)[0]} (alice)`, async function () { let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); @@ -178,44 +177,6 @@ describe('Test Vc (direct invocation)', function () { }); }); - // Testing VCs with request_vc_direct call - defaultAssertions.forEach(({ description, assertion }) => { - step(`request vc direct ${Object.keys(assertion)[0]} (alice)`, async function () { - let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); - const getNextNonce = () => currentNonce++; - const nonce = getNextNonce(); - const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; - console.log( - `request vc direct ${Object.keys(assertion)[0]} for Alice ... Assertion description: ${description}` - ); - const eventsPromise = subscribeToEventsWithExtHash(requestIdentifier, context); - - const requestVcCall = await createSignedTrustedCallRequestVc( - context.api, - context.mrEnclave, - context.api.createType('Index', nonce), - new PolkadotSigner(context.substrateWallet.alice), - aliceSubstrateIdentity, - context.api.createType('Assertion', assertion).toHex(), - context.api.createType('Option', aesKey).toHex(), - requestIdentifier - ); - - const isVcDirect = true; - const res = await sendRequestFromTrustedCall(context, teeShieldingKey, requestVcCall, isVcDirect); - const events = await eventsPromise; - const vcIssuedEvents = events - .map(({ event }) => event) - .filter(({ section, method }) => section === 'vcManagement' && method === 'VCIssued'); - - assert.equal( - vcIssuedEvents.length, - 1, - `vcIssuedEvents.length != 1, please check the ${Object.keys(assertion)[0]} call` - ); - await assertVc(context, aliceSubstrateIdentity, res.value); - }); - }); unconfiguredAssertions.forEach(({ description, assertion }) => { it(`request vc ${Object.keys(assertion)[0]} (alice)`, async function () { let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); diff --git a/tee-worker/ts-tests/integration-tests/dr_vc.test.ts b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts new file mode 100644 index 0000000000..2ec385b174 --- /dev/null +++ b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts @@ -0,0 +1,206 @@ +import { randomBytes, KeyObject } from 'crypto'; +import { step } from 'mocha-steps'; +import { assert } from 'chai'; +import { u8aToHex, bufferToU8a } from '@polkadot/util'; +import { buildIdentityFromKeypair, initIntegrationTestContext, PolkadotSigner } from './common/utils'; +import { assertIsInSidechainBlock, assertVc } from './common/utils/assertion'; +import { + getSidechainNonce, + createSignedTrustedCallLinkIdentity, + getTeeShieldingKey, + sendRequestFromTrustedCall, + createSignedTrustedCallRequestVc, +} from './common/di-utils'; // @fixme move to a better place +import { buildIdentityHelper, buildValidations } from './common/utils'; +import type { IntegrationTestContext } from './common/common-types'; +import { aesKey } from './common/call'; +import { CorePrimitivesIdentity } from 'parachain-api'; +import { subscribeToEventsWithExtHash } from './common/transactions'; +import { defaultAssertions, unconfiguredAssertions } from './common/utils/vc-helper'; +import { LitentryValidationData, Web3Network } from 'parachain-api'; +import { Vec } from '@polkadot/types'; + +describe('Test Vc (direct request)', function () { + let context: IntegrationTestContext = undefined as any; + let teeShieldingKey: KeyObject = undefined as any; + let aliceSubstrateIdentity: CorePrimitivesIdentity = undefined as any; + + // Alice links: + // - a `mock_user` twitter + // - alice's evm identity + // - alice's bitcoin identity] + // + // We need this linking to not have empty eligible identities for any vc request + const linkIdentityRequestParams: { + nonce: number; + identity: CorePrimitivesIdentity; + validation: LitentryValidationData; + networks: Vec; + }[] = []; + this.timeout(6000000); + + before(async () => { + context = await initIntegrationTestContext( + process.env.WORKER_ENDPOINT!, // @fixme evil assertion; centralize env access + process.env.NODE_ENDPOINT! // @fixme evil assertion; centralize env access + ); + teeShieldingKey = await getTeeShieldingKey(context); + aliceSubstrateIdentity = await buildIdentityFromKeypair( + new PolkadotSigner(context.substrateWallet.alice), + context + ); + }); + + step('linking identities (alice)', async function () { + let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); + const getNextNonce = () => currentNonce++; + + const twitterNonce = getNextNonce(); + const twitterIdentity = await buildIdentityHelper('mock_user', 'Twitter', context); + const [twitterValidation] = await buildValidations( + context, + [aliceSubstrateIdentity], + [twitterIdentity], + twitterNonce, + 'twitter' + ); + const twitterNetworks = context.api.createType('Vec', []) as unknown as Vec; // @fixme #1878 + linkIdentityRequestParams.push({ + nonce: twitterNonce, + identity: twitterIdentity, + validation: twitterValidation, + networks: twitterNetworks, + }); + + const evmNonce = getNextNonce(); + const evmIdentity = await buildIdentityHelper(context.ethersWallet.alice.address, 'Evm', context); + const [evmValidation] = await buildValidations( + context, + [aliceSubstrateIdentity], + [evmIdentity], + evmNonce, + 'ethereum', + undefined, + [context.ethersWallet.alice] + ); + const evmNetworks = context.api.createType('Vec', [ + 'Ethereum', + 'Bsc', + ]) as unknown as Vec; // @fixme #1878 + linkIdentityRequestParams.push({ + nonce: evmNonce, + identity: evmIdentity, + validation: evmValidation, + networks: evmNetworks, + }); + + const bitcoinNonce = getNextNonce(); + const bitcoinIdentity = await buildIdentityHelper( + u8aToHex(bufferToU8a(context.bitcoinWallet.alice.toPublicKey().toBuffer())), + 'Bitcoin', + context + ); + console.log('bitcoin id: ', bitcoinIdentity.toHuman()); + const [bitcoinValidation] = await buildValidations( + context, + [aliceSubstrateIdentity], + [bitcoinIdentity], + bitcoinNonce, + 'bitcoin', + undefined, + undefined, + context.bitcoinWallet.alice + ); + const bitcoinNetworks = context.api.createType('Vec', [ + 'BitcoinP2tr', + ]) as unknown as Vec; // @fixme #1878 + linkIdentityRequestParams.push({ + nonce: bitcoinNonce, + identity: bitcoinIdentity, + validation: bitcoinValidation, + networks: bitcoinNetworks, + }); + + for (const { nonce, identity, validation, networks } of linkIdentityRequestParams) { + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + const linkIdentityCall = await createSignedTrustedCallLinkIdentity( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + new PolkadotSigner(context.substrateWallet.alice), + aliceSubstrateIdentity, + identity.toHex(), + validation.toHex(), + networks.toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier + ); + + const res = await sendRequestFromTrustedCall(context, teeShieldingKey, linkIdentityCall); + await assertIsInSidechainBlock('linkIdentityCall', res); + } + }); + + defaultAssertions.forEach(({ description, assertion }) => { + step(`request vc direct ${Object.keys(assertion)[0]} (alice)`, async function () { + let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); + const getNextNonce = () => currentNonce++; + const nonce = getNextNonce(); + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + console.log( + `request vc direct ${Object.keys(assertion)[0]} for Alice ... Assertion description: ${description}` + ); + const eventsPromise = subscribeToEventsWithExtHash(requestIdentifier, context); + + const requestVcCall = await createSignedTrustedCallRequestVc( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + new PolkadotSigner(context.substrateWallet.alice), + aliceSubstrateIdentity, + context.api.createType('Assertion', assertion).toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier + ); + + const isVcDirect = true; + const res = await sendRequestFromTrustedCall(context, teeShieldingKey, requestVcCall, isVcDirect); + const events = await eventsPromise; + const vcIssuedEvents = events + .map(({ event }) => event) + .filter(({ section, method }) => section === 'vcManagement' && method === 'VCIssued'); + + assert.equal( + vcIssuedEvents.length, + 1, + `vcIssuedEvents.length != 1, please check the ${Object.keys(assertion)[0]} call` + ); + await assertVc(context, aliceSubstrateIdentity, res.value); + }); + }); + unconfiguredAssertions.forEach(({ description, assertion }) => { + it(`request vc ${Object.keys(assertion)[0]} (alice)`, async function () { + let currentNonce = (await getSidechainNonce(context, teeShieldingKey, aliceSubstrateIdentity)).toNumber(); + const getNextNonce = () => currentNonce++; + const nonce = getNextNonce(); + const requestIdentifier = `0x${randomBytes(32).toString('hex')}`; + console.log(`request vc ${Object.keys(assertion)[0]} for Alice ... Assertion description: ${description}`); + subscribeToEventsWithExtHash(requestIdentifier, context); + + const requestVcCall = await createSignedTrustedCallRequestVc( + context.api, + context.mrEnclave, + context.api.createType('Index', nonce), + new PolkadotSigner(context.substrateWallet.alice), + aliceSubstrateIdentity, + context.api.createType('Assertion', assertion).toHex(), + context.api.createType('Option', aesKey).toHex(), + requestIdentifier + ); + + await sendRequestFromTrustedCall(context, teeShieldingKey, requestVcCall); + // pending test + this.skip(); + }); + }); +}); From 9bc0e7e25bbfe20c4ee4f3b22b1f170793485daf Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Wed, 14 Feb 2024 10:14:15 +0100 Subject: [PATCH 32/64] authorize signing tasks (#2483) --- bitacross-worker/Cargo.lock | 6 +- .../core/bc-relayer-registry/src/lib.rs | 20 ++++-- .../core/bc-task-receiver/Cargo.toml | 5 +- .../core/bc-task-receiver/src/lib.rs | 50 +++++++------ .../cli/src/trusted_command_utils.rs | 6 +- .../core-primitives/sgx/crypto/src/ecdsa.rs | 11 +-- .../core-primitives/sgx/crypto/src/mocks.rs | 8 +-- .../core-primitives/sgx/crypto/src/schnorr.rs | 10 ++- bitacross-worker/enclave-runtime/Cargo.lock | 4 +- .../enclave-runtime/src/initialization/mod.rs | 2 + .../litentry/core/direct-call/Cargo.toml | 12 +++- .../core/direct-call/src/handler/mod.rs | 2 + .../direct-call/src/handler/sign_bitcoin.rs | 70 +++++++++++++++++++ .../direct-call/src/handler/sign_ethereum.rs | 70 +++++++++++++++++++ .../litentry/core/direct-call/src/lib.rs | 2 + 15 files changed, 235 insertions(+), 43 deletions(-) create mode 100644 bitacross-worker/litentry/core/direct-call/src/handler/mod.rs create mode 100644 bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs create mode 100644 bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 4a3f99453f..4121e7a289 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -630,6 +630,7 @@ dependencies = [ name = "bc-task-receiver" version = "0.1.0" dependencies = [ + "bc-relayer-registry", "bc-task-sender", "frame-support", "futures 0.3.28", @@ -650,7 +651,6 @@ dependencies = [ "itp-types", "itp-utils", "lc-direct-call", - "litentry-macros", "litentry-primitives", "log 0.4.20", "parity-scale-codec", @@ -6308,10 +6308,14 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" name = "lc-direct-call" version = "0.1.0" dependencies = [ + "bc-relayer-registry", "core-primitives", + "itp-sgx-crypto", "itp-stf-primitives", + "k256", "litentry-primitives", "parity-scale-codec", + "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", "sgx_tstd", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs index eff6895977..4768c29b3d 100644 --- a/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/src/lib.rs @@ -111,6 +111,9 @@ pub trait RelayerRegistryUpdater { fn init(&self) -> RegistryResult<()>; fn update(&self, account: Identity) -> RegistryResult<()>; fn remove(&self, account: Identity) -> RegistryResult<()>; +} + +pub trait RelayerRegistryLookup { fn contains_key(&self, account: Identity) -> bool; } @@ -121,7 +124,9 @@ impl RelayerRegistryUpdater for RelayerRegistry { } #[cfg(feature = "std")] - fn update(&self, _account: Identity) -> RegistryResult<()> { + fn update(&self, account: Identity) -> RegistryResult<()> { + let mut registry = self.registry.write().unwrap(); + registry.insert(account, ()); Ok(()) } @@ -130,11 +135,6 @@ impl RelayerRegistryUpdater for RelayerRegistry { Ok(()) } - #[cfg(feature = "std")] - fn contains_key(&self, _account: Identity) -> bool { - true - } - // if `RELAYER_REGISTRY_FILE` exists, unseal and init from it // otherwise create a new instance and seal to static file #[cfg(feature = "sgx")] @@ -184,6 +184,14 @@ impl RelayerRegistryUpdater for RelayerRegistry { } Ok(()) } +} + +impl RelayerRegistryLookup for RelayerRegistry { + #[cfg(feature = "std")] + fn contains_key(&self, account: Identity) -> bool { + let registry = self.registry.read().unwrap(); + registry.contains_key(&account) + } #[cfg(feature = "sgx")] fn contains_key(&self, account: Identity) -> bool { diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml index 1075e7ce5f..e332e15e8f 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml @@ -43,8 +43,8 @@ itp-types = { path = "../../../core-primitives/types", default-features = false itp-utils = { path = "../../../core-primitives/utils", default-features = false } # litentry primities +bc-relayer-registry = { path = "../bc-relayer-registry", default-features = false } lc-direct-call = { path = "../../../litentry/core/direct-call", default-features = false } -litentry-macros = { path = "../../../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../../../litentry/primitives", default-features = false } bc-task-sender = { path = "../bc-task-sender", default-features = false } @@ -56,6 +56,7 @@ sgx = [ "hex-sgx", "sgx_tstd", "bc-task-sender/sgx", + "bc-relayer-registry/sgx", "lc-direct-call/sgx", "litentry-primitives/sgx", "ita-stf/sgx", @@ -75,6 +76,7 @@ std = [ "threadpool", "log/std", "bc-task-sender/std", + "bc-relayer-registry/std", "lc-direct-call/std", "litentry-primitives/std", "ita-sgx-runtime/std", @@ -95,5 +97,4 @@ std = [ "thiserror", ] production = [ - "litentry-macros/production", ] diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs index f9eac4d685..49e50c97ca 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs @@ -1,5 +1,6 @@ #![cfg_attr(not(feature = "std"), no_std)] +extern crate core; #[cfg(all(not(feature = "std"), feature = "sgx"))] extern crate sgx_tstd as std; @@ -17,6 +18,7 @@ compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the sam #[cfg(all(not(feature = "std"), feature = "sgx"))] pub use crate::sgx_reexport_prelude::*; +use core::ops::Deref; use bc_task_sender::init_bit_across_task_sender_storage; use codec::{Decode, Encode}; @@ -39,9 +41,10 @@ use itp_sgx_externalities::SgxExternalitiesTrait; use itp_stf_executor::traits::StfEnclaveSigning; use itp_stf_state_handler::handle_state::HandleState; +use bc_relayer_registry::RelayerRegistryLookup; use ita_stf::TrustedCallSigned; use itp_sgx_crypto::{ecdsa::Pair as EcdsaPair, schnorr::Pair as SchnorrPair}; -use litentry_macros::if_production_or; +use lc_direct_call::handler::{sign_bitcoin, sign_ethereum}; use litentry_primitives::DecryptableRequest; #[derive(Debug, thiserror::Error, Clone)] @@ -60,6 +63,7 @@ pub struct BitAcrossTaskContext< S: StfEnclaveSigning, H: HandleState, O: EnclaveOnChainOCallApi, + RRL: RelayerRegistryLookup, > where SKR: AccessKey, EKR: AccessKey, @@ -72,6 +76,7 @@ pub struct BitAcrossTaskContext< pub enclave_signer: Arc, pub state_handler: Arc, pub ocall_api: Arc, + pub relayer_registry_lookup: Arc, } impl< @@ -81,7 +86,8 @@ impl< S: StfEnclaveSigning, H: HandleState, O: EnclaveOnChainOCallApi, - > BitAcrossTaskContext + RRL: RelayerRegistryLookup, + > BitAcrossTaskContext where SKR: AccessKey, EKR: AccessKey, @@ -96,6 +102,7 @@ where enclave_signer: Arc, state_handler: Arc, ocall_api: Arc, + relayer_registry_lookup: Arc, ) -> Self { Self { shielding_key, @@ -104,12 +111,13 @@ where enclave_signer, state_handler, ocall_api, + relayer_registry_lookup, } } } -pub fn run_bit_across_handler_runner( - context: Arc>, +pub fn run_bit_across_handler_runner( + context: Arc>, ) where SKR: AccessKey + Send + Sync + 'static, EKR: AccessKey + Send + Sync + 'static, @@ -119,6 +127,7 @@ pub fn run_bit_across_handler_runner( H: HandleState + Send + Sync + 'static, H::StateT: SgxExternalitiesTrait, O: EnclaveOnChainOCallApi + EnclaveMetricsOCallApi + EnclaveAttestationOCallApi + 'static, + RRL: RelayerRegistryLookup + Send + Sync + 'static, { let bit_across_task_receiver = init_bit_across_task_sender_storage(); let n_workers = 2; @@ -137,9 +146,9 @@ pub fn run_bit_across_handler_runner( warn!("bit_across_task_receiver loop terminated"); } -pub fn handle_request( +pub fn handle_request( request: &mut AesRequest, - context: Arc>, + context: Arc>, ) -> Result, String> where SKR: AccessKey, @@ -149,6 +158,7 @@ where S: StfEnclaveSigning + Send + Sync + 'static, H: HandleState + Send + Sync + 'static, O: EnclaveOnChainOCallApi + EnclaveMetricsOCallApi + EnclaveAttestationOCallApi + 'static, + RRL: RelayerRegistryLookup + 'static, { let enclave_shielding_key = context .shielding_key @@ -166,19 +176,19 @@ where }; ensure!(dc.verify_signature(&mrenclave, &request.shard), "Failed to verify sig".to_string()); match dc.call { - DirectCall::SignBitcoin(_, aes_key, payload) => { - if_production_or!(unimplemented!(), { - let key = context.bitcoin_key_repository.retrieve_key().unwrap(); - let signature = key.sign(&payload).unwrap(); - Ok(aes_encrypt_default(&aes_key, &signature).encode()) - }) - }, - DirectCall::SignEthereum(_, aes_key, payload) => { - if_production_or!(unimplemented!(), { - let key = context.ethereum_key_repository.retrieve_key().unwrap(); - let signature = key.sign(&payload).unwrap(); - Ok(aes_encrypt_default(&aes_key, &signature).encode()) - }) - }, + DirectCall::SignBitcoin(signer, aes_key, payload) => sign_bitcoin::handle( + signer, + payload, + context.relayer_registry_lookup.deref(), + context.bitcoin_key_repository.deref(), + ) + .map(|r| aes_encrypt_default(&aes_key, &r).encode()), + DirectCall::SignEthereum(signer, aes_key, payload) => sign_ethereum::handle( + signer, + payload, + context.relayer_registry_lookup.deref(), + context.ethereum_key_repository.deref(), + ) + .map(|r| aes_encrypt_default(&aes_key, &r).encode()), } } diff --git a/bitacross-worker/cli/src/trusted_command_utils.rs b/bitacross-worker/cli/src/trusted_command_utils.rs index 57704b982c..ae61dd60cb 100644 --- a/bitacross-worker/cli/src/trusted_command_utils.rs +++ b/bitacross-worker/cli/src/trusted_command_utils.rs @@ -116,7 +116,11 @@ pub(crate) fn get_pair_from_str( ) -> sr25519_core::Pair { info!("getting pair for {}", account); match &account[..2] { - "//" => sr25519_core::Pair::from_string(account, None).unwrap(), + "//" => { + let pair = sr25519_core::Pair::from_string(account, None).unwrap(); + info!("public_key: {:?}", &pair.public().to_hex()); + pair + }, _ => { info!("fetching from keystore at {}", &TRUSTED_KEYSTORE_PATH); // open store without password protection diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index ee2a68c451..1bbee23e2a 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -18,7 +18,7 @@ pub use sgx::*; use crate::error::{Error, Result}; use k256::{ - ecdsa::{signature::Signer, Signature, SigningKey}, + ecdsa::{signature::Signer, Signature, SigningKey, VerifyingKey}, elliptic_curve::group::GroupEncoding, PublicKey, }; @@ -34,6 +34,11 @@ pub struct Pair { } impl Pair { + pub fn new(private: SigningKey) -> Self { + let public = PublicKey::from(VerifyingKey::from(&private)); + Self { private, public } + } + pub fn public_bytes(&self) -> Vec { self.public.as_affine().to_bytes().as_slice().to_vec() } @@ -117,9 +122,7 @@ pub mod sgx { let raw = unseal(self.path())?; let secret = SigningKey::from_slice(&raw) .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - let public = PublicKey::from(VerifyingKey::from(&secret)); - Ok(Pair { public, private: secret }) + Ok(Pair::new(secret)) } fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/mocks.rs b/bitacross-worker/core-primitives/sgx/crypto/src/mocks.rs index 0e199378fd..a871943806 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/mocks.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/mocks.rs @@ -32,14 +32,14 @@ use sgx_crypto_helper::rsa3072::Rsa3072KeyPair; #[derive(Default)] pub struct KeyRepositoryMock where - KeyType: Clone + Default, + KeyType: Clone, { key: RwLock, } impl KeyRepositoryMock where - KeyType: Clone + Default, + KeyType: Clone, { pub fn new(key: KeyType) -> Self { KeyRepositoryMock { key: RwLock::new(key) } @@ -48,7 +48,7 @@ where impl AccessKey for KeyRepositoryMock where - KeyType: Clone + Default, + KeyType: Clone, { type KeyType = KeyType; @@ -59,7 +59,7 @@ where impl MutateKey for KeyRepositoryMock where - KeyType: Clone + Default, + KeyType: Clone, { fn update_key(&self, key: KeyType) -> Result<()> { let mut lock = self.key.write().unwrap(); diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs index 11b0ecd7ae..fe9e91f1fc 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs @@ -34,6 +34,11 @@ pub struct Pair { } impl Pair { + pub fn new(private: SigningKey) -> Self { + let public = PublicKey::from(private.verifying_key()); + Self { private, public } + } + pub fn public_bytes(&self) -> Vec { self.public.as_affine().to_bytes().as_slice().to_vec() } @@ -55,7 +60,7 @@ pub mod sgx { std::string::ToString, }; use itp_sgx_io::{seal, unseal, SealedIO}; - use k256::{schnorr::SigningKey, PublicKey}; + use k256::schnorr::SigningKey; use log::*; use sgx_rand::{Rng, StdRng}; use std::{path::PathBuf, string::String}; @@ -116,8 +121,7 @@ pub mod sgx { let raw = unseal(self.path())?; let secret = SigningKey::from_bytes(&raw) .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - let public = PublicKey::from(secret.verifying_key().clone()); - Ok(Pair { public, private: secret }) + Ok(Pair::new(secret)) } fn seal(&self, unsealed: &Self::Unsealed) -> Result<()> { diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index b83357c121..f400009087 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -284,6 +284,7 @@ dependencies = [ name = "bc-task-receiver" version = "0.1.0" dependencies = [ + "bc-relayer-registry", "bc-task-sender", "frame-support", "futures 0.3.8", @@ -303,7 +304,6 @@ dependencies = [ "itp-types", "itp-utils", "lc-direct-call", - "litentry-macros", "litentry-primitives", "log", "parity-scale-codec", @@ -2930,7 +2930,9 @@ checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" name = "lc-direct-call" version = "0.1.0" dependencies = [ + "bc-relayer-registry", "core-primitives", + "itp-sgx-crypto", "itp-stf-primitives", "litentry-primitives", "parity-scale-codec", diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index 136a071b75..c2692bd1c6 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -256,6 +256,7 @@ fn run_bit_across_handler() -> Result<(), Error> { let author_api = GLOBAL_TOP_POOL_AUTHOR_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let state_observer = GLOBAL_STATE_OBSERVER_COMPONENT.get()?; + let relayer_registry_lookup = GLOBAL_RELAYER_REGISTRY.clone(); let shielding_key_repository = GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT.get()?; let ethereum_key_repository = GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.get()?; @@ -277,6 +278,7 @@ fn run_bit_across_handler() -> Result<(), Error> { stf_enclave_signer, state_handler, ocall_api, + relayer_registry_lookup, ); run_bit_across_handler_runner(Arc::new(stf_task_context)); Ok(()) diff --git a/bitacross-worker/litentry/core/direct-call/Cargo.toml b/bitacross-worker/litentry/core/direct-call/Cargo.toml index b40a525ee3..14214a5355 100644 --- a/bitacross-worker/litentry/core/direct-call/Cargo.toml +++ b/bitacross-worker/litentry/core/direct-call/Cargo.toml @@ -11,6 +11,8 @@ sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0 sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } # internal dependencies +bc-relayer-registry = { path = "../../../bitacross/core/bc-relayer-registry", default-features = false } +itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } itp-stf-primitives = { path = "../../../core-primitives/stf-primitives", default-features = false } litentry-primitives = { path = "../../primitives", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } @@ -18,8 +20,9 @@ parentchain-primitives = { package = "core-primitives", path = "../../../../prim # sgx dependencies sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true } - [dev-dependencies] +k256 = { version = "0.13.3", features = ["ecdsa-core", "schnorr"] } +rand = { version = "0.7" } [features] default = ["std"] @@ -28,13 +31,20 @@ production = [ ] sgx = [ "sgx_tstd", + "bc-relayer-registry/sgx", "litentry-primitives/sgx", + "itp-sgx-crypto/sgx", ] std = [ + "bc-relayer-registry/std", "itp-stf-primitives/std", + "itp-sgx-crypto/std", "litentry-primitives/std", "sp-core/std", "sp-io/std", "sp-runtime/std", "parentchain-primitives/std", ] +test = [ + "itp-sgx-crypto/mocks", +] diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/mod.rs b/bitacross-worker/litentry/core/direct-call/src/handler/mod.rs new file mode 100644 index 0000000000..95984698bb --- /dev/null +++ b/bitacross-worker/litentry/core/direct-call/src/handler/mod.rs @@ -0,0 +1,2 @@ +pub mod sign_bitcoin; +pub mod sign_ethereum; diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs b/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs new file mode 100644 index 0000000000..251d56b435 --- /dev/null +++ b/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs @@ -0,0 +1,70 @@ +use bc_relayer_registry::RelayerRegistryLookup; +use itp_sgx_crypto::{key_repository::AccessKey, schnorr::Pair}; +use parentchain_primitives::Identity; +use std::{ + string::{String, ToString}, + vec::Vec, +}; + +pub fn handle>( + signer: Identity, + payload: Vec, + relayer_registry: &RRL, + key_repository: &BKR, +) -> Result<[u8; 64], String> { + if relayer_registry.contains_key(signer) { + let key = key_repository.retrieve_key().unwrap(); + Ok(key.sign(&payload).unwrap()) + } else { + Err("Unauthorized: Signer is not a valid relayer".to_string()) + } +} + +#[cfg(test)] +pub mod test { + use crate::handler::sign_bitcoin::handle; + use bc_relayer_registry::{RelayerRegistry, RelayerRegistryUpdater}; + use itp_sgx_crypto::mocks::KeyRepositoryMock; + use k256::elliptic_curve::rand_core; + use parentchain_primitives::{Address32, Identity}; + use sp_core::{crypto::Ss58Codec, sr25519 as sr25519_core, sr25519, Pair}; + + #[test] + pub fn it_should_return_ok_for_relayer_signer() { + //given + let relayer_registry = RelayerRegistry::default(); + let alice_key_pair = sr25519::Pair::from_string("//Alice", None).unwrap(); + let relayer_account = Identity::Substrate(alice_key_pair.public().into()); + relayer_registry.update(relayer_account.clone()).unwrap(); + + let private = k256::schnorr::SigningKey::random(&mut rand_core::OsRng); + let signing_key = itp_sgx_crypto::schnorr::Pair::new(private); + + let key_repository = KeyRepositoryMock::new(signing_key); + + //when + let result = handle(relayer_account, vec![], &relayer_registry, &key_repository); + + //then + assert!(result.is_ok()) + } + + #[test] + pub fn it_should_return_err_for_non_relayer_signer() { + //given + let relayer_registry = RelayerRegistry::default(); + let alice_key_pair = sr25519::Pair::from_string("//Alice", None).unwrap(); + let non_relayer_account = Identity::Substrate(alice_key_pair.public().into()); + + let private = k256::schnorr::SigningKey::random(&mut rand_core::OsRng); + let signing_key = itp_sgx_crypto::schnorr::Pair::new(private); + + let key_repository = KeyRepositoryMock::new(signing_key); + + //when + let result = handle(non_relayer_account, vec![], &relayer_registry, &key_repository); + + //then + assert!(result.is_err()) + } +} diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs new file mode 100644 index 0000000000..12bbf397fa --- /dev/null +++ b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs @@ -0,0 +1,70 @@ +use bc_relayer_registry::RelayerRegistryLookup; +use itp_sgx_crypto::{ecdsa::Pair, key_repository::AccessKey}; +use parentchain_primitives::Identity; +use std::{ + string::{String, ToString}, + vec::Vec, +}; + +pub fn handle>( + signer: Identity, + payload: Vec, + relayer_registry: &RRL, + key_repository: &BKR, +) -> Result<[u8; 64], String> { + if relayer_registry.contains_key(signer) { + let key = key_repository.retrieve_key().unwrap(); + Ok(key.sign(&payload).unwrap()) + } else { + Err("Unauthorized: Signer is not a valid relayer".to_string()) + } +} + +#[cfg(test)] +pub mod test { + use crate::handler::sign_ethereum::handle; + use bc_relayer_registry::{RelayerRegistry, RelayerRegistryUpdater}; + use itp_sgx_crypto::mocks::KeyRepositoryMock; + use k256::elliptic_curve::rand_core; + use parentchain_primitives::{Address32, Identity}; + use sp_core::{crypto::Ss58Codec, sr25519 as sr25519_core, sr25519, Pair}; + + #[test] + pub fn it_should_return_ok_for_relayer_signer() { + //given + let relayer_registry = RelayerRegistry::default(); + let alice_key_pair = sr25519::Pair::from_string("//Alice", None).unwrap(); + let relayer_account = Identity::Substrate(alice_key_pair.public().into()); + relayer_registry.update(relayer_account.clone()).unwrap(); + + let private = k256::ecdsa::SigningKey::random(&mut rand_core::OsRng); + let signing_key = itp_sgx_crypto::ecdsa::Pair::new(private); + + let key_repository = KeyRepositoryMock::new(signing_key); + + //when + let result = handle(relayer_account, vec![], &relayer_registry, &key_repository); + + //then + assert!(result.is_ok()) + } + + #[test] + pub fn it_should_return_err_for_non_relayer_signer() { + //given + let relayer_registry = RelayerRegistry::default(); + let alice_key_pair = sr25519::Pair::from_string("//Alice", None).unwrap(); + let non_relayer_account = Identity::Substrate(alice_key_pair.public().into()); + + let private = k256::ecdsa::SigningKey::random(&mut rand_core::OsRng); + let signing_key = itp_sgx_crypto::ecdsa::Pair::new(private); + + let key_repository = KeyRepositoryMock::new(signing_key); + + //when + let result = handle(non_relayer_account, vec![], &relayer_registry, &key_repository); + + //then + assert!(result.is_err()) + } +} diff --git a/bitacross-worker/litentry/core/direct-call/src/lib.rs b/bitacross-worker/litentry/core/direct-call/src/lib.rs index dc32ebf3b7..20b402561a 100644 --- a/bitacross-worker/litentry/core/direct-call/src/lib.rs +++ b/bitacross-worker/litentry/core/direct-call/src/lib.rs @@ -26,6 +26,8 @@ use parentchain_primitives::Identity; use sp_io::hashing::blake2_256; use std::vec::Vec; +pub mod handler; + #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub struct DirectCallSigned { pub call: DirectCall, From 6cfefa17452995d4f1e9be338f599fd266aadf5e Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Wed, 14 Feb 2024 10:15:12 +0100 Subject: [PATCH 33/64] More cleanup around the workers (#2486) * remove more stuff * remove heartbeats * remove drop3 integration * missing impl * fix test --- .../app-libs/stf/src/trusted_call.rs | 7 +- .../cli/lit_set_heartbeat_timeout.sh | 85 --------- .../cli/src/base_cli/commands/litentry/mod.rs | 2 - .../litentry/set_heartbeat_timeout.rs | 54 ------ bitacross-worker/cli/src/base_cli/mod.rs | 5 - .../test/src/builders/enclave_gen_builder.rs | 63 ------- .../core-primitives/test/src/builders/mod.rs | 21 --- .../core-primitives/types/src/lib.rs | 17 +- .../indirect-calls-executor/src/executor.rs | 6 +- .../docker/lit-set-heartbeat-timeout.yml | 24 --- .../src/test/top_pool_tests.rs | 4 +- runtime/litentry/src/lib.rs | 16 -- runtime/litentry/src/weights/mod.rs | 1 - runtime/litentry/src/weights/pallet_drop3.rs | 175 ----------------- runtime/litmus/src/lib.rs | 16 -- runtime/litmus/src/weights/mod.rs | 1 - runtime/litmus/src/weights/pallet_drop3.rs | 177 ------------------ runtime/rococo/src/lib.rs | 16 -- runtime/rococo/src/weights/mod.rs | 1 - runtime/rococo/src/weights/pallet_drop3.rs | 177 ------------------ tee-worker/cli/lit_set_heartbeat_timeout.sh | 85 --------- .../cli/src/base_cli/commands/litentry/mod.rs | 1 - .../litentry/set_heartbeat_timeout.rs | 54 ------ tee-worker/cli/src/base_cli/mod.rs | 9 +- .../test/src/builders/enclave_gen_builder.rs | 63 ------- .../core-primitives/test/src/builders/mod.rs | 21 --- tee-worker/core-primitives/types/src/lib.rs | 33 +--- .../indirect-calls-executor/src/executor.rs | 6 +- .../docker/lit-set-heartbeat-timeout.yml | 24 --- .../src/test/top_pool_tests.rs | 5 +- 30 files changed, 22 insertions(+), 1147 deletions(-) delete mode 100755 bitacross-worker/cli/lit_set_heartbeat_timeout.sh delete mode 100644 bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs delete mode 100644 bitacross-worker/core-primitives/test/src/builders/enclave_gen_builder.rs delete mode 100644 bitacross-worker/core-primitives/test/src/builders/mod.rs delete mode 100644 bitacross-worker/docker/lit-set-heartbeat-timeout.yml delete mode 100644 runtime/litentry/src/weights/pallet_drop3.rs delete mode 100644 runtime/litmus/src/weights/pallet_drop3.rs delete mode 100644 runtime/rococo/src/weights/pallet_drop3.rs delete mode 100755 tee-worker/cli/lit_set_heartbeat_timeout.sh delete mode 100644 tee-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs delete mode 100644 tee-worker/core-primitives/test/src/builders/enclave_gen_builder.rs delete mode 100644 tee-worker/core-primitives/test/src/builders/mod.rs delete mode 100644 tee-worker/docker/lit-set-heartbeat-timeout.yml diff --git a/bitacross-worker/app-libs/stf/src/trusted_call.rs b/bitacross-worker/app-libs/stf/src/trusted_call.rs index c920aad039..028c9e1d0d 100644 --- a/bitacross-worker/app-libs/stf/src/trusted_call.rs +++ b/bitacross-worker/app-libs/stf/src/trusted_call.rs @@ -56,6 +56,7 @@ use sp_core::{ crypto::{AccountId32, UncheckedFrom}, ed25519, }; +use sp_io::hashing::blake2_256; use sp_runtime::MultiAddress; use std::{format, prelude::v1::*, sync::Arc}; @@ -155,7 +156,8 @@ impl TrustedCallSigning for TrustedCall { payload.append(&mut mrenclave.encode()); payload.append(&mut shard.encode()); - TrustedCallSigned { call: self.clone(), nonce, signature: pair.sign(payload.as_slice()) } + // use blake2_256 hash to shorten the payload - see `verify_signature` below + TrustedCallSigned { call: self.clone(), nonce, signature: pair.sign(&blake2_256(&payload)) } } } @@ -208,7 +210,8 @@ impl TrustedCallVerification for TrustedCallSigned { payload.append(&mut mrenclave.encode()); payload.append(&mut shard.encode()); - self.signature.verify(payload.as_slice(), self.call.sender_identity()) + self.signature.verify(&blake2_256(&payload), self.call.sender_identity()) + || self.signature.verify(&payload, self.call.sender_identity()) } } diff --git a/bitacross-worker/cli/lit_set_heartbeat_timeout.sh b/bitacross-worker/cli/lit_set_heartbeat_timeout.sh deleted file mode 100755 index f062118c0a..0000000000 --- a/bitacross-worker/cli/lit_set_heartbeat_timeout.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -# Copyright 2020-2023 Trust Computing GmbH. - -while getopts ":p:A:B:u:W:V:C:" opt; do - case $opt in - p) - NPORT=$OPTARG - ;; - A) - WORKER1PORT=$OPTARG - ;; - B) - WORKER2PORT=$OPTARG - ;; - u) - NODEURL=$OPTARG - ;; - V) - WORKER1URL=$OPTARG - ;; - W) - WORKER2URL=$OPTARG - ;; - C) - CLIENT_BIN=$OPTARG - ;; - esac -done - -# Using default port if none given as arguments. -NPORT=${NPORT:-9944} -NODEURL=${NODEURL:-"ws://127.0.0.1"} - -WORKER1PORT=${WORKER1PORT:-2000} -WORKER1URL=${WORKER1URL:-"wss://127.0.0.1"} - -CLIENT_BIN=${CLIENT_BIN:-"../bin/bitacross-cli"} - -LOG_FOLDER="./../log" - -echo "Using client binary $CLIENT_BIN" -echo "Using node uri $NODEURL:$NPORT" -echo "Using trusted-worker uri $WORKER1URL:$WORKER1PORT" -echo "" - -TIMEOUT=5000 # 5 seconds, smaller than 12s (the block duration) - -CLIENT="$CLIENT_BIN -p $NPORT -P $WORKER1PORT -u $NODEURL -U $WORKER1URL" -echo "CLIENT is: $CLIENT" - -echo "* Query on-chain enclave registry:" -WORKERS=$($CLIENT list-workers) -echo "WORKERS: " -echo "${WORKERS}" -echo "" - -if [ "$READMRENCLAVE" = "file" ] -then - read MRENCLAVE <<< $(cat ~/mrenclave.b58) - echo "Reading MRENCLAVE from file: ${MRENCLAVE}" -else - # This will always take the first MRENCLAVE found in the registry !! - read MRENCLAVE <<< $(echo "$WORKERS" | awk '/ MRENCLAVE: / { print $2; exit }') - echo "Reading MRENCLAVE from worker list: ${MRENCLAVE}" -fi -[[ -z $MRENCLAVE ]] && { echo "MRENCLAVE is empty. cannot continue" ; exit 1; } - - -# indirect call that will be sent to the parachain, it will be synchronously handled -sleep 10 -echo "* Set heartbeat timeout to $TIMEOUT" -${CLIENT} set-heartbeat-timeout "$TIMEOUT" -echo "" - -sleep 120 - -read MRENCLAVE <<< $($CLIENT list-workers | awk '/ MRENCLAVE: / { print $2; exit }') -if [[ -z $MRENCLAVE ]] -then - echo "All workers removed, test passed" -else - echo "Worker(s) still exist(s), test fail" - exit 1 -fi diff --git a/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs b/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs index 4a7cec7cd4..20182fa371 100644 --- a/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs +++ b/bitacross-worker/cli/src/base_cli/commands/litentry/mod.rs @@ -13,5 +13,3 @@ // // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . - -pub mod set_heartbeat_timeout; diff --git a/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs b/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs deleted file mode 100644 index 996b369a59..0000000000 --- a/bitacross-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use crate::{command_utils::get_chain_api, Cli}; - -use crate::{CliResult, CliResultOk}; -use itp_node_api::api_client::TEEREX; -use log::*; -use sp_keyring::AccountKeyring; -use substrate_api_client::{ac_compose_macros::compose_extrinsic, SubmitAndWatch, XtStatus}; -#[derive(Parser)] -pub struct SetHeartbeatTimeoutCommand { - /// Heartbeat timeout - timeout: u64, -} - -impl SetHeartbeatTimeoutCommand { - pub(crate) fn run(&self, cli: &Cli) -> CliResult { - let mut chain_api = get_chain_api(cli); - - // has to be //Alice as this is the genesis admin for teerex pallet, - // otherwise `set_heartbeat_timeout` call won't work - chain_api.set_signer(AccountKeyring::Alice.pair().into()); - - // call set_heartbeat_timeout - let xt = compose_extrinsic!( - chain_api, - TEEREX, - "set_heartbeat_timeout", - codec::Compact(self.timeout) - ); - - let tx_hash = chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).unwrap(); - println!( - "[+] SetHeartbeatTimeoutCommand TrustedOperation got finalized. Hash: {:?}\n", - tx_hash - ); - - Ok(CliResultOk::None) - } -} diff --git a/bitacross-worker/cli/src/base_cli/mod.rs b/bitacross-worker/cli/src/base_cli/mod.rs index a2ec00e64c..dd82a8ee33 100644 --- a/bitacross-worker/cli/src/base_cli/mod.rs +++ b/bitacross-worker/cli/src/base_cli/mod.rs @@ -18,7 +18,6 @@ use crate::{ base_cli::commands::{ balance::BalanceCommand, faucet::FaucetCommand, listen::ListenCommand, - litentry::set_heartbeat_timeout::SetHeartbeatTimeoutCommand, register_tcb_info::RegisterTcbInfoCommand, shield_funds::ShieldFundsCommand, transfer::TransferCommand, }, @@ -81,9 +80,6 @@ pub enum BaseCommand { /// we could have added a parameter like `--raw` to `PrintSgxMetadata`, but /// we want to keep our changes isolated PrintSgxMetadataRaw, - - /// set heartbeat timeout storage - SetHeartbeatTimeout(SetHeartbeatTimeoutCommand), } impl BaseCommand { @@ -102,7 +98,6 @@ impl BaseCommand { BaseCommand::ShieldFunds(cmd) => cmd.run(cli), // Litentry's commands below BaseCommand::PrintSgxMetadataRaw => print_sgx_metadata_raw(cli), - BaseCommand::SetHeartbeatTimeout(cmd) => cmd.run(cli), } } } diff --git a/bitacross-worker/core-primitives/test/src/builders/enclave_gen_builder.rs b/bitacross-worker/core-primitives/test/src/builders/enclave_gen_builder.rs deleted file mode 100644 index 85e807c628..0000000000 --- a/bitacross-worker/core-primitives/test/src/builders/enclave_gen_builder.rs +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use itp_time_utils::now_as_millis; -use itp_types::{Enclave, PalletString}; - -/// Builder for a generic enclave (`EnclaveGen`) struct. -pub struct EnclaveGenBuilder { - pubkey: AccountId, - mr_enclave: [u8; 32], - timestamp: u64, - url: PalletString, // utf8 encoded url -} - -impl Default for EnclaveGenBuilder -where - AccountId: Default, -{ - fn default() -> Self { - EnclaveGenBuilder { - pubkey: AccountId::default(), - mr_enclave: [0u8; 32], - timestamp: now_as_millis(), - url: PalletString::default(), - } - } -} - -impl EnclaveGenBuilder { - pub fn with_account(mut self, account: AccountId) -> Self { - self.pubkey = account; - self - } - - pub fn with_url(mut self, url: PalletString) -> Self { - self.url = url; - self - } - - pub fn build(self) -> EnclaveGen { - EnclaveGen { - pubkey: self.pubkey, - mr_enclave: self.mr_enclave, - timestamp: self.timestamp, - url: self.url, - } - } -} diff --git a/bitacross-worker/core-primitives/test/src/builders/mod.rs b/bitacross-worker/core-primitives/test/src/builders/mod.rs deleted file mode 100644 index 610066f015..0000000000 --- a/bitacross-worker/core-primitives/test/src/builders/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -//! Builder patterns for common structs used in tests. - -pub mod enclave_gen_builder; diff --git a/bitacross-worker/core-primitives/types/src/lib.rs b/bitacross-worker/core-primitives/types/src/lib.rs index cc5d26ea9c..a4e378786c 100644 --- a/bitacross-worker/core-primitives/types/src/lib.rs +++ b/bitacross-worker/core-primitives/types/src/lib.rs @@ -27,15 +27,6 @@ use sp_std::{boxed::Box, fmt::Debug, vec::Vec}; pub mod parentchain; pub mod storage; -/// Substrate runtimes provide no string type. Hence, for arbitrary data of varying length the -/// `Vec` is used. In the polkadot-js the typedef `Text` is used to automatically -/// utf8 decode bytes into a string. -#[cfg(not(feature = "std"))] -pub type PalletString = Vec; - -#[cfg(feature = "std")] -pub type PalletString = String; - pub use itp_sgx_runtime_primitives::types::*; pub use litentry_primitives::{ AttestationType, DecryptableRequest, Enclave, EnclaveFingerprint, MrEnclave, WorkerType, @@ -45,13 +36,7 @@ pub use sp_core::{crypto::AccountId32 as AccountId, H256}; pub type IpfsHash = [u8; 46]; pub type CallIndex = [u8; 2]; -// pallet teerex -pub type ConfirmCallFn = (CallIndex, ShardIdentifier, H256, Vec); -pub type ShieldFundsFn = (CallIndex, Vec, Balance, ShardIdentifier); -pub type CallWorkerFn = (CallIndex, RsaRequest); - -pub type SetScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); -pub type RemoveScheduledEnclaveFn = (CallIndex, SidechainBlockNumber); +pub type PostOpaqueTaskFn = (CallIndex, RsaRequest); /// Simple blob to hold an encoded call #[derive(Debug, PartialEq, Eq, Clone, Default)] diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs index 7132b0bdeb..aae0dcc240 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs @@ -320,7 +320,7 @@ mod test { stf_mock::{GetterMock, TrustedCallSignedMock}, }; use itp_top_pool_author::mocks::AuthorApiMock; - use itp_types::{Block, CallWorkerFn, RsaRequest, ShardIdentifier, ShieldFundsFn}; + use itp_types::{Block, PostOpaqueTaskFn, RsaRequest, ShardIdentifier}; use sp_core::{ed25519, Pair}; use sp_runtime::{MultiAddress, MultiSignature, OpaqueExtrinsic}; use std::assert_matches::assert_matches; @@ -411,12 +411,12 @@ mod test { assert_ne!(call.0, zero_root_call); } - fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { + fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { let request = RsaRequest::new(shard_id(), vec![1u8, 2u8]); let dummy_metadata = NodeMetadataMock::new(); let call_worker_indexes = dummy_metadata.post_opaque_task_call_indexes().unwrap(); - ParentchainUncheckedExtrinsic::::new_signed( + ParentchainUncheckedExtrinsic::::new_signed( (call_worker_indexes, request), MultiAddress::Address32([1u8; 32]), MultiSignature::Ed25519(default_signature()), diff --git a/bitacross-worker/docker/lit-set-heartbeat-timeout.yml b/bitacross-worker/docker/lit-set-heartbeat-timeout.yml deleted file mode 100644 index b4e271ce4c..0000000000 --- a/bitacross-worker/docker/lit-set-heartbeat-timeout.yml +++ /dev/null @@ -1,24 +0,0 @@ -services: - lit-set-heartbeat-timeout: - image: litentry/bitacross-cli:latest - container_name: litentry-set-heartbeat-timeout - volumes: - - ../cli:/usr/local/worker-cli - build: - context: .. - dockerfile: build.Dockerfile - target: deployed-client - depends_on: - litentry-node: - condition: service_healthy - bitacross-worker-1: - condition: service_healthy - networks: - - litentry-test-network - entrypoint: - "/usr/local/worker-cli/lit_set_heartbeat_timeout.sh -p 9912 -u ws://litentry-node - -V wss://bitacross-worker-1 -A 2011 -W wss://bitacross-worker-2 -B 2012 -C /usr/local/bin/bitacross-cli 2>&1" - restart: "no" -networks: - litentry-test-network: - driver: bridge \ No newline at end of file diff --git a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs index cb4e5e7a1d..49e39239e2 100644 --- a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -52,7 +52,7 @@ use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, traits::AuthorApi, }; -use itp_types::{parentchain::Address, Block, CallWorkerFn, RsaRequest, ShardIdentifier, H256}; +use itp_types::{parentchain::Address, Block, RsaRequest, ShardIdentifier, H256}; use jsonrpc_core::futures::executor; use litentry_primitives::Identity; use log::*; @@ -190,6 +190,7 @@ fn encrypted_indirect_call< encrypt_trusted_operation(shielding_key, &trusted_operation) } +/* fn create_opaque_call_extrinsic( _shard: ShardIdentifier, _shielding_key: &ShieldingKey, @@ -224,3 +225,4 @@ fn create_opaque_call_extrinsic( .with_extrinsics(vec![opaque_extrinsic]) .build() } +*/ diff --git a/runtime/litentry/src/lib.rs b/runtime/litentry/src/lib.rs index 7158892b06..fd3e2b13bd 100644 --- a/runtime/litentry/src/lib.rs +++ b/runtime/litentry/src/lib.rs @@ -837,20 +837,6 @@ impl pallet_bridge_transfer::Config for Runtime { type WeightInfo = weights::pallet_bridge_transfer::WeightInfo; } -parameter_types! { - pub const SlashPercent: Percent = Percent::from_percent(20); -} - -impl pallet_drop3::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type PoolId = u64; - type SetAdminOrigin = EnsureRootOrHalfCouncil; - type Currency = Balances; - type WeightInfo = weights::pallet_drop3::WeightInfo; - type SlashPercent = SlashPercent; - type MaximumNameLength = ConstU32<16>; -} - impl pallet_extrinsic_filter::Config for Runtime { type RuntimeEvent = RuntimeEvent; type UpdateOrigin = EnsureRootOrHalfTechnicalCommittee; @@ -928,7 +914,6 @@ construct_runtime! { // Litentry pallets ChainBridge: pallet_bridge = 60, BridgeTransfer: pallet_bridge_transfer = 61, - Drop3: pallet_drop3 = 62, ExtrinsicFilter: pallet_extrinsic_filter = 63, AssetManager: pallet_asset_manager = 64, @@ -1017,7 +1002,6 @@ mod benches { [pallet_proxy, Proxy] [pallet_membership, CouncilMembership] [pallet_multisig, Multisig] - [pallet_drop3, Drop3] [pallet_extrinsic_filter, ExtrinsicFilter] [pallet_scheduler, Scheduler] [pallet_preimage, Preimage] diff --git a/runtime/litentry/src/weights/mod.rs b/runtime/litentry/src/weights/mod.rs index 91e65a5b49..ac1f92c554 100644 --- a/runtime/litentry/src/weights/mod.rs +++ b/runtime/litentry/src/weights/mod.rs @@ -24,7 +24,6 @@ pub mod pallet_bridge; pub mod pallet_bridge_transfer; pub mod pallet_collective; pub mod pallet_democracy; -pub mod pallet_drop3; pub mod pallet_extrinsic_filter; pub mod pallet_identity; pub mod pallet_membership; diff --git a/runtime/litentry/src/weights/pallet_drop3.rs b/runtime/litentry/src/weights/pallet_drop3.rs deleted file mode 100644 index 3cbc4782e4..0000000000 --- a/runtime/litentry/src/weights/pallet_drop3.rs +++ /dev/null @@ -1,175 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for `pallet_drop3` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-17, STEPS: `20`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `parachain-benchmark`, CPU: `Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("litentry-dev"), DB CACHE: 20 - -// Executed Command: -// ./litentry-collator -// benchmark -// pallet -// --chain=litentry-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_drop3 -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --output=./runtime/litentry/src/weights/pallet_drop3.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_drop3`. -pub struct WeightInfo(PhantomData); -impl pallet_drop3::WeightInfo for WeightInfo { - /// Storage: Drop3 Admin (r:1 w:1) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn set_admin() -> Weight { - // Proof Size summary in bytes: - // Measured: `4` - // Estimated: `1489` - // Minimum execution time: 13_718_000 picoseconds. - Weight::from_parts(14_166_000, 0) - .saturating_add(Weight::from_parts(0, 1489)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - fn approve_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_750_000 picoseconds. - Weight::from_parts(23_174_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn reject_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 72_798_000 picoseconds. - Weight::from_parts(73_833_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn start_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_765_000 picoseconds. - Weight::from_parts(23_419_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn stop_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_772_000 picoseconds. - Weight::from_parts(25_459_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn close_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 46_124_000 picoseconds. - Weight::from_parts(47_229_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 CurrentMaxPoolId (r:1 w:1) - /// Proof Skipped: Drop3 CurrentMaxPoolId (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:0 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// The range of component `n` is `[0, 16]`. - fn propose_reward_pool(_n: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `105` - // Estimated: `3593` - // Minimum execution time: 41_888_000 picoseconds. - Weight::from_parts(43_378_625, 0) - .saturating_add(Weight::from_parts(0, 3593)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:2 w:2) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn send_reward() -> Weight { - // Proof Size summary in bytes: - // Measured: `449` - // Estimated: `6196` - // Minimum execution time: 49_512_000 picoseconds. - Weight::from_parts(50_424_000, 0) - .saturating_add(Weight::from_parts(0, 6196)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } -} diff --git a/runtime/litmus/src/lib.rs b/runtime/litmus/src/lib.rs index 34f5b08997..c18d7fad3e 100644 --- a/runtime/litmus/src/lib.rs +++ b/runtime/litmus/src/lib.rs @@ -778,20 +778,6 @@ impl pallet_bridge_transfer::Config for Runtime { type WeightInfo = weights::pallet_bridge_transfer::WeightInfo; } -parameter_types! { - pub const SlashPercent: Percent = Percent::from_percent(20); -} - -impl pallet_drop3::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type PoolId = u64; - type SetAdminOrigin = EnsureRootOrHalfCouncil; - type Currency = Balances; - type WeightInfo = weights::pallet_drop3::WeightInfo; - type SlashPercent = SlashPercent; - type MaximumNameLength = ConstU32<16>; -} - impl pallet_extrinsic_filter::Config for Runtime { type RuntimeEvent = RuntimeEvent; type UpdateOrigin = EnsureRootOrHalfTechnicalCommittee; @@ -894,7 +880,6 @@ construct_runtime! { // Litmus pallets ChainBridge: pallet_bridge = 60, BridgeTransfer: pallet_bridge_transfer = 61, - Drop3: pallet_drop3 = 62, ExtrinsicFilter: pallet_extrinsic_filter = 63, AssetManager: pallet_asset_manager = 65, @@ -984,7 +969,6 @@ mod benches { [pallet_proxy, Proxy] [pallet_membership, CouncilMembership] [pallet_multisig, Multisig] - [pallet_drop3, Drop3] [pallet_extrinsic_filter, ExtrinsicFilter] [pallet_scheduler, Scheduler] [pallet_preimage, Preimage] diff --git a/runtime/litmus/src/weights/mod.rs b/runtime/litmus/src/weights/mod.rs index 4dd3450d13..b21dff635a 100644 --- a/runtime/litmus/src/weights/mod.rs +++ b/runtime/litmus/src/weights/mod.rs @@ -25,7 +25,6 @@ pub mod pallet_bridge_transfer; pub mod pallet_collator_selection; pub mod pallet_collective; pub mod pallet_democracy; -pub mod pallet_drop3; pub mod pallet_extrinsic_filter; pub mod pallet_identity; pub mod pallet_membership; diff --git a/runtime/litmus/src/weights/pallet_drop3.rs b/runtime/litmus/src/weights/pallet_drop3.rs deleted file mode 100644 index 222c028456..0000000000 --- a/runtime/litmus/src/weights/pallet_drop3.rs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for `pallet_drop3` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-17, STEPS: `20`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `parachain-benchmark`, CPU: `Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("litmus-dev"), DB CACHE: 20 - -// Executed Command: -// ./litentry-collator -// benchmark -// pallet -// --chain=litmus-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_drop3 -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --output=./runtime/litmus/src/weights/pallet_drop3.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_drop3`. -pub struct WeightInfo(PhantomData); -impl pallet_drop3::WeightInfo for WeightInfo { - /// Storage: Drop3 Admin (r:1 w:1) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn set_admin() -> Weight { - // Proof Size summary in bytes: - // Measured: `4` - // Estimated: `1489` - // Minimum execution time: 13_429_000 picoseconds. - Weight::from_parts(14_049_000, 0) - .saturating_add(Weight::from_parts(0, 1489)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - fn approve_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_492_000 picoseconds. - Weight::from_parts(23_602_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn reject_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 72_618_000 picoseconds. - Weight::from_parts(73_440_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn start_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_769_000 picoseconds. - Weight::from_parts(23_335_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn stop_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_458_000 picoseconds. - Weight::from_parts(22_907_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn close_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 45_729_000 picoseconds. - Weight::from_parts(46_801_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 CurrentMaxPoolId (r:1 w:1) - /// Proof Skipped: Drop3 CurrentMaxPoolId (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:0 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// The range of component `n` is `[0, 16]`. - fn propose_reward_pool(n: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `105` - // Estimated: `3593` - // Minimum execution time: 40_985_000 picoseconds. - Weight::from_parts(42_517_928, 0) - .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 9_347 - .saturating_add(Weight::from_parts(31_854, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:2 w:2) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn send_reward() -> Weight { - // Proof Size summary in bytes: - // Measured: `449` - // Estimated: `6196` - // Minimum execution time: 48_658_000 picoseconds. - Weight::from_parts(49_698_000, 0) - .saturating_add(Weight::from_parts(0, 6196)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } -} diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index fe51e24651..1bbb2866b6 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -969,20 +969,6 @@ impl pallet_bridge_transfer::Config for Runtime { type WeightInfo = weights::pallet_bridge_transfer::WeightInfo; } -parameter_types! { - pub const SlashPercent: Percent = Percent::from_percent(20); -} - -impl pallet_drop3::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type PoolId = u64; - type SetAdminOrigin = EnsureRootOrHalfCouncil; - type Currency = Balances; - type WeightInfo = weights::pallet_drop3::WeightInfo; - type SlashPercent = SlashPercent; - type MaximumNameLength = ConstU32<16>; -} - impl pallet_extrinsic_filter::Config for Runtime { type RuntimeEvent = RuntimeEvent; type UpdateOrigin = EnsureRootOrHalfTechnicalCommittee; @@ -1257,7 +1243,6 @@ construct_runtime! { // Rococo pallets ChainBridge: pallet_bridge = 60, BridgeTransfer: pallet_bridge_transfer = 61, - Drop3: pallet_drop3 = 62, ExtrinsicFilter: pallet_extrinsic_filter = 63, IdentityManagement: pallet_identity_management = 64, AssetManager: pallet_asset_manager = 65, @@ -1388,7 +1373,6 @@ mod benches { [pallet_proxy, Proxy] [pallet_membership, CouncilMembership] [pallet_multisig, Multisig] - [pallet_drop3, Drop3] [paleet_evm, EVM] [pallet_extrinsic_filter, ExtrinsicFilter] [pallet_scheduler, Scheduler] diff --git a/runtime/rococo/src/weights/mod.rs b/runtime/rococo/src/weights/mod.rs index 47af2c4cc6..f17dcf17eb 100644 --- a/runtime/rococo/src/weights/mod.rs +++ b/runtime/rococo/src/weights/mod.rs @@ -24,7 +24,6 @@ pub mod pallet_bridge; pub mod pallet_bridge_transfer; pub mod pallet_collective; pub mod pallet_democracy; -pub mod pallet_drop3; pub mod pallet_evm; pub mod pallet_extrinsic_filter; pub mod pallet_identity; diff --git a/runtime/rococo/src/weights/pallet_drop3.rs b/runtime/rococo/src/weights/pallet_drop3.rs deleted file mode 100644 index 4a9cbee7ca..0000000000 --- a/runtime/rococo/src/weights/pallet_drop3.rs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for `pallet_drop3` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-17, STEPS: `20`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `parachain-benchmark`, CPU: `Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 20 - -// Executed Command: -// ./litentry-collator -// benchmark -// pallet -// --chain=rococo-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_drop3 -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --output=./runtime/rococo/src/weights/pallet_drop3.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_drop3`. -pub struct WeightInfo(PhantomData); -impl pallet_drop3::WeightInfo for WeightInfo { - /// Storage: Drop3 Admin (r:1 w:1) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn set_admin() -> Weight { - // Proof Size summary in bytes: - // Measured: `4` - // Estimated: `1489` - // Minimum execution time: 14_412_000 picoseconds. - Weight::from_parts(14_711_000, 0) - .saturating_add(Weight::from_parts(0, 1489)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - fn approve_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_259_000 picoseconds. - Weight::from_parts(22_808_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn reject_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 71_555_000 picoseconds. - Weight::from_parts(72_466_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn start_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 22_465_000 picoseconds. - Weight::from_parts(22_907_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - fn stop_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `264` - // Estimated: `3729` - // Minimum execution time: 21_132_000 picoseconds. - Weight::from_parts(21_632_000, 0) - .saturating_add(Weight::from_parts(0, 3729)) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 Admin (r:1 w:0) - /// Proof Skipped: Drop3 Admin (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - fn close_reward_pool() -> Weight { - // Proof Size summary in bytes: - // Measured: `365` - // Estimated: `3830` - // Minimum execution time: 44_315_000 picoseconds. - Weight::from_parts(45_012_000, 0) - .saturating_add(Weight::from_parts(0, 3830)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } - /// Storage: System Account (r:1 w:1) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - /// Storage: Drop3 CurrentMaxPoolId (r:1 w:1) - /// Proof Skipped: Drop3 CurrentMaxPoolId (max_values: Some(1), max_size: None, mode: Measured) - /// Storage: Drop3 RewardPoolOwners (r:0 w:1) - /// Proof Skipped: Drop3 RewardPoolOwners (max_values: None, max_size: None, mode: Measured) - /// Storage: Drop3 RewardPools (r:0 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// The range of component `n` is `[0, 16]`. - fn propose_reward_pool(n: u32, ) -> Weight { - // Proof Size summary in bytes: - // Measured: `105` - // Estimated: `3593` - // Minimum execution time: 40_590_000 picoseconds. - Weight::from_parts(41_673_286, 0) - .saturating_add(Weight::from_parts(0, 3593)) - // Standard Error: 10_395 - .saturating_add(Weight::from_parts(154_934, 0).saturating_mul(n.into())) - .saturating_add(T::DbWeight::get().reads(2)) - .saturating_add(T::DbWeight::get().writes(4)) - } - /// Storage: Drop3 RewardPools (r:1 w:1) - /// Proof Skipped: Drop3 RewardPools (max_values: None, max_size: None, mode: Measured) - /// Storage: System Account (r:2 w:2) - /// Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) - fn send_reward() -> Weight { - // Proof Size summary in bytes: - // Measured: `449` - // Estimated: `6196` - // Minimum execution time: 48_032_000 picoseconds. - Weight::from_parts(50_660_000, 0) - .saturating_add(Weight::from_parts(0, 6196)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(3)) - } -} diff --git a/tee-worker/cli/lit_set_heartbeat_timeout.sh b/tee-worker/cli/lit_set_heartbeat_timeout.sh deleted file mode 100755 index 7c806480c4..0000000000 --- a/tee-worker/cli/lit_set_heartbeat_timeout.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -# Copyright 2020-2024 Trust Computing GmbH. - -while getopts ":p:A:B:u:W:V:C:" opt; do - case $opt in - p) - NPORT=$OPTARG - ;; - A) - WORKER1PORT=$OPTARG - ;; - B) - WORKER2PORT=$OPTARG - ;; - u) - NODEURL=$OPTARG - ;; - V) - WORKER1URL=$OPTARG - ;; - W) - WORKER2URL=$OPTARG - ;; - C) - CLIENT_BIN=$OPTARG - ;; - esac -done - -# Using default port if none given as arguments. -NPORT=${NPORT:-9944} -NODEURL=${NODEURL:-"ws://127.0.0.1"} - -WORKER1PORT=${WORKER1PORT:-2000} -WORKER1URL=${WORKER1URL:-"wss://127.0.0.1"} - -CLIENT_BIN=${CLIENT_BIN:-"../bin/litentry-cli"} - -LOG_FOLDER="./../log" - -echo "Using client binary $CLIENT_BIN" -echo "Using node uri $NODEURL:$NPORT" -echo "Using trusted-worker uri $WORKER1URL:$WORKER1PORT" -echo "" - -TIMEOUT=5000 # 5 seconds, smaller than 12s (the block duration) - -CLIENT="$CLIENT_BIN -p $NPORT -P $WORKER1PORT -u $NODEURL -U $WORKER1URL" -echo "CLIENT is: $CLIENT" - -echo "* Query on-chain enclave registry:" -WORKERS=$($CLIENT list-workers) -echo "WORKERS: " -echo "${WORKERS}" -echo "" - -if [ "$READMRENCLAVE" = "file" ] -then - read MRENCLAVE <<< $(cat ~/mrenclave.b58) - echo "Reading MRENCLAVE from file: ${MRENCLAVE}" -else - # This will always take the first MRENCLAVE found in the registry !! - read MRENCLAVE <<< $(echo "$WORKERS" | awk '/ MRENCLAVE: / { print $2; exit }') - echo "Reading MRENCLAVE from worker list: ${MRENCLAVE}" -fi -[[ -z $MRENCLAVE ]] && { echo "MRENCLAVE is empty. cannot continue" ; exit 1; } - - -# indirect call that will be sent to the parachain, it will be synchronously handled -sleep 10 -echo "* Set heartbeat timeout to $TIMEOUT" -${CLIENT} set-heartbeat-timeout "$TIMEOUT" -echo "" - -sleep 120 - -read MRENCLAVE <<< $($CLIENT list-workers | awk '/ MRENCLAVE: / { print $2; exit }') -if [[ -z $MRENCLAVE ]] -then - echo "All workers removed, test passed" -else - echo "Worker(s) still exist(s), test fail" - exit 1 -fi diff --git a/tee-worker/cli/src/base_cli/commands/litentry/mod.rs b/tee-worker/cli/src/base_cli/commands/litentry/mod.rs index 71f772d2ed..edbda2b23a 100644 --- a/tee-worker/cli/src/base_cli/commands/litentry/mod.rs +++ b/tee-worker/cli/src/base_cli/commands/litentry/mod.rs @@ -16,7 +16,6 @@ pub mod id_graph_hash; pub mod link_identity; -pub mod set_heartbeat_timeout; // TODO: maybe move it to use itp_node_api::api_client pub const IMP: &str = "IdentityManagement"; diff --git a/tee-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs b/tee-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs deleted file mode 100644 index 996b369a59..0000000000 --- a/tee-worker/cli/src/base_cli/commands/litentry/set_heartbeat_timeout.rs +++ /dev/null @@ -1,54 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use crate::{command_utils::get_chain_api, Cli}; - -use crate::{CliResult, CliResultOk}; -use itp_node_api::api_client::TEEREX; -use log::*; -use sp_keyring::AccountKeyring; -use substrate_api_client::{ac_compose_macros::compose_extrinsic, SubmitAndWatch, XtStatus}; -#[derive(Parser)] -pub struct SetHeartbeatTimeoutCommand { - /// Heartbeat timeout - timeout: u64, -} - -impl SetHeartbeatTimeoutCommand { - pub(crate) fn run(&self, cli: &Cli) -> CliResult { - let mut chain_api = get_chain_api(cli); - - // has to be //Alice as this is the genesis admin for teerex pallet, - // otherwise `set_heartbeat_timeout` call won't work - chain_api.set_signer(AccountKeyring::Alice.pair().into()); - - // call set_heartbeat_timeout - let xt = compose_extrinsic!( - chain_api, - TEEREX, - "set_heartbeat_timeout", - codec::Compact(self.timeout) - ); - - let tx_hash = chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).unwrap(); - println!( - "[+] SetHeartbeatTimeoutCommand TrustedOperation got finalized. Hash: {:?}\n", - tx_hash - ); - - Ok(CliResultOk::None) - } -} diff --git a/tee-worker/cli/src/base_cli/mod.rs b/tee-worker/cli/src/base_cli/mod.rs index a42888e4bf..23de8b50f9 100644 --- a/tee-worker/cli/src/base_cli/mod.rs +++ b/tee-worker/cli/src/base_cli/mod.rs @@ -20,10 +20,7 @@ use crate::{ balance::BalanceCommand, faucet::FaucetCommand, listen::ListenCommand, - litentry::{ - id_graph_hash::IDGraphHashCommand, link_identity::LinkIdentityCommand, - set_heartbeat_timeout::SetHeartbeatTimeoutCommand, - }, + litentry::{id_graph_hash::IDGraphHashCommand, link_identity::LinkIdentityCommand}, register_tcb_info::RegisterTcbInfoCommand, shield_funds::ShieldFundsCommand, transfer::TransferCommand, @@ -91,9 +88,6 @@ pub enum BaseCommand { /// create idenity graph LinkIdentity(LinkIdentityCommand), - /// set heartbeat timeout storage - SetHeartbeatTimeout(SetHeartbeatTimeoutCommand), - /// get the IDGraph hash of the given identity IDGraphHash(IDGraphHashCommand), } @@ -115,7 +109,6 @@ impl BaseCommand { // Litentry's commands below BaseCommand::PrintSgxMetadataRaw => print_sgx_metadata_raw(cli), BaseCommand::LinkIdentity(cmd) => cmd.run(cli), - BaseCommand::SetHeartbeatTimeout(cmd) => cmd.run(cli), BaseCommand::IDGraphHash(cmd) => cmd.run(cli), } } diff --git a/tee-worker/core-primitives/test/src/builders/enclave_gen_builder.rs b/tee-worker/core-primitives/test/src/builders/enclave_gen_builder.rs deleted file mode 100644 index 85e807c628..0000000000 --- a/tee-worker/core-primitives/test/src/builders/enclave_gen_builder.rs +++ /dev/null @@ -1,63 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use itp_time_utils::now_as_millis; -use itp_types::{Enclave, PalletString}; - -/// Builder for a generic enclave (`EnclaveGen`) struct. -pub struct EnclaveGenBuilder { - pubkey: AccountId, - mr_enclave: [u8; 32], - timestamp: u64, - url: PalletString, // utf8 encoded url -} - -impl Default for EnclaveGenBuilder -where - AccountId: Default, -{ - fn default() -> Self { - EnclaveGenBuilder { - pubkey: AccountId::default(), - mr_enclave: [0u8; 32], - timestamp: now_as_millis(), - url: PalletString::default(), - } - } -} - -impl EnclaveGenBuilder { - pub fn with_account(mut self, account: AccountId) -> Self { - self.pubkey = account; - self - } - - pub fn with_url(mut self, url: PalletString) -> Self { - self.url = url; - self - } - - pub fn build(self) -> EnclaveGen { - EnclaveGen { - pubkey: self.pubkey, - mr_enclave: self.mr_enclave, - timestamp: self.timestamp, - url: self.url, - } - } -} diff --git a/tee-worker/core-primitives/test/src/builders/mod.rs b/tee-worker/core-primitives/test/src/builders/mod.rs deleted file mode 100644 index 610066f015..0000000000 --- a/tee-worker/core-primitives/test/src/builders/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -//! Builder patterns for common structs used in tests. - -pub mod enclave_gen_builder; diff --git a/tee-worker/core-primitives/types/src/lib.rs b/tee-worker/core-primitives/types/src/lib.rs index e04dcaf02e..882265fae2 100644 --- a/tee-worker/core-primitives/types/src/lib.rs +++ b/tee-worker/core-primitives/types/src/lib.rs @@ -21,21 +21,12 @@ use crate::storage::StorageEntry; use codec::{Decode, Encode}; use itp_sgx_crypto::ShieldingCryptoDecrypt; -use litentry_primitives::{decl_rsa_request, RequestAesKeyNonce}; +use litentry_primitives::decl_rsa_request; use sp_std::{boxed::Box, fmt::Debug, vec::Vec}; pub mod parentchain; pub mod storage; -/// Substrate runtimes provide no string type. Hence, for arbitrary data of varying length the -/// `Vec` is used. In the polkadot-js the typedef `Text` is used to automatically -/// utf8 decode bytes into a string. -#[cfg(not(feature = "std"))] -pub type PalletString = Vec; - -#[cfg(feature = "std")] -pub type PalletString = String; - pub use itp_sgx_runtime_primitives::types::*; pub use litentry_primitives::{ Assertion, AttestationType, DecryptableRequest, Enclave, EnclaveFingerprint, MrEnclave, @@ -46,27 +37,7 @@ pub use sp_core::{crypto::AccountId32 as AccountId, H256}; pub type IpfsHash = [u8; 46]; pub type CallIndex = [u8; 2]; -// pallet teerex -pub type ConfirmCallFn = (CallIndex, ShardIdentifier, H256, Vec); -pub type ShieldFundsFn = (CallIndex, Vec, Balance, ShardIdentifier); -pub type CallWorkerFn = (CallIndex, RsaRequest); - -pub type SetScheduledEnclaveFn = (CallIndex, SidechainBlockNumber, MrEnclave); -pub type RemoveScheduledEnclaveFn = (CallIndex, SidechainBlockNumber); - -// pallet IMP -pub type LinkIdentityParams = (ShardIdentifier, AccountId, Vec, Vec, RequestAesKeyNonce); -pub type LinkIdentityFn = (CallIndex, LinkIdentityParams); - -pub type DeactivateIdentityParams = (ShardIdentifier, Vec); -pub type DeactivateIdentityFn = (CallIndex, DeactivateIdentityParams); - -pub type ActivateIdentityParams = (ShardIdentifier, Vec); -pub type ActivateIdentityFn = (CallIndex, DeactivateIdentityParams); - -// pallet VCMP -pub type RequestVCParams = (ShardIdentifier, Assertion); -pub type RequestVCFn = (CallIndex, RequestVCParams); +pub type PostOpaqueTaskFn = (CallIndex, RsaRequest); /// Simple blob to hold an encoded call #[derive(Debug, PartialEq, Eq, Clone, Default)] diff --git a/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs b/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs index 7132b0bdeb..aae0dcc240 100644 --- a/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs +++ b/tee-worker/core/parentchain/indirect-calls-executor/src/executor.rs @@ -320,7 +320,7 @@ mod test { stf_mock::{GetterMock, TrustedCallSignedMock}, }; use itp_top_pool_author::mocks::AuthorApiMock; - use itp_types::{Block, CallWorkerFn, RsaRequest, ShardIdentifier, ShieldFundsFn}; + use itp_types::{Block, PostOpaqueTaskFn, RsaRequest, ShardIdentifier}; use sp_core::{ed25519, Pair}; use sp_runtime::{MultiAddress, MultiSignature, OpaqueExtrinsic}; use std::assert_matches::assert_matches; @@ -411,12 +411,12 @@ mod test { assert_ne!(call.0, zero_root_call); } - fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { + fn invoke_unchecked_extrinsic() -> ParentchainUncheckedExtrinsic { let request = RsaRequest::new(shard_id(), vec![1u8, 2u8]); let dummy_metadata = NodeMetadataMock::new(); let call_worker_indexes = dummy_metadata.post_opaque_task_call_indexes().unwrap(); - ParentchainUncheckedExtrinsic::::new_signed( + ParentchainUncheckedExtrinsic::::new_signed( (call_worker_indexes, request), MultiAddress::Address32([1u8; 32]), MultiSignature::Ed25519(default_signature()), diff --git a/tee-worker/docker/lit-set-heartbeat-timeout.yml b/tee-worker/docker/lit-set-heartbeat-timeout.yml deleted file mode 100644 index 2dc293fbc8..0000000000 --- a/tee-worker/docker/lit-set-heartbeat-timeout.yml +++ /dev/null @@ -1,24 +0,0 @@ -services: - lit-set-heartbeat-timeout: - image: litentry/litentry-cli:latest - container_name: litentry-set-heartbeat-timeout - volumes: - - ../cli:/usr/local/worker-cli - build: - context: .. - dockerfile: build.Dockerfile - target: deployed-client - depends_on: - litentry-node: - condition: service_healthy - litentry-worker-1: - condition: service_healthy - networks: - - litentry-test-network - entrypoint: - "/usr/local/worker-cli/lit_set_heartbeat_timeout.sh -p 9912 -u ws://litentry-node - -V wss://litentry-worker-1 -A 2011 -W wss://litentry-worker-2 -B 2012 -C /usr/local/bin/litentry-cli 2>&1" - restart: "no" -networks: - litentry-test-network: - driver: bridge \ No newline at end of file diff --git a/tee-worker/enclave-runtime/src/test/top_pool_tests.rs b/tee-worker/enclave-runtime/src/test/top_pool_tests.rs index 21c86ce3a2..f7ecff84b7 100644 --- a/tee-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/tee-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -59,7 +59,7 @@ use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, traits::AuthorApi, }; -use itp_types::{parentchain::Address, Block, CallWorkerFn, RsaRequest, ShardIdentifier, H256}; +use itp_types::{parentchain::Address, Block, RsaRequest, ShardIdentifier, H256}; use jsonrpc_core::futures::executor; use litentry_primitives::Identity; use log::*; @@ -197,6 +197,7 @@ fn encrypted_indirect_call< encrypt_trusted_operation(shielding_key, &trusted_operation) } +/* fn create_opaque_call_extrinsic( _shard: ShardIdentifier, _shielding_key: &ShieldingKey, @@ -231,3 +232,5 @@ fn create_opaque_call_extrinsic( .with_extrinsics(vec![opaque_extrinsic]) .build() } + +*/ From 8d3fcd75834a747d42af1519de8424aee908d66d Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Wed, 14 Feb 2024 11:42:33 +0100 Subject: [PATCH 34/64] Remove teeracle related code (#2488) * remove teeracle * remove teeracle * remove teeracle feature * remove extra edl * fix clippy --- Cargo.lock | 64 +-- Cargo.toml | 3 - bitacross-worker/Cargo.lock | 34 -- .../core-primitives/settings/src/lib.rs | 3 - .../parentchain/integritee_parachain.rs | 1 - .../parentchain/integritee_solochain.rs | 1 - .../parentchain/target_a_parachain.rs | 1 - .../parentchain/target_a_solochain.rs | 1 - .../parentchain/target_b_parachain.rs | 1 - .../parentchain/target_b_solochain.rs | 1 - .../src/tls_ra/tls_ra_server.rs | 1 - bitacross-worker/service/src/main_impl.rs | 1 - bitacross-worker/service/src/teeracle/mod.rs | 142 ----- pallets/teebag/src/types.rs | 1 - pallets/teeracle/Cargo.toml | 75 --- pallets/teeracle/README.md | 3 - pallets/teeracle/src/benchmarking.rs | 160 ------ pallets/teeracle/src/lib.rs | 260 --------- pallets/teeracle/src/mock.rs | 176 ------ pallets/teeracle/src/tests.rs | 524 ------------------ pallets/teeracle/src/weights.rs | 124 ----- primitives/common/Cargo.toml | 19 - primitives/common/src/lib.rs | 46 -- primitives/teeracle/Cargo.toml | 23 - primitives/teeracle/src/lib.rs | 12 - runtime/litmus/Cargo.toml | 4 - runtime/litmus/src/lib.rs | 10 - runtime/litmus/src/weights/mod.rs | 1 - runtime/litmus/src/weights/pallet_teeracle.rs | 112 ---- runtime/rococo/Cargo.toml | 4 - runtime/rococo/src/lib.rs | 11 - runtime/rococo/src/weights/mod.rs | 1 - runtime/rococo/src/weights/pallet_teeracle.rs | 112 ---- scripts/pre-commit.sh | 1 - tee-worker/Cargo.lock | 53 -- tee-worker/Cargo.toml | 22 - tee-worker/app-libs/oracle/Cargo.toml | 48 -- .../src/certificates/amazon_root_ca_a.pem | 20 - .../baltimore_cyber_trust_root_v3.pem | 21 - .../certificates/lets_encrypt_root_cert.pem | 31 -- .../src/certificates/open_meteo_root.pem | 31 -- tee-worker/app-libs/oracle/src/error.rs | 39 -- tee-worker/app-libs/oracle/src/lib.rs | 84 --- .../app-libs/oracle/src/metrics_exporter.rs | 104 ---- tee-worker/app-libs/oracle/src/mock.rs | 120 ---- .../oracle/src/oracle_sources/coin_gecko.rs | 220 -------- .../src/oracle_sources/coin_market_cap.rs | 242 -------- .../app-libs/oracle/src/oracle_sources/mod.rs | 19 - .../oracle_sources/weather_oracle_source.rs | 120 ---- .../src/oracles/exchange_rate_oracle.rs | 154 ----- tee-worker/app-libs/oracle/src/oracles/mod.rs | 18 - .../oracle/src/oracles/weather_oracle.rs | 83 --- tee-worker/app-libs/oracle/src/test.rs | 125 ----- tee-worker/app-libs/oracle/src/traits.rs | 55 -- tee-worker/app-libs/oracle/src/types.rs | 61 -- tee-worker/cli/Cargo.toml | 1 - tee-worker/cli/src/commands.rs | 13 - tee-worker/cli/src/lib.rs | 3 - .../src/oracle/commands/add_to_whitelist.rs | 67 --- .../src/oracle/commands/listen_to_exchange.rs | 79 --- .../src/oracle/commands/listen_to_oracle.rs | 91 --- tee-worker/cli/src/oracle/commands/mod.rs | 25 - tee-worker/cli/src/oracle/mod.rs | 49 -- .../enclave-api/ffi/src/lib.rs | 24 - .../core-primitives/enclave-api/src/lib.rs | 1 - .../enclave-api/src/teeracle_api.rs | 114 ---- .../enclave-metrics/src/lib.rs | 23 - .../node-api/api-client-extensions/src/lib.rs | 3 - .../src/pallet_teeracle.rs | 19 - .../node-api/metadata/src/pallet_teeracle.rs | 46 -- .../core-primitives/settings/Cargo.toml | 1 - .../core-primitives/settings/src/lib.rs | 24 +- .../settings/src/worker_mode.rs | 9 +- .../top-pool-author/Cargo.toml | 1 - .../top-pool-author/src/author.rs | 9 +- tee-worker/enclave-runtime/Cargo.lock | 18 - tee-worker/enclave-runtime/Cargo.toml | 6 - tee-worker/enclave-runtime/Enclave.edl | 14 - tee-worker/enclave-runtime/src/empty_impls.rs | 32 -- .../parentchain/integritee_parachain.rs | 2 - .../parentchain/integritee_solochain.rs | 2 - .../parentchain/target_a_parachain.rs | 2 - .../parentchain/target_a_solochain.rs | 2 - .../parentchain/target_b_parachain.rs | 2 - .../parentchain/target_b_solochain.rs | 2 - tee-worker/enclave-runtime/src/lib.rs | 9 +- .../enclave-runtime/src/teeracle/mod.rs | 279 ---------- tee-worker/enclave-runtime/src/test/mod.rs | 3 - .../src/test/teeracle_tests.rs | 50 -- .../enclave-runtime/src/test/tests_main.rs | 14 - .../src/tls_ra/tls_ra_server.rs | 3 +- tee-worker/service/Cargo.toml | 1 - tee-worker/service/src/cli.yml | 11 - tee-worker/service/src/config.rs | 49 +- tee-worker/service/src/main.rs | 2 - tee-worker/service/src/main_impl.rs | 52 +- tee-worker/service/src/prometheus_metrics.rs | 9 - tee-worker/service/src/teeracle/mod.rs | 142 ----- .../service/src/teeracle/schedule_periodic.rs | 46 -- .../service/src/teeracle/teeracle_metrics.rs | 76 --- 100 files changed, 20 insertions(+), 5013 deletions(-) delete mode 100644 bitacross-worker/service/src/teeracle/mod.rs delete mode 100644 pallets/teeracle/Cargo.toml delete mode 100644 pallets/teeracle/README.md delete mode 100644 pallets/teeracle/src/benchmarking.rs delete mode 100644 pallets/teeracle/src/lib.rs delete mode 100644 pallets/teeracle/src/mock.rs delete mode 100644 pallets/teeracle/src/tests.rs delete mode 100644 pallets/teeracle/src/weights.rs delete mode 100644 primitives/common/Cargo.toml delete mode 100644 primitives/common/src/lib.rs delete mode 100644 primitives/teeracle/Cargo.toml delete mode 100644 primitives/teeracle/src/lib.rs delete mode 100644 runtime/litmus/src/weights/pallet_teeracle.rs delete mode 100644 runtime/rococo/src/weights/pallet_teeracle.rs delete mode 100644 tee-worker/app-libs/oracle/Cargo.toml delete mode 100644 tee-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem delete mode 100644 tee-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem delete mode 100644 tee-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem delete mode 100644 tee-worker/app-libs/oracle/src/certificates/open_meteo_root.pem delete mode 100644 tee-worker/app-libs/oracle/src/error.rs delete mode 100644 tee-worker/app-libs/oracle/src/lib.rs delete mode 100644 tee-worker/app-libs/oracle/src/metrics_exporter.rs delete mode 100644 tee-worker/app-libs/oracle/src/mock.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracle_sources/mod.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracles/mod.rs delete mode 100644 tee-worker/app-libs/oracle/src/oracles/weather_oracle.rs delete mode 100644 tee-worker/app-libs/oracle/src/test.rs delete mode 100644 tee-worker/app-libs/oracle/src/traits.rs delete mode 100644 tee-worker/app-libs/oracle/src/types.rs delete mode 100644 tee-worker/cli/src/oracle/commands/add_to_whitelist.rs delete mode 100644 tee-worker/cli/src/oracle/commands/listen_to_exchange.rs delete mode 100644 tee-worker/cli/src/oracle/commands/listen_to_oracle.rs delete mode 100644 tee-worker/cli/src/oracle/commands/mod.rs delete mode 100644 tee-worker/cli/src/oracle/mod.rs delete mode 100644 tee-worker/core-primitives/enclave-api/src/teeracle_api.rs delete mode 100644 tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs delete mode 100644 tee-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs delete mode 100644 tee-worker/enclave-runtime/src/teeracle/mod.rs delete mode 100644 tee-worker/enclave-runtime/src/test/teeracle_tests.rs delete mode 100644 tee-worker/service/src/teeracle/mod.rs delete mode 100644 tee-worker/service/src/teeracle/schedule_periodic.rs delete mode 100644 tee-worker/service/src/teeracle/teeracle_metrics.rs diff --git a/Cargo.lock b/Cargo.lock index fb4cb29b08..46b2331249 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1116,13 +1116,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "common-primitives" -version = "0.1.0" -dependencies = [ - "sp-std", -] - [[package]] name = "concurrent-queue" version = "2.2.0" @@ -5590,7 +5583,6 @@ dependencies = [ "pallet-scheduler", "pallet-session", "pallet-sidechain", - "pallet-teeracle", "pallet-teerex", "pallet-timestamp", "pallet-transaction-payment", @@ -7682,7 +7674,7 @@ dependencies = [ "sp-runtime", "sp-staking", "sp-std", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed)", + "substrate-fixed", ] [[package]] @@ -7994,31 +7986,6 @@ dependencies = [ "x509-cert", ] -[[package]] -name = "pallet-teeracle" -version = "0.1.0" -dependencies = [ - "frame-benchmarking", - "frame-support", - "frame-system", - "hex-literal 0.4.1", - "log", - "pallet-balances", - "pallet-teerex", - "pallet-timestamp", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-externalities", - "sp-io", - "sp-keyring", - "sp-runtime", - "sp-std", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed.git?tag=v0.5.9)", - "teeracle-primitives", - "test-utils", -] - [[package]] name = "pallet-teerex" version = "0.9.0" @@ -10602,7 +10569,6 @@ dependencies = [ "pallet-sidechain", "pallet-sudo", "pallet-teebag", - "pallet-teeracle", "pallet-teerex", "pallet-timestamp", "pallet-tips", @@ -13630,17 +13596,6 @@ dependencies = [ "platforms 2.0.0", ] -[[package]] -name = "substrate-fixed" -version = "0.5.9" -source = "git+https://github.com/encointer/substrate-fixed.git?tag=v0.5.9#a4fb461aae6205ffc55bed51254a40c52be04e5d" -dependencies = [ - "parity-scale-codec", - "scale-info", - "serde", - "typenum 1.16.0 (git+https://github.com/encointer/typenum?tag=v1.16.0)", -] - [[package]] name = "substrate-fixed" version = "0.5.9" @@ -13813,14 +13768,6 @@ version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" -[[package]] -name = "teeracle-primitives" -version = "0.1.0" -dependencies = [ - "common-primitives", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed.git?tag=v0.5.9)", -] - [[package]] name = "teerex-primitives" version = "0.1.0" @@ -14488,15 +14435,6 @@ dependencies = [ "scale-info", ] -[[package]] -name = "typenum" -version = "1.16.0" -source = "git+https://github.com/encointer/typenum?tag=v1.16.0#4c8dddaa8bdd13130149e43b4085ad14e960617f" -dependencies = [ - "parity-scale-codec", - "scale-info", -] - [[package]] name = "ucd-trie" version = "0.1.6" diff --git a/Cargo.toml b/Cargo.toml index 636b081f91..f45978d303 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ members = [ 'pallets/vc-management', 'pallets/sidechain', 'pallets/teebag', - 'pallets/teeracle', 'pallets/teerex', 'pallets/teerex/sgx-verify', 'pallets/parentchain', @@ -20,13 +19,11 @@ members = [ 'pallets/group', 'pallets/bitacross', 'precompiles/*', - 'primitives/common', 'primitives/core', 'primitives/core/proc-macros', 'primitives/core/macros', 'primitives/hex', 'primitives/sidechain', - 'primitives/teeracle', 'primitives/teerex', 'runtime/litentry', 'runtime/litmus', diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 4121e7a289..7ce5e2fbdd 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -1426,13 +1426,6 @@ dependencies = [ "thiserror 1.0.44", ] -[[package]] -name = "common-primitives" -version = "0.1.0" -dependencies = [ - "sp-std 5.0.0", -] - [[package]] name = "concurrent-queue" version = "2.2.0" @@ -9130,24 +9123,6 @@ dependencies = [ "x509-cert", ] -[[package]] -name = "pallet-teeracle" -version = "0.1.0" -dependencies = [ - "frame-support", - "frame-system", - "log 0.4.20", - "pallet-teerex", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", - "sp-std 5.0.0", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", - "teeracle-primitives", -] - [[package]] name = "pallet-teerex" version = "0.9.0" @@ -11003,7 +10978,6 @@ dependencies = [ "pallet-sidechain", "pallet-sudo", "pallet-teebag", - "pallet-teeracle", "pallet-teerex", "pallet-timestamp", "pallet-tips", @@ -14225,14 +14199,6 @@ version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" -[[package]] -name = "teeracle-primitives" -version = "0.1.0" -dependencies = [ - "common-primitives", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", -] - [[package]] name = "teerex-primitives" version = "0.1.0" diff --git a/bitacross-worker/core-primitives/settings/src/lib.rs b/bitacross-worker/core-primitives/settings/src/lib.rs index 22989cda90..401dc2904c 100644 --- a/bitacross-worker/core-primitives/settings/src/lib.rs +++ b/bitacross-worker/core-primitives/settings/src/lib.rs @@ -101,6 +101,3 @@ pub mod sidechain { pub static SLOT_DURATION: Duration = Duration::from_millis(6000); } - -/// Settings concerning the enclave -pub mod enclave {} diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs index bc9b9efe52..aeeec12a5b 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs @@ -100,7 +100,6 @@ impl IntegriteeParachainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs index 07f3801d3f..207115d47f 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs @@ -99,7 +99,6 @@ impl IntegriteeSolochainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs index 0d278eeef5..e4a08cce6d 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs @@ -104,7 +104,6 @@ impl TargetAParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs index ef9f6c07ed..e26ce6833d 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs @@ -97,7 +97,6 @@ impl TargetASolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs index d44910c478..36d83a0e06 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs @@ -104,7 +104,6 @@ impl TargetBParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let parachain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs index ab4278ce79..015ff2cea6 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs @@ -97,7 +97,6 @@ impl TargetBSolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), }; let solochain_handler = Self { diff --git a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs index abf5cd230a..e5fbed0a09 100644 --- a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs +++ b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs @@ -57,7 +57,6 @@ impl From for ProvisioningPayload { match m { WorkerMode::OffChainWorker => ProvisioningPayload::ShieldingKeyAndLightClient, WorkerMode::Sidechain => ProvisioningPayload::Everything, - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), } } } diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 2072418dc2..b87ac9f1b9 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -587,7 +587,6 @@ fn start_worker( spawn_worker_for_shard_polling(shard, litentry_rpc_api.clone(), initialization_handler); }, - WorkerMode::Teeracle => unreachable!("WorkerMode::Teeracle is not supported"), } if let Some(url) = config.target_a_parentchain_rpc_endpoint() { diff --git a/bitacross-worker/service/src/teeracle/mod.rs b/bitacross-worker/service/src/teeracle/mod.rs deleted file mode 100644 index 415f7c461d..0000000000 --- a/bitacross-worker/service/src/teeracle/mod.rs +++ /dev/null @@ -1,142 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::ServiceResult, teeracle::schedule_periodic::schedule_periodic}; -use codec::{Decode, Encode}; -use itp_enclave_api::teeracle_api::TeeracleApi; -use itp_node_api::api_client::ParentchainApi; -use itp_types::parentchain::Hash; -use litentry_hex_utils::hex_encode; -use log::*; -use sp_runtime::OpaqueExtrinsic; -use std::time::Duration; -use substrate_api_client::{SubmitAndWatch, XtStatus}; -use teeracle_metrics::{increment_number_of_request_failures, set_extrinsics_inclusion_success}; -use tokio::runtime::Handle; - -pub(crate) mod schedule_periodic; -pub(crate) mod teeracle_metrics; - -/// Schedule periodic reregistration of the enclave. -/// -/// The `send_register_xt` needs to create a fresh registration extrinsic every time it is called -/// (updated nonce, fresh IAS-RA or DCAP-Quote). -/// -/// Currently, this is only used for the teeracle, but could also be used for other flavors in the -/// future. -pub(crate) fn schedule_periodic_reregistration_thread( - send_register_xt: impl Fn() -> Option + std::marker::Send + 'static, - period: Duration, -) { - println!("Schedule periodic enclave reregistration every: {:?}", period); - - std::thread::Builder::new() - .name("enclave_reregistration_thread".to_owned()) - .spawn(move || { - schedule_periodic( - || { - trace!("Reregistering the enclave."); - if let Some(block_hash) = send_register_xt() { - println!( - "✅ Successfully reregistered the enclave. Block hash: {}.", - block_hash - ) - } else { - error!("❌ Could not reregister the enclave.") - } - }, - period, - ); - }) - .unwrap(); -} - -/// Executes a periodic teeracle data update and sends the new data to the parentchain. -/// -/// Note: Puts the current thread to sleep for `period`. -pub(crate) fn start_periodic_market_update( - api: &ParentchainApi, - period: Duration, - enclave_api: &E, - tokio_handle: &Handle, -) { - let updates_to_run = || { - if let Err(e) = execute_oracle_update(api, tokio_handle, || { - // Get market data for usd (hardcoded) - enclave_api.update_market_data_xt("TEER", "USD") - }) { - error!("Error running market update {:?}", e) - } - - // TODO: Refactor and add this back according to ISSUE: https://github.com/integritee-network/worker/issues/1300 - // if let Err(e) = execute_oracle_update(api, tokio_handle, || { - // enclave_api.update_weather_data_xt("54.32", "15.37") - // }) { - // error!("Error running weather update {:?}", e) - // } - }; - info!("Teeracle will update now"); - updates_to_run(); - - info!("Schedule teeracle updates every {:?}", period); - schedule_periodic(updates_to_run, period); -} - -fn execute_oracle_update( - node_api: &ParentchainApi, - tokio_handle: &Handle, - get_oracle_xt: F, -) -> ServiceResult<()> -where - F: Fn() -> Result, itp_enclave_api::error::Error>, -{ - let oracle_xt = get_oracle_xt().map_err(|e| { - increment_number_of_request_failures(); - e - })?; - - let extrinsics = >::decode(&mut oracle_xt.as_slice())?; - - // Send the extrinsics to the parentchain and wait for InBlock confirmation. - for call in extrinsics.into_iter() { - let node_api_clone = node_api.clone(); - tokio_handle.spawn(async move { - let encoded_extrinsic = call.encode(); - debug!("Hex encoded extrinsic to be sent: {}", hex_encode(&encoded_extrinsic)); - - println!("[>] Update oracle data (send the extrinsic)"); - let extrinsic_hash = match node_api_clone.submit_and_watch_opaque_extrinsic_until( - &encoded_extrinsic.into(), - XtStatus::InBlock, - ) { - Err(e) => { - error!("Failed to send extrinsic: {:?}", e); - set_extrinsics_inclusion_success(false); - return - }, - Ok(report) => { - set_extrinsics_inclusion_success(true); - report.extrinsic_hash - }, - }; - - println!("[<] Extrinsic got included into a block. Hash: {:?}\n", extrinsic_hash); - }); - } - - Ok(()) -} diff --git a/pallets/teebag/src/types.rs b/pallets/teebag/src/types.rs index 9320f1d826..874214cf42 100644 --- a/pallets/teebag/src/types.rs +++ b/pallets/teebag/src/types.rs @@ -79,7 +79,6 @@ pub enum WorkerMode { #[default] OffChainWorker, Sidechain, - Teeracle, } #[derive(Encode, Decode, Copy, Clone, Default, PartialEq, Eq, RuntimeDebug, TypeInfo)] diff --git a/pallets/teeracle/Cargo.toml b/pallets/teeracle/Cargo.toml deleted file mode 100644 index 9eb337f4ab..0000000000 --- a/pallets/teeracle/Cargo.toml +++ /dev/null @@ -1,75 +0,0 @@ -[package] -name = "pallet-teeracle" -description = "A pallet to store cryptocurrency market data" -version = "0.1.0" -authors = ['Trust Computing GmbH '] -homepage = 'https://litentry.com/' -repository = 'https://github.com/litentry/litentry-parachain' -license = "Apache-2.0" -edition = "2021" - -[dependencies] -codec = { version = "3.0.0", default-features = false, features = ["derive"], package = "parity-scale-codec" } -log = { version = "0.4", default-features = false } -scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } - -# local -pallet-teerex = { path = "../teerex", default-features = false } -teeracle-primitives = { path = "../../primitives/teeracle", default-features = false } - -# encointer -substrate-fixed = { tag = "v0.5.9", default-features = false, git = "https://github.com/encointer/substrate-fixed.git" } - -# substrate -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -frame-system = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-io = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# benchmarking -frame-benchmarking = { default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -hex-literal = { version = "0.4.1", optional = true } -test-utils = { path = "../test-utils", optional = true, default-features = false } -timestamp = { package = "pallet-timestamp", default-features = false, optional = true, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[dev-dependencies] -externalities = { package = "sp-externalities", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -frame-benchmarking = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -hex-literal = "0.4.1" -sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -test-utils = { path = "../test-utils" } -timestamp = { package = "pallet-timestamp", git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# litentry -pallet-teerex = { path = "../teerex" } - -[features] -default = ["std"] -std = [ - "codec/std", - "log/std", - "scale-info/std", - # local - "pallet-teerex/std", - "teeracle-primitives/std", - # encointer - "substrate-fixed/std", - # substrate - "frame-support/std", - "frame-system/std", - "sp-core/std", - "sp-io/std", - "sp-runtime/std", - "sp-std/std", -] -runtime-benchmarks = [ - "frame-benchmarking", - "hex-literal", - "test-utils", - "timestamp/runtime-benchmarks", -] - -try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/teeracle/README.md b/pallets/teeracle/README.md deleted file mode 100644 index ecb505a2f1..0000000000 --- a/pallets/teeracle/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# pallet-teeracle - -A pallet for [Integritee](https://integritee.network) which provides functionality for handling exchange rates of the coin (ex: TEER) to different currencies diff --git a/pallets/teeracle/src/benchmarking.rs b/pallets/teeracle/src/benchmarking.rs deleted file mode 100644 index 286c0c7a51..0000000000 --- a/pallets/teeracle/src/benchmarking.rs +++ /dev/null @@ -1,160 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Teeracle pallet benchmarking -#![allow(dead_code, unused_imports, const_item_mutation)] -#![cfg(any(test, feature = "runtime-benchmarks"))] - -use super::*; - -use crate::Pallet as Teeracle; -use frame_benchmarking::benchmarks; -use frame_system::RawOrigin; -use pallet_teerex::Pallet as Teerex; -use sp_runtime::traits::CheckedConversion; -use sp_std::prelude::*; -use teeracle_primitives::{DataSource, OracleDataName, TradingPairString}; - -use test_utils::{ - get_signer, - ias::{consts::*, setups::*}, -}; - -fn ensure_not_skipping_ra_check() { - #[cfg(not(test))] - if cfg!(feature = "skip-ias-check") { - panic!("Benchmark does not allow the `skip-ias-check` flag."); - }; -} -benchmarks! { - where_clause { where T::AccountId: From<[u8; 32]>, T::Hash: From<[u8; 32]> } - update_exchange_rate { - ensure_not_skipping_ra_check(); - timestamp::Pallet::::set_timestamp(TEST4_SETUP.timestamp.checked_into().unwrap()); - let signer: T::AccountId = get_signer(TEST4_SETUP.signer_pub); - let trading_pair: TradingPairString = "DOT/USD".into(); - let rate = U32F32::from_num(43.65); - let data_source: DataSource = "https://api.coingecko.com".into(); - - Teerex::::set_admin( - RawOrigin::Root.into(), - signer.clone(), - ).unwrap(); - - // we need different parameters, unfortunately - since the way to calculate - // MRENCLAVE differs depending on if `skip-ias-check` feature is present. - Teerex::::update_scheduled_enclave( - RawOrigin::Signed(signer.clone()).into(), - 0u64, - #[cfg(feature = "skip-ias-check")] - MrEnclave::decode(&mut TEST4_SETUP.cert).unwrap_or_default(), - #[cfg(not(feature = "skip-ias-check"))] - TEST4_MRENCLAVE, - ).unwrap(); - - // simply register the enclave before to make sure it already - // exists when running the benchmark - Teerex::::register_enclave( - RawOrigin::Signed(signer.clone()).into(), - TEST4_SETUP.cert.to_vec(), - URL.to_vec(), - None, - None, - ).unwrap(); - let mrenclave = Teerex::::enclave(1).unwrap().mr_enclave; - Teeracle::::add_to_whitelist(RawOrigin::Root.into(), data_source.clone(), mrenclave).unwrap(); - - }: _(RawOrigin::Signed(signer), data_source.clone(), trading_pair.clone(), Some(rate)) - verify { - assert_eq!(Teeracle::::exchange_rate(trading_pair, data_source), U32F32::from_num(43.65)); - } - - update_oracle { - ensure_not_skipping_ra_check(); - timestamp::Pallet::::set_timestamp(TEST4_SETUP.timestamp.checked_into().unwrap()); - let signer: T::AccountId = get_signer(TEST4_SETUP.signer_pub); - let oracle_name = OracleDataName::from("Test_Oracle_Name"); - let data_source = DataSource::from("Test_Source_Name"); - let oracle_blob: crate::OracleDataBlob = - vec![1].try_into().expect("Can Convert to OracleDataBlob; QED"); - - Teerex::::set_admin( - RawOrigin::Root.into(), - signer.clone(), - ).unwrap(); - - // we need different parameters, unfortunately - since the way to calculate - // MRENCLAVE differs depending on if `skip-ias-check` feature is present. - Teerex::::update_scheduled_enclave( - RawOrigin::Signed(signer.clone()).into(), - 0u64, - #[cfg(feature = "skip-ias-check")] - MrEnclave::decode(&mut TEST4_SETUP.cert).unwrap_or_default(), - #[cfg(not(feature = "skip-ias-check"))] - TEST4_MRENCLAVE, - ).unwrap(); - - Teerex::::set_skip_scheduled_enclave_check( - RawOrigin::Signed(signer.clone()).into(), - true - ).unwrap(); - - // simply register the enclave before to make sure it already - // exists when running the benchmark - Teerex::::register_enclave( - RawOrigin::Signed(signer.clone()).into(), - TEST4_SETUP.cert.to_vec(), - URL.to_vec(), - None, - None, - ).unwrap(); - let mrenclave = Teerex::::enclave(1).unwrap().mr_enclave; - Teeracle::::add_to_whitelist(RawOrigin::Root.into(), data_source.clone(), mrenclave).unwrap(); - }: _(RawOrigin::Signed(signer), oracle_name.clone(), data_source.clone(), oracle_blob.clone()) - verify { - assert_eq!(Teeracle::::oracle_data(oracle_name, data_source), oracle_blob); - } - - add_to_whitelist { - let mrenclave = TEST4_MRENCLAVE; - let data_source: DataSource = "https://api.coingecko.com".into(); - - }: _(RawOrigin::Root, data_source.clone(), mrenclave) - verify { - assert_eq!(Teeracle::::whitelist(data_source).len(), 1, "mrenclave not added to whitelist") - } - - remove_from_whitelist { - let mrenclave = TEST4_MRENCLAVE; - let data_source: DataSource = "https://api.coingecko.com".into(); - - Teeracle::::add_to_whitelist(RawOrigin::Root.into(), data_source.clone(), mrenclave).unwrap(); - - }: _(RawOrigin::Root, data_source.clone(), mrenclave) - verify { - assert_eq!(Teeracle::::whitelist(data_source).len(), 0, "mrenclave not removed from whitelist") - } -} - -#[cfg(test)] -use crate::{Config, Pallet as PalletModule}; - -#[cfg(test)] -use frame_benchmarking::impl_benchmark_test_suite; - -#[cfg(test)] -impl_benchmark_test_suite!(PalletModule, crate::mock::new_test_ext(), crate::mock::Test,); diff --git a/pallets/teeracle/src/lib.rs b/pallets/teeracle/src/lib.rs deleted file mode 100644 index a7122eec0d..0000000000 --- a/pallets/teeracle/src/lib.rs +++ /dev/null @@ -1,260 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -//! # Cryptocurrency teeracle pallet -//! -//! The teeracle pallet provides functionality for handling exchange rates of the coin (ex: TEER) to -//! different currencies -//! -//! - [`Config`] -//! - [`Call`] -//! - [`Pallet`] -//! -//! ## Overview -//! -//! The teeracle pallet provides functions for: -//! -//! - Setting exchange rates. -#![cfg_attr(not(feature = "std"), no_std)] -pub use crate::weights::WeightInfo; -pub use pallet::*; -pub use substrate_fixed::types::U32F32; -use teeracle_primitives::{DataSource, MAX_ORACLE_DATA_NAME_LEN}; - -const MAX_TRADING_PAIR_LEN: usize = 11; -const MAX_SOURCE_LEN: usize = 40; - -#[frame_support::pallet] -pub mod pallet { - use super::*; - use frame_support::{pallet_prelude::*, BoundedVec, WeakBoundedVec}; - use frame_system::pallet_prelude::*; - use sp_std::prelude::*; - use teeracle_primitives::*; - - pub type OracleDataBlob = BoundedVec::MaxOracleBlobLen>; - - #[pallet::pallet] - #[pallet::without_storage_info] - pub struct Pallet(PhantomData); - - #[pallet::config] - pub trait Config: frame_system::Config + pallet_teerex::Config { - /// The overarching event type. - type RuntimeEvent: From> + IsType<::RuntimeEvent>; - type WeightInfo: WeightInfo; - /// Max number of whitelisted oracle's releases allowed - #[pallet::constant] - type MaxWhitelistedReleases: Get; - - #[pallet::constant] - type MaxOracleBlobLen: Get; - } - - /// Exchange rates chain's cryptocurrency/currency (trading pair) from different sources - #[pallet::storage] - #[pallet::getter(fn exchange_rate)] - pub(super) type ExchangeRates = StorageDoubleMap< - _, - Blake2_128Concat, - TradingPairString, - Blake2_128Concat, - DataSource, - ExchangeRate, - ValueQuery, - >; - - #[pallet::storage] - #[pallet::getter(fn oracle_data)] - pub(super) type OracleData = StorageDoubleMap< - _, - Blake2_128Concat, - OracleDataName, - Blake2_128Concat, - DataSource, - OracleDataBlob, - ValueQuery, - >; - - /// whitelist of trusted oracle's releases for different data sources - #[pallet::storage] - #[pallet::getter(fn whitelist)] - pub(super) type Whitelists = StorageMap< - _, - Blake2_128Concat, - DataSource, - WeakBoundedVec<[u8; 32], T::MaxWhitelistedReleases>, - ValueQuery, - >; - - // pub(super) type Whitelist = - // StorageValue<_, WeakBoundedVec<[u8; 32], T::MaxWhitelistedReleases>, ValueQuery>; - - #[pallet::event] - #[pallet::generate_deposit(pub(super) fn deposit_event)] - pub enum Event { - /// The exchange rate of trading pair was set/updated with value from source. - /// \[data_source], [trading_pair], [new value\] - ExchangeRateUpdated(DataSource, TradingPairString, Option), - ExchangeRateDeleted(DataSource, TradingPairString), - OracleUpdated(OracleDataName, DataSource), - AddedToWhitelist(DataSource, [u8; 32]), - RemovedFromWhitelist(DataSource, [u8; 32]), - } - - #[pallet::error] - pub enum Error { - InvalidCurrency, - /// Too many MrEnclave in the whitelist. - ReleaseWhitelistOverflow, - ReleaseNotWhitelisted, - ReleaseAlreadyWhitelisted, - TradingPairStringTooLong, - OracleDataNameStringTooLong, - DataSourceStringTooLong, - OracleBlobTooBig, - } - - #[pallet::hooks] - impl Hooks> for Pallet {} - - #[pallet::call] - impl Pallet { - #[pallet::call_index(0)] - #[pallet::weight(::WeightInfo::add_to_whitelist())] - pub fn add_to_whitelist( - origin: OriginFor, - data_source: DataSource, - mrenclave: [u8; 32], - ) -> DispatchResult { - ensure_root(origin)?; - ensure!(data_source.len() <= MAX_SOURCE_LEN, Error::::DataSourceStringTooLong); - ensure!( - !Self::is_whitelisted(&data_source, mrenclave), - >::ReleaseAlreadyWhitelisted - ); - >::try_mutate(data_source.clone(), |mrenclave_vec| { - mrenclave_vec.try_push(mrenclave) - }) - .map_err(|_| Error::::ReleaseWhitelistOverflow)?; - Self::deposit_event(Event::AddedToWhitelist(data_source, mrenclave)); - Ok(()) - } - #[pallet::call_index(1)] - #[pallet::weight(::WeightInfo::remove_from_whitelist())] - pub fn remove_from_whitelist( - origin: OriginFor, - data_source: DataSource, - mrenclave: [u8; 32], - ) -> DispatchResult { - ensure_root(origin)?; - ensure!( - Self::is_whitelisted(&data_source, mrenclave), - >::ReleaseNotWhitelisted - ); - >::mutate(&data_source, |mrenclave_vec| { - mrenclave_vec.retain(|m| *m != mrenclave) - }); - Self::deposit_event(Event::RemovedFromWhitelist(data_source, mrenclave)); - Ok(()) - } - - #[pallet::call_index(2)] - #[pallet::weight(::WeightInfo::update_oracle())] - pub fn update_oracle( - origin: OriginFor, - oracle_name: OracleDataName, - data_source: DataSource, - new_blob: OracleDataBlob, - ) -> DispatchResultWithPostInfo { - let signer = ensure_signed(origin)?; - >::ensure_registered_enclave(&signer)?; - let signer_index = >::enclave_index(signer); - let signer_enclave = >::enclave(signer_index) - .ok_or(pallet_teerex::Error::::EmptyEnclaveRegistry)?; - - ensure!( - Self::is_whitelisted(&data_source, signer_enclave.mr_enclave), - >::ReleaseNotWhitelisted - ); - ensure!( - oracle_name.len() <= MAX_ORACLE_DATA_NAME_LEN, - Error::::OracleDataNameStringTooLong - ); - ensure!(data_source.len() <= MAX_SOURCE_LEN, Error::::DataSourceStringTooLong); - ensure!( - new_blob.len() as u32 <= T::MaxOracleBlobLen::get(), - Error::::OracleBlobTooBig - ); - - OracleData::::insert(&oracle_name, &data_source, new_blob); - Self::deposit_event(Event::::OracleUpdated(oracle_name, data_source)); - Ok(().into()) - } - - #[pallet::call_index(3)] - #[pallet::weight(::WeightInfo::update_exchange_rate())] - pub fn update_exchange_rate( - origin: OriginFor, - data_source: DataSource, - trading_pair: TradingPairString, - new_value: Option, - ) -> DispatchResultWithPostInfo { - let sender = ensure_signed(origin)?; - >::ensure_registered_enclave(&sender)?; - let sender_index = >::enclave_index(sender); - let sender_enclave = >::enclave(sender_index) - .ok_or(pallet_teerex::Error::::EmptyEnclaveRegistry)?; - // Todo: Never checks data source len - ensure!( - trading_pair.len() <= MAX_TRADING_PAIR_LEN, - Error::::TradingPairStringTooLong - ); - ensure!( - Self::is_whitelisted(&data_source, sender_enclave.mr_enclave), - >::ReleaseNotWhitelisted - ); - if new_value.is_none() || new_value == Some(U32F32::from_num(0)) { - log::info!("Delete exchange rate : {:?}", new_value); - ExchangeRates::::mutate_exists(&trading_pair, &data_source, |rate| *rate = None); - Self::deposit_event(Event::ExchangeRateDeleted(data_source, trading_pair)); - } else { - log::info!("Update exchange rate : {:?}", new_value); - ExchangeRates::::mutate_exists(&trading_pair, &data_source, |rate| { - *rate = new_value - }); - Self::deposit_event(Event::ExchangeRateUpdated( - data_source, - trading_pair, - new_value, - )); - } - Ok(().into()) - } - } -} -impl Pallet { - fn is_whitelisted(data_source: &DataSource, mrenclave: [u8; 32]) -> bool { - Self::whitelist(data_source).contains(&mrenclave) - } -} - -mod benchmarking; -#[cfg(test)] -mod mock; -#[cfg(test)] -mod tests; -pub mod weights; diff --git a/pallets/teeracle/src/mock.rs b/pallets/teeracle/src/mock.rs deleted file mode 100644 index f5955bd20e..0000000000 --- a/pallets/teeracle/src/mock.rs +++ /dev/null @@ -1,176 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate as pallet_teeracle; -use frame_support::{assert_ok, pallet_prelude::GenesisBuild, parameter_types}; -use frame_system as system; -use frame_system::EnsureRoot; -use pallet_teeracle::Config; -use sp_core::H256; -use sp_keyring::AccountKeyring; -use sp_runtime::{ - generic, - traits::{BlakeTwo256, IdentifyAccount, IdentityLookup, Verify}, -}; - -pub type Signature = sp_runtime::MultiSignature; -pub type AccountId = <::Signer as IdentifyAccount>::AccountId; -pub type Address = sp_runtime::MultiAddress; - -pub type BlockNumber = u32; -pub type Header = generic::Header; -pub type Block = generic::Block; -pub type UncheckedExtrinsic = - generic::UncheckedExtrinsic; - -pub type SignedExtra = ( - frame_system::CheckSpecVersion, - frame_system::CheckTxVersion, - frame_system::CheckGenesis, - frame_system::CheckEra, - frame_system::CheckNonce, - frame_system::CheckWeight, -); - -frame_support::construct_runtime!( - pub enum Test where - Block = Block, - NodeBlock = Block, - UncheckedExtrinsic = UncheckedExtrinsic, - { - System: frame_system, - Balances: pallet_balances, - Timestamp: timestamp, - Teerex: pallet_teerex, - Teeracle: pallet_teeracle, - } -); - -parameter_types! { - pub const BlockHashCount: u32 = 250; -} -impl frame_system::Config for Test { - type BaseCallFilter = frame_support::traits::Everything; - type BlockWeights = (); - type BlockLength = (); - type DbWeight = (); - type RuntimeOrigin = RuntimeOrigin; - type Index = u64; - type RuntimeCall = RuntimeCall; - type BlockNumber = BlockNumber; - type Hash = H256; - type Hashing = BlakeTwo256; - type AccountId = AccountId; - type Lookup = IdentityLookup; - type Header = Header; - type RuntimeEvent = RuntimeEvent; - type BlockHashCount = BlockHashCount; - type Version = (); - type PalletInfo = PalletInfo; - type AccountData = pallet_balances::AccountData; - type OnNewAccount = (); - type OnKilledAccount = (); - type SystemWeightInfo = (); - type SS58Prefix = (); - type OnSetCode = (); - type MaxConsumers = frame_support::traits::ConstU32<16>; -} - -pub type Balance = u64; - -parameter_types! { - pub const ExistentialDeposit: u64 = 1; -} - -impl pallet_balances::Config for Test { - type MaxLocks = (); - type Balance = u64; - type DustRemoval = (); - type RuntimeEvent = RuntimeEvent; - type ExistentialDeposit = ExistentialDeposit; - type AccountStore = System; - type WeightInfo = (); - type MaxReserves = (); - type ReserveIdentifier = (); - type HoldIdentifier = (); - type FreezeIdentifier = (); - type MaxHolds = (); - type MaxFreezes = (); -} - -parameter_types! { - pub const MinimumPeriod: u64 = 6000 / 2; -} - -pub type Moment = u64; - -impl timestamp::Config for Test { - type Moment = Moment; - type OnTimestampSet = Teerex; - type MinimumPeriod = MinimumPeriod; - type WeightInfo = (); -} - -parameter_types! { - pub const MomentsPerDay: u64 = 86_400_000; // [ms/d] - pub const MaxSilenceTime: u64 = 172_800_000; // 48h - pub const MaxWhitelistedReleases: u32 = 10; - pub const MaxOracleBlobLen: u32 = 4096; -} - -impl pallet_teerex::Config for Test { - type RuntimeEvent = RuntimeEvent; - type Currency = Balances; - type MomentsPerDay = MomentsPerDay; - type WeightInfo = (); - type SetAdminOrigin = EnsureRoot; - type MaxSilenceTime = MaxSilenceTime; -} - -impl Config for Test { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = (); - type MaxWhitelistedReleases = MaxWhitelistedReleases; - type MaxOracleBlobLen = MaxOracleBlobLen; -} - -// This function basically just builds a genesis storage key/value store according to -// our desired mockup. RA from enclave compiled in debug mode is allowed -pub fn new_test_ext() -> sp_io::TestExternalities { - let mut t = system::GenesisConfig::default().build_storage::().unwrap(); - pallet_balances::GenesisConfig:: { - balances: vec![(AccountKeyring::Alice.to_account_id(), 1 << 60)], - } - .assimilate_storage(&mut t) - .unwrap(); - let teerex_config = pallet_teerex::GenesisConfig { - allow_sgx_debug_mode: true, - admin: None, - skip_scheduled_enclave_check: true, - }; - GenesisBuild::::assimilate_storage(&teerex_config, &mut t).unwrap(); - - let mut ext: sp_io::TestExternalities = t.into(); - ext.execute_with(|| { - System::set_block_number(1); - assert_ok!(Teerex::set_admin(RuntimeOrigin::root(), AccountKeyring::Alice.to_account_id())); - assert_ok!(Teerex::set_skip_scheduled_enclave_check( - RuntimeOrigin::signed(AccountKeyring::Alice.to_account_id()), - true - )); - }); - ext -} diff --git a/pallets/teeracle/src/tests.rs b/pallets/teeracle/src/tests.rs deleted file mode 100644 index 0afb5bb4a2..0000000000 --- a/pallets/teeracle/src/tests.rs +++ /dev/null @@ -1,524 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -#![allow(dead_code, unused_imports, const_item_mutation)] - -use crate::{mock::*, ExchangeRates}; -use frame_support::{assert_err, assert_noop, assert_ok}; -use hex_literal::hex; -use pallet_teerex::Error; -use sp_runtime::DispatchError::BadOrigin; -use substrate_fixed::types::U32F32; -use teeracle_primitives::*; -use test_utils::ias::consts::{ - TEST4_CERT, TEST4_MRENCLAVE, TEST4_SIGNER_PUB, TEST4_TIMESTAMP, TEST5_MRENCLAVE, - TEST5_SIGNER_PUB, TEST8_MRENCLAVE, URL, -}; - -const COINGECKO_SRC: &str = "https://api.coingecko.com"; -const COINMARKETCAP_SRC: &str = "https://coinmarketcap.com/"; - -const DOT_USD_TRADING_PAIR: &str = "DOT/USD"; - -// give get_signer a concrete type -fn get_signer(pubkey: &[u8; 32]) -> AccountId { - test_utils::get_signer(pubkey) -} - -fn register_enclave_and_add_oracle_to_whitelist_ok(src: &str) { - Timestamp::set_timestamp(TEST4_TIMESTAMP); - let signer = get_signer(TEST4_SIGNER_PUB); - assert_ok!(Teerex::register_enclave( - RuntimeOrigin::signed(signer), - TEST4_CERT.to_vec(), - URL.to_vec(), - None, - None, - )); - let mrenclave = Teerex::enclave(1).unwrap().mr_enclave; - assert_ok!(Teeracle::add_to_whitelist(RuntimeOrigin::root(), src.to_owned(), mrenclave)); -} - -fn update_exchange_rate_dot_dollars_ok(src: &str, rate: Option) { - let signer = get_signer(TEST4_SIGNER_PUB); - assert_ok!(Teeracle::update_exchange_rate( - RuntimeOrigin::signed(signer), - src.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - rate - )); -} - -#[test] -fn update_exchange_rate_works() { - new_test_ext().execute_with(|| { - register_enclave_and_add_oracle_to_whitelist_ok(COINGECKO_SRC); - - let rate = U32F32::from_num(43.65); - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, Some(rate)); - let expected_event = RuntimeEvent::Teeracle(crate::Event::ExchangeRateUpdated( - COINGECKO_SRC.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - Some(rate), - )); - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert_eq!( - Teeracle::exchange_rate(DOT_USD_TRADING_PAIR.to_owned(), COINGECKO_SRC.to_owned()), - rate - ); - - let rate2 = U32F32::from_num(4294967295.65); - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, Some(rate2)); - assert_eq!( - Teeracle::exchange_rate(DOT_USD_TRADING_PAIR.to_owned(), COINGECKO_SRC.to_owned()), - rate2 - ); - }) -} - -#[test] -fn update_oracle_works() { - new_test_ext().execute_with(|| { - let signer = get_signer(TEST4_SIGNER_PUB); - register_enclave_and_add_oracle_to_whitelist_ok(&DataSource::from("Test_Source_Name")); - let oracle_blob: crate::OracleDataBlob = - vec![1].try_into().expect("Can Convert to BoundedVec; QED"); - assert_ok!(Teeracle::update_oracle( - RuntimeOrigin::signed(signer), - OracleDataName::from("Test_Oracle_Name"), - DataSource::from("Test_Source_Name"), - oracle_blob.clone() - ),); - let expected_event = RuntimeEvent::Teeracle(crate::Event::OracleUpdated( - OracleDataName::from("Test_Oracle_Name"), - DataSource::from("Test_Source_Name"), - )); - assert!(System::events().iter().any(|a| a.event == expected_event)); - - assert_eq!( - Teeracle::oracle_data( - OracleDataName::from("Test_Oracle_Name"), - DataSource::from("Test_Source_Name") - ), - oracle_blob - ); - }) -} - -#[test] -fn get_existing_exchange_rate_works() { - new_test_ext().execute_with(|| { - let rate = U32F32::from_num(43.65); - register_enclave_and_add_oracle_to_whitelist_ok(COINGECKO_SRC); - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, Some(rate)); - assert_eq!( - Teeracle::exchange_rate(DOT_USD_TRADING_PAIR.to_owned(), COINGECKO_SRC.to_owned()), - rate - ); - }) -} - -#[test] -fn get_inexisting_exchange_rate_is_zero() { - new_test_ext().execute_with(|| { - assert!(!ExchangeRates::::contains_key( - DOT_USD_TRADING_PAIR.to_owned(), - COINGECKO_SRC.to_owned() - )); - assert_eq!( - Teeracle::exchange_rate(DOT_USD_TRADING_PAIR.to_owned(), COINGECKO_SRC.to_owned()), - U32F32::from_num(0) - ); - }) -} - -#[test] -fn update_exchange_rate_to_none_delete_exchange_rate() { - new_test_ext().execute_with(|| { - register_enclave_and_add_oracle_to_whitelist_ok(COINGECKO_SRC); - let rate = U32F32::from_num(43.65); - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, Some(rate)); - - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, None); - - let expected_event = RuntimeEvent::Teeracle(crate::Event::ExchangeRateDeleted( - COINGECKO_SRC.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - )); - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert!(!ExchangeRates::::contains_key( - DOT_USD_TRADING_PAIR.to_owned(), - COINGECKO_SRC.to_owned() - )); - }) -} - -#[test] -fn update_exchange_rate_to_zero_delete_exchange_rate() { - new_test_ext().execute_with(|| { - register_enclave_and_add_oracle_to_whitelist_ok(COINGECKO_SRC); - let rate = Some(U32F32::from_num(43.65)); - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, rate); - - update_exchange_rate_dot_dollars_ok(COINGECKO_SRC, Some(U32F32::from_num(0))); - - let expected_event = RuntimeEvent::Teeracle(crate::Event::ExchangeRateDeleted( - COINGECKO_SRC.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - )); - - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert!(!ExchangeRates::::contains_key( - DOT_USD_TRADING_PAIR.to_owned(), - COINGECKO_SRC.to_owned() - )); - }) -} - -#[test] -fn update_exchange_rate_from_not_registered_enclave_fails() { - new_test_ext().execute_with(|| { - let signer = get_signer(TEST4_SIGNER_PUB); - let rate = U32F32::from_num(43.65); - assert_err!( - Teeracle::update_exchange_rate( - RuntimeOrigin::signed(signer), - COINGECKO_SRC.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - Some(rate) - ), - Error::::EnclaveIsNotRegistered - ); - }) -} - -#[test] -fn update_oracle_from_not_registered_enclave_fails() { - new_test_ext().execute_with(|| { - let signer = get_signer(TEST4_SIGNER_PUB); - assert_noop!( - Teeracle::update_oracle( - RuntimeOrigin::signed(signer), - OracleDataName::from("Test_Oracle_Name"), - DataSource::from("Test_Source_Name"), - vec![0].try_into().expect("Can Convert to BoundedVec; QED") - ), - Error::::EnclaveIsNotRegistered - ); - }) -} - -#[test] -fn update_exchange_rate_from_not_whitelisted_oracle_fails() { - new_test_ext().execute_with(|| { - Timestamp::set_timestamp(TEST4_TIMESTAMP); - let signer = get_signer(TEST4_SIGNER_PUB); - assert_ok!(Teerex::register_enclave( - RuntimeOrigin::signed(signer.clone()), - TEST4_CERT.to_vec(), - URL.to_vec(), - None, - None, - )); - - let rate = U32F32::from_num(43.65); - assert_err!( - Teeracle::update_exchange_rate( - RuntimeOrigin::signed(signer), - COINGECKO_SRC.to_owned(), - DOT_USD_TRADING_PAIR.to_owned(), - Some(rate) - ), - crate::Error::::ReleaseNotWhitelisted - ); - }) -} - -#[test] -fn update_oracle_from_not_whitelisted_oracle_fails() { - new_test_ext().execute_with(|| { - Timestamp::set_timestamp(TEST4_TIMESTAMP); - let signer = get_signer(TEST4_SIGNER_PUB); - assert_ok!(Teerex::register_enclave( - RuntimeOrigin::signed(signer.clone()), - TEST4_CERT.to_vec(), - URL.to_vec(), - None, - None, - )); - - assert_noop!( - Teeracle::update_oracle( - RuntimeOrigin::signed(signer), - OracleDataName::from("Test_Oracle_Name"), - DataSource::from("Test_Source_Name"), - vec![0].try_into().expect("Can Convert to BoundedVec; QED") - ), - crate::Error::::ReleaseNotWhitelisted - ); - }) -} - -#[test] -fn update_exchange_rate_with_too_long_trading_pair_fails() { - new_test_ext().execute_with(|| { - register_enclave_and_add_oracle_to_whitelist_ok(COINGECKO_SRC); - - let rate = Some(U32F32::from_num(43.65)); - let signer = get_signer(TEST4_SIGNER_PUB); - let too_long_trading_pair = "123456789_12".to_owned(); - assert_err!( - Teeracle::update_exchange_rate( - RuntimeOrigin::signed(signer), - COINGECKO_SRC.to_owned(), - too_long_trading_pair, - rate - ), - crate::Error::::TradingPairStringTooLong - ); - }) -} - -#[test] -fn add_to_whitelist_works() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - let expected_event = RuntimeEvent::Teeracle(crate::Event::AddedToWhitelist( - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE, - )); - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 1); - }) -} - -#[test] -fn add_mulitple_src_to_whitelists_works() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINMARKETCAP_SRC.to_owned(), - TEST4_MRENCLAVE - )); - let expected_event = RuntimeEvent::Teeracle(crate::Event::AddedToWhitelist( - COINMARKETCAP_SRC.to_owned(), - TEST4_MRENCLAVE, - )); - - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 1); - assert_eq!(Teeracle::whitelist(COINMARKETCAP_SRC.to_owned()).len(), 1); - }) -} - -#[test] -fn add_two_times_to_whitelist_fails() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_err!( - Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - ), - crate::Error::::ReleaseAlreadyWhitelisted - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 1); - }) -} - -#[test] -fn add_too_many_oracles_to_whitelist_fails() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST5_MRENCLAVE - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d2") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d3") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d4") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d5") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d6") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d7") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d8") - )); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - hex!("f4dedfc9e5fcc48443332bc9b23161c34a3c3f5a692eaffdb228db27b704d9d9") - )); - assert_err!( - Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST8_MRENCLAVE - ), - crate::Error::::ReleaseWhitelistOverflow - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 10); - }) -} -#[test] -fn add_to_whitelist_too_long_source_fails() { - new_test_ext().execute_with(|| { - let too_long_source = "123456789_223456789_323456789_423456789_1".to_owned(); - assert_err!( - Teeracle::add_to_whitelist(RuntimeOrigin::root(), too_long_source, TEST4_MRENCLAVE), - crate::Error::::DataSourceStringTooLong - ); - }) -} - -#[test] -fn non_root_add_to_whitelist_fails() { - new_test_ext().execute_with(|| { - let signer = get_signer(TEST5_SIGNER_PUB); - assert_err!( - Teeracle::add_to_whitelist( - RuntimeOrigin::signed(signer), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - ), - BadOrigin - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 0); - }) -} - -#[test] -fn remove_from_whitelist_works() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_ok!(Teeracle::remove_from_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - let expected_event = RuntimeEvent::Teeracle(crate::Event::RemovedFromWhitelist( - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE, - )); - assert!(System::events().iter().any(|a| a.event == expected_event)); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 0); - }) -} - -#[test] -fn remove_from_whitelist_not_whitelisted_fails() { - new_test_ext().execute_with(|| { - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_err!( - Teeracle::remove_from_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST5_MRENCLAVE - ), - crate::Error::::ReleaseNotWhitelisted - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 1); - }) -} - -#[test] -fn remove_from_empty_whitelist_doesnt_crash() { - new_test_ext().execute_with(|| { - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 0); - assert_err!( - Teeracle::remove_from_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST5_MRENCLAVE - ), - crate::Error::::ReleaseNotWhitelisted - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 0); - }) -} - -#[test] -fn non_root_remove_from_whitelist_fails() { - new_test_ext().execute_with(|| { - let signer = get_signer(TEST5_SIGNER_PUB); - assert_ok!(Teeracle::add_to_whitelist( - RuntimeOrigin::root(), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - )); - assert_err!( - Teeracle::remove_from_whitelist( - RuntimeOrigin::signed(signer), - COINGECKO_SRC.to_owned(), - TEST4_MRENCLAVE - ), - BadOrigin - ); - assert_eq!(Teeracle::whitelist(COINGECKO_SRC.to_owned()).len(), 1); - }) -} diff --git a/pallets/teeracle/src/weights.rs b/pallets/teeracle/src/weights.rs deleted file mode 100644 index cd3798a18f..0000000000 --- a/pallets/teeracle/src/weights.rs +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for pallet_teeracle -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2022-12-27, STEPS: `20`, REPEAT: 50, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 20 - -// Executed Command: -// ./target/release/litentry-collator -// benchmark -// pallet -// --chain=rococo-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_teeracle -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --template=./templates/benchmark/pallet-weight-template.hbs -// --output=./pallets/teeracle/src/weights.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(clippy::unnecessary_cast)] - -use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; -use sp_std::marker::PhantomData; - -/// Weight functions needed for pallet_teeracle. -pub trait WeightInfo { - fn add_to_whitelist() -> Weight; - fn remove_from_whitelist() -> Weight; - fn update_exchange_rate() -> Weight; - fn update_oracle() -> Weight; -} - -/// Weights for pallet_teeracle using the Litentry node and recommended hardware. -pub struct LitentryWeight(PhantomData); -impl WeightInfo for LitentryWeight { - // Storage: Teerex EnclaveIndex (r:1 w:0) - // Storage: Teerex EnclaveRegistry (r:1 w:0) - // Storage: Teeracle Whitelists (r:1 w:0) - // Storage: Teeracle ExchangeRates (r:1 w:1) - fn update_exchange_rate() -> Weight { - Weight::from_parts(44_063_000 as u64, 0) - .saturating_add(T::DbWeight::get().reads(4 as u64)) - .saturating_add(T::DbWeight::get().writes(1 as u64)) - } - // Storage: Teerex EnclaveIndex (r:1 w:0) - // Storage: Teerex EnclaveRegistry (r:1 w:0) - // Storage: Teeracle Whitelists (r:1 w:0) - // Storage: Teeracle OracleData (r:0 w:1) - fn update_oracle() -> Weight { - Weight::from_parts(39_144_000 as u64, 0) - .saturating_add(T::DbWeight::get().reads(3 as u64)) - .saturating_add(T::DbWeight::get().writes(1 as u64)) - } - // Storage: Teeracle Whitelists (r:1 w:1) - fn add_to_whitelist() -> Weight { - Weight::from_parts(22_062_000 as u64, 0) - .saturating_add(T::DbWeight::get().reads(1 as u64)) - .saturating_add(T::DbWeight::get().writes(1 as u64)) - } - // Storage: Teeracle Whitelists (r:1 w:1) - fn remove_from_whitelist() -> Weight { - Weight::from_parts(24_196_000 as u64, 0) - .saturating_add(T::DbWeight::get().reads(1 as u64)) - .saturating_add(T::DbWeight::get().writes(1 as u64)) - } -} - -// For backwards compatibility and tests -impl WeightInfo for () { - // Storage: Teerex EnclaveIndex (r:1 w:0) - // Storage: Teerex EnclaveRegistry (r:1 w:0) - // Storage: Teeracle Whitelists (r:1 w:0) - // Storage: Teeracle ExchangeRates (r:1 w:1) - fn update_exchange_rate() -> Weight { - Weight::from_parts(44_063_000 as u64, 0) - .saturating_add(RocksDbWeight::get().reads(4 as u64)) - .saturating_add(RocksDbWeight::get().writes(1 as u64)) - } - // Storage: Teerex EnclaveIndex (r:1 w:0) - // Storage: Teerex EnclaveRegistry (r:1 w:0) - // Storage: Teeracle Whitelists (r:1 w:0) - // Storage: Teeracle OracleData (r:0 w:1) - fn update_oracle() -> Weight { - Weight::from_parts(39_144_000 as u64, 0) - .saturating_add(RocksDbWeight::get().reads(3 as u64)) - .saturating_add(RocksDbWeight::get().writes(1 as u64)) - } - // Storage: Teeracle Whitelists (r:1 w:1) - fn add_to_whitelist() -> Weight { - Weight::from_parts(22_062_000 as u64, 0) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) - .saturating_add(RocksDbWeight::get().writes(1 as u64)) - } - // Storage: Teeracle Whitelists (r:1 w:1) - fn remove_from_whitelist() -> Weight { - Weight::from_parts(24_196_000 as u64, 0) - .saturating_add(RocksDbWeight::get().reads(1 as u64)) - .saturating_add(RocksDbWeight::get().writes(1 as u64)) - } -} - diff --git a/primitives/common/Cargo.toml b/primitives/common/Cargo.toml deleted file mode 100644 index cf8f8b94bd..0000000000 --- a/primitives/common/Cargo.toml +++ /dev/null @@ -1,19 +0,0 @@ -[package] -authors = ['Trust Computing GmbH '] -edition = "2021" -homepage = 'https://litentry.com/' -license = "Apache-2.0" -name = "common-primitives" -repository = 'https://github.com/litentry/litentry-parachain' -version = "0.1.0" - -[dependencies] - -# substrate deps -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[features] -default = ["std"] -std = [ - "sp-std/std", -] diff --git a/primitives/common/src/lib.rs b/primitives/common/src/lib.rs deleted file mode 100644 index 144071644c..0000000000 --- a/primitives/common/src/lib.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -#![cfg_attr(not(feature = "std"), no_std)] -//!Primitives for all pallets - -#[cfg(not(feature = "std"))] -use sp_std::vec::Vec; - -/// Substrate runtimes provide no string type. Hence, for arbitrary data of varying length the -/// `Vec` is used. In the polkadot-js the typedef `Text` is used to automatically -/// utf8 decode bytes into a string. -#[cfg(not(feature = "std"))] -pub type PalletString = Vec; - -#[cfg(feature = "std")] -pub type PalletString = String; - -pub trait AsByteOrNoop { - fn as_bytes_or_noop(&self) -> &[u8]; -} - -impl AsByteOrNoop for PalletString { - #[cfg(feature = "std")] - fn as_bytes_or_noop(&self) -> &[u8] { - self.as_bytes() - } - - #[cfg(not(feature = "std"))] - fn as_bytes_or_noop(&self) -> &[u8] { - self - } -} diff --git a/primitives/teeracle/Cargo.toml b/primitives/teeracle/Cargo.toml deleted file mode 100644 index 83b443bf43..0000000000 --- a/primitives/teeracle/Cargo.toml +++ /dev/null @@ -1,23 +0,0 @@ -[package] -authors = ['Trust Computing GmbH '] -edition = "2021" -homepage = 'https://litentry.com/' -license = "Apache-2.0" -name = "teeracle-primitives" -repository = 'https://github.com/litentry/litentry-parachain' -version = "0.1.0" - -[dependencies] -# local -common-primitives = { path = "../common", default-features = false } - -# encointer -substrate-fixed = { tag = "v0.5.9", default-features = false, git = "https://github.com/encointer/substrate-fixed.git" } - - -[features] -default = ["std"] -std = [ - "common-primitives/std", - "substrate-fixed/std", -] diff --git a/primitives/teeracle/src/lib.rs b/primitives/teeracle/src/lib.rs deleted file mode 100644 index e9f7f6d808..0000000000 --- a/primitives/teeracle/src/lib.rs +++ /dev/null @@ -1,12 +0,0 @@ -//!Primitives for teeracle -#![cfg_attr(not(feature = "std"), no_std)] -use common_primitives::PalletString; -use substrate_fixed::types::U32F32; - -pub const MAX_ORACLE_DATA_NAME_LEN: usize = 40; - -pub type ExchangeRate = U32F32; -pub type TradingPairString = PalletString; -pub type MarketDataSourceString = PalletString; -pub type OracleDataName = PalletString; -pub type DataSource = PalletString; diff --git a/runtime/litmus/Cargo.toml b/runtime/litmus/Cargo.toml index d69d3b9d9a..8276559faf 100644 --- a/runtime/litmus/Cargo.toml +++ b/runtime/litmus/Cargo.toml @@ -96,7 +96,6 @@ runtime-common = { path = '../common', default-features = false } # TEE pallets pallet-group = { path = "../../pallets/group", default-features = false } pallet-sidechain = { path = "../../pallets/sidechain", default-features = false } -pallet-teeracle = { path = "../../pallets/teeracle", default-features = false } pallet-teerex = { path = "../../pallets/teerex", default-features = false } [dev-dependencies] @@ -149,7 +148,6 @@ runtime-benchmarks = [ "pallet-identity-management/runtime-benchmarks", "pallet-teerex/runtime-benchmarks", "pallet-sidechain/runtime-benchmarks", - "pallet-teeracle/runtime-benchmarks", "pallet-vc-management/runtime-benchmarks", ] std = [ @@ -223,7 +221,6 @@ std = [ "pallet-identity-management/std", "pallet-teerex/std", "pallet-sidechain/std", - "pallet-teeracle/std", "pallet-vc-management/std", ] try-runtime = [ @@ -259,7 +256,6 @@ try-runtime = [ "pallet-scheduler/try-runtime", "pallet-session/try-runtime", "pallet-sidechain/try-runtime", - "pallet-teeracle/try-runtime", "pallet-teerex/try-runtime", "pallet-timestamp/try-runtime", "pallet-transaction-payment/try-runtime", diff --git a/runtime/litmus/src/lib.rs b/runtime/litmus/src/lib.rs index c18d7fad3e..5a11451966 100644 --- a/runtime/litmus/src/lib.rs +++ b/runtime/litmus/src/lib.rs @@ -40,7 +40,6 @@ use hex_literal::hex; // for TEE pub use pallet_balances::Call as BalancesCall; pub use pallet_sidechain; -pub use pallet_teeracle; pub use pallet_teerex; use sp_api::impl_runtime_apis; @@ -806,13 +805,6 @@ impl pallet_sidechain::Config for Runtime { type WeightInfo = weights::pallet_sidechain::WeightInfo; } -impl pallet_teeracle::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = weights::pallet_teeracle::WeightInfo; - type MaxWhitelistedReleases = ConstU32<10>; - type MaxOracleBlobLen = ConstU32<4096>; -} - impl runtime_common::BaseRuntimeRequirements for Runtime {} impl runtime_common::ParaRuntimeRequirements for Runtime {} @@ -886,7 +878,6 @@ construct_runtime! { // TEE Teerex: pallet_teerex = 90, Sidechain: pallet_sidechain = 91, - Teeracle: pallet_teeracle = 92, } } @@ -978,7 +969,6 @@ mod benches { [cumulus_pallet_xcmp_queue, XcmpQueue] [pallet_teerex, Teerex] [pallet_sidechain, Sidechain] - [pallet_teeracle, Teeracle] [pallet_bridge,ChainBridge] [pallet_bridge_transfer,BridgeTransfer] ); diff --git a/runtime/litmus/src/weights/mod.rs b/runtime/litmus/src/weights/mod.rs index b21dff635a..44a01085d1 100644 --- a/runtime/litmus/src/weights/mod.rs +++ b/runtime/litmus/src/weights/mod.rs @@ -34,7 +34,6 @@ pub mod pallet_proxy; pub mod pallet_scheduler; pub mod pallet_session; pub mod pallet_sidechain; -pub mod pallet_teeracle; pub mod pallet_teerex; pub mod pallet_timestamp; pub mod pallet_treasury; diff --git a/runtime/litmus/src/weights/pallet_teeracle.rs b/runtime/litmus/src/weights/pallet_teeracle.rs deleted file mode 100644 index d0ff0ba31b..0000000000 --- a/runtime/litmus/src/weights/pallet_teeracle.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for `pallet_teeracle` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-17, STEPS: `20`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `parachain-benchmark`, CPU: `Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("litmus-dev"), DB CACHE: 20 - -// Executed Command: -// ./litentry-collator -// benchmark -// pallet -// --chain=litmus-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_teeracle -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --output=./runtime/litmus/src/weights/pallet_teeracle.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_teeracle`. -pub struct WeightInfo(PhantomData); -impl pallet_teeracle::WeightInfo for WeightInfo { - /// Storage: Teerex EnclaveIndex (r:1 w:0) - /// Proof Skipped: Teerex EnclaveIndex (max_values: None, max_size: None, mode: Measured) - /// Storage: Teerex EnclaveRegistry (r:1 w:0) - /// Proof Skipped: Teerex EnclaveRegistry (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle Whitelists (r:1 w:0) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle ExchangeRates (r:1 w:1) - /// Proof Skipped: Teeracle ExchangeRates (max_values: None, max_size: None, mode: Measured) - fn update_exchange_rate() -> Weight { - // Proof Size summary in bytes: - // Measured: `3401` - // Estimated: `6866` - // Minimum execution time: 46_610_000 picoseconds. - Weight::from_parts(48_666_000, 0) - .saturating_add(Weight::from_parts(0, 6866)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teerex EnclaveIndex (r:1 w:0) - /// Proof Skipped: Teerex EnclaveIndex (max_values: None, max_size: None, mode: Measured) - /// Storage: Teerex EnclaveRegistry (r:1 w:0) - /// Proof Skipped: Teerex EnclaveRegistry (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle Whitelists (r:1 w:0) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle OracleData (r:0 w:1) - /// Proof Skipped: Teeracle OracleData (max_values: None, max_size: None, mode: Measured) - fn update_oracle() -> Weight { - // Proof Size summary in bytes: - // Measured: `3392` - // Estimated: `6857` - // Minimum execution time: 40_934_000 picoseconds. - Weight::from_parts(42_127_000, 0) - .saturating_add(Weight::from_parts(0, 6857)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teeracle Whitelists (r:1 w:1) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - fn add_to_whitelist() -> Weight { - // Proof Size summary in bytes: - // Measured: `142` - // Estimated: `3607` - // Minimum execution time: 18_686_000 picoseconds. - Weight::from_parts(19_223_000, 0) - .saturating_add(Weight::from_parts(0, 3607)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teeracle Whitelists (r:1 w:1) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - fn remove_from_whitelist() -> Weight { - // Proof Size summary in bytes: - // Measured: `244` - // Estimated: `3709` - // Minimum execution time: 20_760_000 picoseconds. - Weight::from_parts(21_141_000, 0) - .saturating_add(Weight::from_parts(0, 3709)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } -} diff --git a/runtime/rococo/Cargo.toml b/runtime/rococo/Cargo.toml index b2031d6872..4b2a9d896c 100644 --- a/runtime/rococo/Cargo.toml +++ b/runtime/rococo/Cargo.toml @@ -102,7 +102,6 @@ runtime-common = { path = '../common', default-features = false } pallet-group = { path = "../../pallets/group", default-features = false } pallet-sidechain = { path = "../../pallets/sidechain", default-features = false } pallet-teebag = { path = "../../pallets/teebag", default-features = false } -pallet-teeracle = { path = "../../pallets/teeracle", default-features = false } pallet-teerex = { path = "../../pallets/teerex", default-features = false } # EVM/Frontier @@ -181,7 +180,6 @@ runtime-benchmarks = [ "pallet-teerex/runtime-benchmarks", "pallet-teebag/runtime-benchmarks", "pallet-sidechain/runtime-benchmarks", - "pallet-teeracle/runtime-benchmarks", "pallet-vc-management/runtime-benchmarks", "pallet-account-fix/runtime-benchmarks", ] @@ -272,7 +270,6 @@ std = [ "pallet-teerex/std", "pallet-teebag/std", "pallet-sidechain/std", - "pallet-teeracle/std", "pallet-vc-management/std", "pallet-account-fix/std", "moonbeam-evm-tracer/std", @@ -320,7 +317,6 @@ try-runtime = [ "pallet-session/try-runtime", "pallet-sidechain/try-runtime", "pallet-sudo/try-runtime", - "pallet-teeracle/try-runtime", "pallet-teerex/try-runtime", "pallet-teebag/try-runtime", "pallet-timestamp/try-runtime", diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index 1bbb2866b6..e9b4a74d2b 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -42,7 +42,6 @@ use runtime_common::EnsureEnclaveSigner; pub use pallet_balances::Call as BalancesCall; pub use pallet_sidechain; pub use pallet_teebag::{self, OperationalMode as TeebagOperationalMode}; -pub use pallet_teeracle; pub use pallet_teerex; use sp_api::impl_runtime_apis; @@ -1004,13 +1003,6 @@ impl pallet_sidechain::Config for Runtime { type WeightInfo = weights::pallet_sidechain::WeightInfo; } -impl pallet_teeracle::Config for Runtime { - type RuntimeEvent = RuntimeEvent; - type WeightInfo = weights::pallet_teeracle::WeightInfo; - type MaxWhitelistedReleases = ConstU32<10>; - type MaxOracleBlobLen = ConstU32<4096>; -} - impl pallet_teebag::Config for Runtime { type RuntimeEvent = RuntimeEvent; type MomentsPerDay = MomentsPerDay; @@ -1254,7 +1246,6 @@ construct_runtime! { // TEE Teerex: pallet_teerex = 90, Sidechain: pallet_sidechain = 91, - Teeracle: pallet_teeracle = 92, Teebag: pallet_teebag = 93, // Frontier @@ -1332,7 +1323,6 @@ impl Contains for NormalModeFilter { // TEE pallets RuntimeCall::Teerex(_) | RuntimeCall::Sidechain(_) | - RuntimeCall::Teeracle(_) | RuntimeCall::Teebag(_) | // ParachainStaking; Only the collator part RuntimeCall::ParachainStaking(pallet_parachain_staking::Call::join_candidates { .. }) | @@ -1384,7 +1374,6 @@ mod benches { [pallet_vc_management, VCManagement] [pallet_teerex, Teerex] [pallet_sidechain, Sidechain] - [pallet_teeracle, Teeracle] [pallet_bridge,ChainBridge] [pallet_bridge_transfer,BridgeTransfer] ); diff --git a/runtime/rococo/src/weights/mod.rs b/runtime/rococo/src/weights/mod.rs index f17dcf17eb..7e857c9a81 100644 --- a/runtime/rococo/src/weights/mod.rs +++ b/runtime/rococo/src/weights/mod.rs @@ -36,7 +36,6 @@ pub mod pallet_proxy; pub mod pallet_scheduler; pub mod pallet_session; pub mod pallet_sidechain; -pub mod pallet_teeracle; pub mod pallet_teerex; pub mod pallet_timestamp; pub mod pallet_treasury; diff --git a/runtime/rococo/src/weights/pallet_teeracle.rs b/runtime/rococo/src/weights/pallet_teeracle.rs deleted file mode 100644 index f38318ebb8..0000000000 --- a/runtime/rococo/src/weights/pallet_teeracle.rs +++ /dev/null @@ -1,112 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -//! Autogenerated weights for `pallet_teeracle` -//! -//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 4.0.0-dev -//! DATE: 2023-10-17, STEPS: `20`, REPEAT: `50`, LOW RANGE: `[]`, HIGH RANGE: `[]` -//! WORST CASE MAP SIZE: `1000000` -//! HOSTNAME: `parachain-benchmark`, CPU: `Intel(R) Xeon(R) Platinum 8259CL CPU @ 2.50GHz` -//! EXECUTION: Some(Wasm), WASM-EXECUTION: Compiled, CHAIN: Some("rococo-dev"), DB CACHE: 20 - -// Executed Command: -// ./litentry-collator -// benchmark -// pallet -// --chain=rococo-dev -// --execution=wasm -// --db-cache=20 -// --wasm-execution=compiled -// --pallet=pallet_teeracle -// --extrinsic=* -// --heap-pages=4096 -// --steps=20 -// --repeat=50 -// --header=./LICENSE_HEADER -// --output=./runtime/rococo/src/weights/pallet_teeracle.rs - -#![cfg_attr(rustfmt, rustfmt_skip)] -#![allow(unused_parens)] -#![allow(unused_imports)] -#![allow(missing_docs)] - -use frame_support::{traits::Get, weights::Weight}; -use core::marker::PhantomData; - -/// Weight functions for `pallet_teeracle`. -pub struct WeightInfo(PhantomData); -impl pallet_teeracle::WeightInfo for WeightInfo { - /// Storage: Teerex EnclaveIndex (r:1 w:0) - /// Proof Skipped: Teerex EnclaveIndex (max_values: None, max_size: None, mode: Measured) - /// Storage: Teerex EnclaveRegistry (r:1 w:0) - /// Proof Skipped: Teerex EnclaveRegistry (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle Whitelists (r:1 w:0) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle ExchangeRates (r:1 w:1) - /// Proof Skipped: Teeracle ExchangeRates (max_values: None, max_size: None, mode: Measured) - fn update_exchange_rate() -> Weight { - // Proof Size summary in bytes: - // Measured: `3401` - // Estimated: `6866` - // Minimum execution time: 48_341_000 picoseconds. - Weight::from_parts(57_660_000, 0) - .saturating_add(Weight::from_parts(0, 6866)) - .saturating_add(T::DbWeight::get().reads(4)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teerex EnclaveIndex (r:1 w:0) - /// Proof Skipped: Teerex EnclaveIndex (max_values: None, max_size: None, mode: Measured) - /// Storage: Teerex EnclaveRegistry (r:1 w:0) - /// Proof Skipped: Teerex EnclaveRegistry (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle Whitelists (r:1 w:0) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - /// Storage: Teeracle OracleData (r:0 w:1) - /// Proof Skipped: Teeracle OracleData (max_values: None, max_size: None, mode: Measured) - fn update_oracle() -> Weight { - // Proof Size summary in bytes: - // Measured: `3392` - // Estimated: `6857` - // Minimum execution time: 41_386_000 picoseconds. - Weight::from_parts(42_413_000, 0) - .saturating_add(Weight::from_parts(0, 6857)) - .saturating_add(T::DbWeight::get().reads(3)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teeracle Whitelists (r:1 w:1) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - fn add_to_whitelist() -> Weight { - // Proof Size summary in bytes: - // Measured: `142` - // Estimated: `3607` - // Minimum execution time: 18_800_000 picoseconds. - Weight::from_parts(19_354_000, 0) - .saturating_add(Weight::from_parts(0, 3607)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } - /// Storage: Teeracle Whitelists (r:1 w:1) - /// Proof Skipped: Teeracle Whitelists (max_values: None, max_size: None, mode: Measured) - fn remove_from_whitelist() -> Weight { - // Proof Size summary in bytes: - // Measured: `244` - // Estimated: `3709` - // Minimum execution time: 21_200_000 picoseconds. - Weight::from_parts(21_772_000, 0) - .saturating_add(Weight::from_parts(0, 3709)) - .saturating_add(T::DbWeight::get().reads(1)) - .saturating_add(T::DbWeight::get().writes(1)) - } -} diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index f490cbc93e..61b4d162e8 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -8,7 +8,6 @@ function worker_clippy() { cargo clippy --release -- -D warnings cargo clippy --release --features evm -- -D warnings cargo clippy --release --features sidechain -- -D warnings - cargo clippy --release --features teeracle -- -D warnings cargo clippy --release --features offchain-worker -- -D warnings } diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index 02b90b7393..e326ea1d60 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -1225,13 +1225,6 @@ dependencies = [ "thiserror 1.0.44", ] -[[package]] -name = "common-primitives" -version = "0.1.0" -dependencies = [ - "sp-std 5.0.0", -] - [[package]] name = "concurrent-queue" version = "2.2.0" @@ -4486,25 +4479,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "ita-oracle" -version = "0.9.0" -dependencies = [ - "itc-rest-client", - "itp-enclave-metrics", - "itp-ocall-api", - "lazy_static", - "log 0.4.20", - "parity-scale-codec", - "serde 1.0.193", - "sgx_tstd", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", - "thiserror 1.0.44", - "thiserror 1.0.9", - "url 2.1.1", - "url 2.4.0", -] - [[package]] name = "ita-parentchain-interface" version = "0.9.0" @@ -9388,24 +9362,6 @@ dependencies = [ "x509-cert", ] -[[package]] -name = "pallet-teeracle" -version = "0.1.0" -dependencies = [ - "frame-support", - "frame-system", - "log 0.4.20", - "pallet-teerex", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", - "sp-std 5.0.0", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", - "teeracle-primitives", -] - [[package]] name = "pallet-teerex" version = "0.9.0" @@ -11261,7 +11217,6 @@ dependencies = [ "pallet-sidechain", "pallet-sudo", "pallet-teebag", - "pallet-teeracle", "pallet-teerex", "pallet-timestamp", "pallet-tips", @@ -14492,14 +14447,6 @@ version = "0.12.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1d2faeef5759ab89935255b1a4cd98e0baf99d1085e37d36599c625dac49ae8e" -[[package]] -name = "teeracle-primitives" -version = "0.1.0" -dependencies = [ - "common-primitives", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", -] - [[package]] name = "teerex-primitives" version = "0.1.0" diff --git a/tee-worker/Cargo.toml b/tee-worker/Cargo.toml index 120306da83..1e7b3af498 100644 --- a/tee-worker/Cargo.toml +++ b/tee-worker/Cargo.toml @@ -1,7 +1,6 @@ [workspace] members = [ - "app-libs/oracle", "app-libs/parentchain-interface", "app-libs/sgx-runtime", "app-libs/stf", @@ -97,24 +96,3 @@ sgx_urts = { version = "1.1.6", git = "https://github.com/apache/incubator-teacl [patch.crates-io] ring = { git = "https://github.com/betrusted-io/ring-xous", branch = "0.16.20-cleanup" } - -#[patch."https://github.com/integritee-network/integritee-node"] -#my-node-runtime = { package = "integritee-node-runtime", git = "https://github.com/integritee-network//integritee-node", branch = "ab/integrate-pallet-teerex-refactoring" } - -#[patch."https://github.com/scs/substrate-api-client"] -#substrate-api-client = { path = "../../scs/substrate-api-client" } -#substrate-client-keystore = { path = "../../scs/substrate-api-client/client-keystore" } - -#[patch."https://github.com/integritee-network/pallets.git"] -#pallet-claims = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-enclave-bridge = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-teerex = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-sidechain = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#sgx-verify = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-teeracle = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#test-utils = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#claims-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#enclave-bridge-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#teerex-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#teeracle-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#common-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } diff --git a/tee-worker/app-libs/oracle/Cargo.toml b/tee-worker/app-libs/oracle/Cargo.toml deleted file mode 100644 index eb8fa135e2..0000000000 --- a/tee-worker/app-libs/oracle/Cargo.toml +++ /dev/null @@ -1,48 +0,0 @@ -[package] -name = "ita-oracle" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] - -# std dependencies -thiserror = { version = "1.0.26", optional = true } -url = { version = "2.0.0", optional = true } - -# sgx dependencies -sgx_tstd = { rev = "v1.1.3", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } -thiserror_sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } -url_sgx = { package = "url", git = "https://github.com/mesalock-linux/rust-url-sgx", tag = "sgx_1.1.3", optional = true } - -# no_std dependencies -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -lazy_static = { version = "1.1.0", features = ["spin_no_std"] } -log = { version = "0.4", default-features = false } -serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } -substrate-fixed = { default-features = false, git = "https://github.com/encointer/substrate-fixed", tag = "v0.5.9" } - -# internal dependencies -itc-rest-client = { path = "../../core/rest-client", default-features = false } -itp-enclave-metrics = { path = "../../core-primitives/enclave-metrics", default-features = false } -itp-ocall-api = { path = "../../core-primitives/ocall-api", default-features = false } - -[features] -default = ["std"] -std = [ - "itc-rest-client/std", - "itp-enclave-metrics/std", - "itp-ocall-api/std", - "log/std", - "serde/std", - "substrate-fixed/std", - "thiserror", - "url", -] -sgx = [ - "itc-rest-client/sgx", - "itp-enclave-metrics/sgx", - "sgx_tstd", - "thiserror_sgx", - "url_sgx", -] diff --git a/tee-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem b/tee-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem deleted file mode 100644 index a6f3e92af5..0000000000 --- a/tee-worker/app-libs/oracle/src/certificates/amazon_root_ca_a.pem +++ /dev/null @@ -1,20 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDQTCCAimgAwIBAgITBmyfz5m/jAo54vB4ikPmljZbyjANBgkqhkiG9w0BAQsF -ADA5MQswCQYDVQQGEwJVUzEPMA0GA1UEChMGQW1hem9uMRkwFwYDVQQDExBBbWF6 -b24gUm9vdCBDQSAxMB4XDTE1MDUyNjAwMDAwMFoXDTM4MDExNzAwMDAwMFowOTEL -MAkGA1UEBhMCVVMxDzANBgNVBAoTBkFtYXpvbjEZMBcGA1UEAxMQQW1hem9uIFJv -b3QgQ0EgMTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALJ4gHHKeNXj -ca9HgFB0fW7Y14h29Jlo91ghYPl0hAEvrAIthtOgQ3pOsqTQNroBvo3bSMgHFzZM -9O6II8c+6zf1tRn4SWiw3te5djgdYZ6k/oI2peVKVuRF4fn9tBb6dNqcmzU5L/qw -IFAGbHrQgLKm+a/sRxmPUDgH3KKHOVj4utWp+UhnMJbulHheb4mjUcAwhmahRWa6 -VOujw5H5SNz/0egwLX0tdHA114gk957EWW67c4cX8jJGKLhD+rcdqsq08p8kDi1L -93FcXmn/6pUCyziKrlA4b9v7LWIbxcceVOF34GfID5yHI9Y/QCB/IIDEgEw+OyQm -jgSubJrIqg0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMC -AYYwHQYDVR0OBBYEFIQYzIU07LwMlJQuCFmcx7IQTgoIMA0GCSqGSIb3DQEBCwUA -A4IBAQCY8jdaQZChGsV2USggNiMOruYou6r4lK5IpDB/G/wkjUu0yKGX9rbxenDI -U5PMCCjjmCXPI6T53iHTfIUJrU6adTrCC2qJeHZERxhlbI1Bjjt/msv0tadQ1wUs -N+gDS63pYaACbvXy8MWy7Vu33PqUXHeeE6V/Uq2V8viTO96LXFvKWlJbYK8U90vv -o/ufQJVtMVT8QtPHRh8jrdkPSHCa2XV4cdFyQzR1bldZwgJcJmApzyMZFo6IQ6XU -5MsI+yMRQ+hDKXJioaldXgjUkK642M4UwtBV8ob2xJNDd2ZhwLnoQdeXeGADbkpy -rqXRfboQnoZsG4q5WTP468SQvvG5 ------END CERTIFICATE----- diff --git a/tee-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem b/tee-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem deleted file mode 100644 index 519028c63b..0000000000 --- a/tee-worker/app-libs/oracle/src/certificates/baltimore_cyber_trust_root_v3.pem +++ /dev/null @@ -1,21 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDdzCCAl+gAwIBAgIEAgAAuTANBgkqhkiG9w0BAQUFADBaMQswCQYDVQQGEwJJ -RTESMBAGA1UEChMJQmFsdGltb3JlMRMwEQYDVQQLEwpDeWJlclRydXN0MSIwIAYD -VQQDExlCYWx0aW1vcmUgQ3liZXJUcnVzdCBSb290MB4XDTAwMDUxMjE4NDYwMFoX -DTI1MDUxMjIzNTkwMFowWjELMAkGA1UEBhMCSUUxEjAQBgNVBAoTCUJhbHRpbW9y -ZTETMBEGA1UECxMKQ3liZXJUcnVzdDEiMCAGA1UEAxMZQmFsdGltb3JlIEN5YmVy -VHJ1c3QgUm9vdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKMEuyKr -mD1X6CZymrV51Cni4eiVgLGw41uOKymaZN+hXe2wCQVt2yguzmKiYv60iNoS6zjr -IZ3AQSsBUnuId9Mcj8e6uYi1agnnc+gRQKfRzMpijS3ljwumUNKoUMMo6vWrJYeK -mpYcqWe4PwzV9/lSEy/CG9VwcPCPwBLKBsua4dnKM3p31vjsufFoREJIE9LAwqSu -XmD+tqYF/LTdB1kC1FkYmGP1pWPgkAx9XbIGevOF6uvUA65ehD5f/xXtabz5OTZy -dc93Uk3zyZAsuT3lySNTPx8kmCFcB5kpvcY67Oduhjprl3RjM71oGDHweI12v/ye -jl0qhqdNkNwnGjkCAwEAAaNFMEMwHQYDVR0OBBYEFOWdWTCCR1jMrPoIVDaGezq1 -BE3wMBIGA1UdEwEB/wQIMAYBAf8CAQMwDgYDVR0PAQH/BAQDAgEGMA0GCSqGSIb3 -DQEBBQUAA4IBAQCFDF2O5G9RaEIFoN27TyclhAO992T9Ldcw46QQF+vaKSm2eT92 -9hkTI7gQCvlYpNRhcL0EYWoSihfVCr3FvDB81ukMJY2GQE/szKN+OMY3EU/t3Wgx -jkzSswF07r51XgdIGn9w/xZchMB5hbgF/X++ZRGjD8ACtPhSNzkE1akxehi/oCr0 -Epn3o0WC4zxe9Z2etciefC7IpJ5OCBRLbf1wbWsaY71k5h+3zvDyny67G7fyUIhz -ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS -R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp ------END CERTIFICATE----- diff --git a/tee-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem b/tee-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem deleted file mode 100644 index 57d4a3766c..0000000000 --- a/tee-worker/app-libs/oracle/src/certificates/lets_encrypt_root_cert.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- \ No newline at end of file diff --git a/tee-worker/app-libs/oracle/src/certificates/open_meteo_root.pem b/tee-worker/app-libs/oracle/src/certificates/open_meteo_root.pem deleted file mode 100644 index b85c8037f6..0000000000 --- a/tee-worker/app-libs/oracle/src/certificates/open_meteo_root.pem +++ /dev/null @@ -1,31 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw -TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh -cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4 -WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu -ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY -MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc -h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+ -0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U -A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW -T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH -B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC -B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv -KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn -OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn -jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw -qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI -rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq -hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL -ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ -3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK -NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5 -ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur -TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC -jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc -oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq -4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA -mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d -emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc= ------END CERTIFICATE----- diff --git a/tee-worker/app-libs/oracle/src/error.rs b/tee-worker/app-libs/oracle/src/error.rs deleted file mode 100644 index df72280f34..0000000000 --- a/tee-worker/app-libs/oracle/src/error.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::types::TradingPair; -use std::{boxed::Box, string::String}; - -/// Exchange rate error -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("Rest client error")] - RestClient(#[from] itc_rest_client::error::Error), - #[error("Could not retrieve any data from {0} for {1}")] - NoValidData(String, String), - #[error("Value for exchange rate is null")] - EmptyExchangeRate(TradingPair), - #[error("Invalid id for crypto currency")] - InvalidCryptoCurrencyId, - #[error("Invalid id for fiat currency")] - InvalidFiatCurrencyId, - #[error(transparent)] - Other(#[from] Box), -} diff --git a/tee-worker/app-libs/oracle/src/lib.rs b/tee-worker/app-libs/oracle/src/lib.rs deleted file mode 100644 index 6faee79a63..0000000000 --- a/tee-worker/app-libs/oracle/src/lib.rs +++ /dev/null @@ -1,84 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(test, feature(assert_matches))] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -// re-export module to properly feature gate sgx and regular std environment -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub mod sgx_reexport_prelude { - pub use thiserror_sgx as thiserror; - pub use url_sgx as url; -} - -use crate::{error::Error, metrics_exporter::MetricsExporter}; -use itp_ocall_api::EnclaveMetricsOCallApi; -use std::sync::Arc; - -pub mod error; -pub mod metrics_exporter; -pub mod traits; -pub mod types; - -pub mod oracles; -pub use oracles::{exchange_rate_oracle::ExchangeRateOracle, weather_oracle::WeatherOracle}; - -pub mod oracle_sources; -pub use oracle_sources::{ - coin_gecko::CoinGeckoSource, coin_market_cap::CoinMarketCapSource, - weather_oracle_source::WeatherOracleSource, -}; - -#[cfg(test)] -mod mock; - -#[cfg(test)] -mod test; - -pub type CoinGeckoExchangeRateOracle = - ExchangeRateOracle>; - -pub type CoinMarketCapExchangeRateOracle = - ExchangeRateOracle>; - -pub type OpenMeteoWeatherOracle = - WeatherOracle>; - -pub fn create_coin_gecko_oracle( - ocall_api: Arc, -) -> CoinGeckoExchangeRateOracle { - ExchangeRateOracle::new(CoinGeckoSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} - -pub fn create_coin_market_cap_oracle( - ocall_api: Arc, -) -> CoinMarketCapExchangeRateOracle { - ExchangeRateOracle::new(CoinMarketCapSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} - -pub fn create_open_meteo_weather_oracle( - ocall_api: Arc, -) -> OpenMeteoWeatherOracle { - WeatherOracle::new(WeatherOracleSource {}, Arc::new(MetricsExporter::new(ocall_api))) -} diff --git a/tee-worker/app-libs/oracle/src/metrics_exporter.rs b/tee-worker/app-libs/oracle/src/metrics_exporter.rs deleted file mode 100644 index aa10516fd1..0000000000 --- a/tee-worker/app-libs/oracle/src/metrics_exporter.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::types::{ExchangeRate, TradingPair}; -use itp_enclave_metrics::{EnclaveMetric, ExchangeRateOracleMetric, OracleMetric}; -use itp_ocall_api::EnclaveMetricsOCallApi; -use log::error; -use std::{string::String, sync::Arc, time::Instant}; - -/// Trait to export metrics for any Teeracle. -pub trait ExportMetrics { - fn increment_number_requests(&self, source: String); - - fn record_response_time(&self, source: String, timer: Instant); - - fn update_exchange_rate( - &self, - source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ); - - fn update_weather(&self, source: String, metrics_info: MetricsInfo); -} - -pub trait UpdateMetric { - fn update_metric(&self, metric: OracleMetric); -} - -/// Metrics exporter implementation. -pub struct MetricsExporter { - ocall_api: Arc, -} - -impl UpdateMetric for MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - fn update_metric(&self, _metric: OracleMetric) { - // TODO: Implement me - } -} - -impl MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - pub fn new(ocall_api: Arc) -> Self { - MetricsExporter { ocall_api } - } - - fn update_metric(&self, metric: ExchangeRateOracleMetric) { - if let Err(e) = self.ocall_api.update_metric(EnclaveMetric::ExchangeRateOracle(metric)) { - error!("Failed to update enclave metric, sgx_status_t: {}", e) - } - } -} - -impl ExportMetrics for MetricsExporter -where - OCallApi: EnclaveMetricsOCallApi, -{ - fn increment_number_requests(&self, source: String) { - self.update_metric(ExchangeRateOracleMetric::NumberRequestsIncrement(source)); - } - - fn record_response_time(&self, source: String, timer: Instant) { - self.update_metric(ExchangeRateOracleMetric::ResponseTime( - source, - timer.elapsed().as_millis(), - )); - } - - fn update_exchange_rate( - &self, - source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ) { - self.update_metric(ExchangeRateOracleMetric::ExchangeRate( - source, - trading_pair.key(), - exchange_rate, - )); - } - - fn update_weather(&self, _source: String, _metrics_info: MetricsInfo) { - // TODO: Implement me - } -} diff --git a/tee-worker/app-libs/oracle/src/mock.rs b/tee-worker/app-libs/oracle/src/mock.rs deleted file mode 100644 index f12224b0ea..0000000000 --- a/tee-worker/app-libs/oracle/src/mock.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(feature = "sgx")] -use std::sync::SgxRwLock as RwLock; - -#[cfg(feature = "std")] -use std::sync::RwLock; - -use crate::{ - error::Error, - metrics_exporter::ExportMetrics, - traits::OracleSource, - types::{ExchangeRate, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use std::{ - time::{Duration, Instant}, - vec, - vec::Vec, -}; -use url::Url; - -/// Mock metrics exporter. -#[derive(Default)] -pub(crate) struct MetricsExporterMock { - number_requests: RwLock, - response_times: RwLock>, - exchange_rates: RwLock>, -} - -impl MetricsExporterMock { - pub fn get_number_request(&self) -> u64 { - *self.number_requests.read().unwrap() - } - - pub fn get_response_times(&self) -> Vec { - self.response_times.read().unwrap().clone() - } - - pub fn get_exchange_rates(&self) -> Vec<(TradingPair, ExchangeRate)> { - self.exchange_rates.read().unwrap().clone() - } -} - -impl ExportMetrics for MetricsExporterMock { - fn increment_number_requests(&self, _source: String) { - (*self.number_requests.write().unwrap()) += 1; - } - - fn record_response_time(&self, _source: String, timer: Instant) { - self.response_times.write().unwrap().push(timer.elapsed().as_millis()); - } - - fn update_exchange_rate( - &self, - _source: String, - exchange_rate: ExchangeRate, - trading_pair: TradingPair, - ) { - self.exchange_rates.write().unwrap().push((trading_pair, exchange_rate)); - } - - fn update_weather(&self, _source: String, _metrics_info: MetricsInfo) {} -} - -/// Mock oracle source. -#[derive(Default)] -pub(crate) struct OracleSourceMock; - -impl OracleSource for OracleSourceMock { - type OracleRequestResult = Result; - - fn metrics_id(&self) -> String { - "source_mock".to_string() - } - - fn request_timeout(&self) -> Option { - None - } - - fn base_url(&self) -> Result { - Url::parse("https://mock.base.url").map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec!["MOCK_CERTIFICATE".to_string()] - } - fn execute_exchange_rate_request( - &self, - _rest_client: &mut RestClient>, - _trading_pair: TradingPair, - ) -> Result { - Ok(ExchangeRate::from_num(42.3f32)) - } - - fn execute_request( - _rest_client: &mut RestClient>, - _source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - Ok(42.3f32) - } -} diff --git a/tee-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs b/tee-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs deleted file mode 100644 index d9b8ad91ee..0000000000 --- a/tee-worker/app-libs/oracle/src/oracle_sources/coin_gecko.rs +++ /dev/null @@ -1,220 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use lazy_static::lazy_static; -use log::{debug, error}; -use serde::{Deserialize, Serialize}; -use std::{ - collections::HashMap, - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const COINGECKO_URL: &str = "https://api.coingecko.com"; -const COINGECKO_PARAM_CURRENCY: &str = "vs_currency"; -const COINGECKO_PARAM_COIN: &str = "ids"; -const COINGECKO_PATH: &str = "api/v3/coins/markets"; -const COINGECKO_TIMEOUT: Duration = Duration::from_secs(20u64); -const COINGECKO_ROOT_CERTIFICATE_BALTIMORE: &str = - include_str!("../certificates/baltimore_cyber_trust_root_v3.pem"); -const COINGECKO_ROOT_CERTIFICATE_LETSENCRYPT: &str = - include_str!("../certificates/lets_encrypt_root_cert.pem"); - -lazy_static! { - static ref SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = HashMap::from([ - ("DOT", "polkadot"), - ("TEER", "integritee"), - ("KSM", "kusama"), - ("BTC", "bitcoin"), - ]); -} - -/// CoinGecko oracle source. -#[derive(Default)] -pub struct CoinGeckoSource; - -impl CoinGeckoSource { - fn map_crypto_currency_id(trading_pair: &TradingPair) -> Result { - let key = &trading_pair.crypto_currency; - match SYMBOL_ID_MAP.get(key.as_str()) { - Some(v) => Ok(v.to_string()), - None => Err(Error::InvalidCryptoCurrencyId), - } - } -} - -impl> OracleSource for CoinGeckoSource { - type OracleRequestResult = Result<(), Error>; - - fn metrics_id(&self) -> String { - "coin_gecko".to_string() - } - - fn request_timeout(&self) -> Option { - Some(COINGECKO_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(COINGECKO_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec![ - COINGECKO_ROOT_CERTIFICATE_LETSENCRYPT.to_string(), - COINGECKO_ROOT_CERTIFICATE_BALTIMORE.to_string(), - ] - } - - fn execute_request( - _rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let _trading_info: TradingInfo = source_info.into(); - // TODO Implement me - Ok(()) - } - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result { - let fiat_id = trading_pair.fiat_currency.clone(); - let crypto_id = Self::map_crypto_currency_id(&trading_pair)?; - - let response = rest_client.get_with::( - COINGECKO_PATH.to_string(), - &[(COINGECKO_PARAM_CURRENCY, &fiat_id), (COINGECKO_PARAM_COIN, &crypto_id)], - ); - - let response = match response { - Ok(response) => response, - Err(e) => { - error!("coingecko execute_exchange_rate_request() failed with: {:?}", &e); - return Err(Error::RestClient(e)) - }, - }; - - debug!("coingecko received response: {:?}", &response); - let list = response.0; - if list.is_empty() { - return Err(Error::NoValidData(COINGECKO_URL.to_string(), trading_pair.key())) - } - - match list[0].current_price { - Some(r) => Ok(ExchangeRate::from_num(r)), - None => Err(Error::EmptyExchangeRate(trading_pair)), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinGeckoMarketStruct { - id: String, - symbol: String, - name: String, - current_price: Option, - last_updated: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinGeckoMarket(pub Vec); - -impl RestPath for CoinGeckoMarket { - fn get_path(path: String) -> Result { - Ok(path) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - mock::MetricsExporterMock, - oracles::exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - }; - use core::assert_matches::assert_matches; - use std::sync::Arc; - - type TestCoinGeckoClient = ExchangeRateOracle; - - fn get_coin_gecko_crypto_currency_id(crypto_currency: &str) -> Result { - let trading_pair = TradingPair { - crypto_currency: crypto_currency.to_string(), - fiat_currency: "USD".to_string(), - }; - CoinGeckoSource::map_crypto_currency_id(&trading_pair) - } - - #[test] - fn crypto_currency_id_works_for_dot() { - let coin_id = get_coin_gecko_crypto_currency_id("DOT").unwrap(); - assert_eq!(&coin_id, "polkadot"); - } - - #[test] - fn crypto_currency_id_works_for_teer() { - let coin_id = get_coin_gecko_crypto_currency_id("TEER").unwrap(); - assert_eq!(&coin_id, "integritee"); - } - - #[test] - fn crypto_currency_id_works_for_ksm() { - let coin_id = get_coin_gecko_crypto_currency_id("KSM").unwrap(); - assert_eq!(&coin_id, "kusama"); - } - - #[test] - fn crypto_currency_id_works_for_btc() { - let coin_id = get_coin_gecko_crypto_currency_id("BTC").unwrap(); - assert_eq!(&coin_id, "bitcoin"); - } - - #[test] - fn crypto_currency_id_fails_for_undefined_crypto_currency() { - let result = get_coin_gecko_crypto_currency_id("Undefined"); - assert_matches!(result, Err(Error::InvalidCryptoCurrencyId)); - } - - #[test] - fn get_exchange_rate_for_undefined_fiat_currency_fails() { - let coin_gecko_client = create_coin_gecko_client(); - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CH".to_string() }; - let result = coin_gecko_client.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::RestClient(_))); - } - - fn create_coin_gecko_client() -> TestCoinGeckoClient { - TestCoinGeckoClient::new(CoinGeckoSource {}, Arc::new(MetricsExporterMock::default())) - } -} diff --git a/tee-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs b/tee-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs deleted file mode 100644 index a0e053b8e6..0000000000 --- a/tee-worker/app-libs/oracle/src/oracle_sources/coin_market_cap.rs +++ /dev/null @@ -1,242 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use lazy_static::lazy_static; -use serde::{Deserialize, Serialize}; -use std::{ - collections::{BTreeMap, HashMap}, - env, - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const COINMARKETCAP_URL: &str = "https://pro-api.coinmarketcap.com"; -const COINMARKETCAP_KEY_PARAM: &str = "CMC_PRO_API_KEY"; -const FIAT_CURRENCY_PARAM: &str = "convert_id"; -const CRYPTO_CURRENCY_PARAM: &str = "id"; -const COINMARKETCAP_PATH: &str = "v2/cryptocurrency/quotes/latest"; // API endpoint to get the exchange rate with a basic API plan (free) -const COINMARKETCAP_TIMEOUT: Duration = Duration::from_secs(3u64); -const COINMARKETCAP_ROOT_CERTIFICATE: &str = include_str!("../certificates/amazon_root_ca_a.pem"); - -lazy_static! { - static ref CRYPTO_SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = - HashMap::from([("DOT", "6636"), ("TEER", "13323"), ("KSM", "5034"), ("BTC", "1"),]); - static ref COINMARKETCAP_KEY: String = env::var("COINMARKETCAP_KEY").unwrap_or_default(); -} - -lazy_static! { - static ref FIAT_SYMBOL_ID_MAP: HashMap<&'static str, &'static str> = - HashMap::from([("USD", "2781"), ("EUR", "2790"), ("CHF", "2785"), ("JPY", "2797"),]); -} - -#[derive(Default)] -pub struct CoinMarketCapSource; - -impl CoinMarketCapSource { - fn map_crypto_currency_id(trading_pair: &TradingPair) -> Result { - CRYPTO_SYMBOL_ID_MAP - .get(trading_pair.crypto_currency.as_str()) - .map(|v| v.to_string()) - .ok_or(Error::InvalidCryptoCurrencyId) - } - - fn map_fiat_currency_id(trading_pair: &TradingPair) -> Result { - FIAT_SYMBOL_ID_MAP - .get(trading_pair.fiat_currency.as_str()) - .map(|v| v.to_string()) - .ok_or(Error::InvalidFiatCurrencyId) - } -} - -impl> OracleSource for CoinMarketCapSource { - // TODO Change this to return something useful? - type OracleRequestResult = Result<(), Error>; - - fn metrics_id(&self) -> String { - "coin_market_cap".to_string() - } - - fn request_timeout(&self) -> Option { - Some(COINMARKETCAP_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(COINMARKETCAP_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - fn root_certificates_content(&self) -> Vec { - vec![COINMARKETCAP_ROOT_CERTIFICATE.to_string()] - } - - fn execute_request( - _rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let trading_info: TradingInfo = source_info.into(); - let _fiat_currency = trading_info.trading_pair.fiat_currency; - let _crypto_currency = trading_info.trading_pair.crypto_currency; - // TODO Implement me - Ok(()) - } - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result { - let fiat_id = Self::map_fiat_currency_id(&trading_pair)?; - let crypto_id = Self::map_crypto_currency_id(&trading_pair)?; - - let response = rest_client - .get_with::( - COINMARKETCAP_PATH.to_string(), - &[ - (FIAT_CURRENCY_PARAM, &fiat_id), - (CRYPTO_CURRENCY_PARAM, &crypto_id), - (COINMARKETCAP_KEY_PARAM, &COINMARKETCAP_KEY), - ], - ) - .map_err(Error::RestClient)?; - - let data_struct = response.0; - - let data = match data_struct.data.get(&crypto_id) { - Some(d) => d, - None => - return Err(Error::NoValidData( - COINMARKETCAP_URL.to_string(), - trading_pair.crypto_currency, - )), - }; - - let quote = match data.quote.get(&fiat_id) { - Some(q) => q, - None => - return Err(Error::NoValidData(COINMARKETCAP_URL.to_string(), trading_pair.key())), - }; - match quote.price { - Some(r) => Ok(ExchangeRate::from_num(r)), - None => Err(Error::EmptyExchangeRate(trading_pair)), - } - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct DataStruct { - id: Option, - name: String, - symbol: String, - quote: BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug)] -struct QuoteStruct { - price: Option, - last_updated: Option, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinMarketCapMarketStruct { - data: BTreeMap, -} - -#[derive(Serialize, Deserialize, Debug)] -struct CoinMarketCapMarket(pub CoinMarketCapMarketStruct); - -impl RestPath for CoinMarketCapMarket { - fn get_path(path: String) -> Result { - Ok(path) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - mock::MetricsExporterMock, - oracles::exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - }; - use core::assert_matches::assert_matches; - use std::sync::Arc; - - type TestClient = ExchangeRateOracle; - - fn get_coin_market_cap_crypto_currency_id(crypto_currency: &str) -> Result { - let trading_pair = TradingPair { - crypto_currency: crypto_currency.to_string(), - fiat_currency: "USD".to_string(), - }; - CoinMarketCapSource::map_crypto_currency_id(&trading_pair) - } - - #[test] - fn crypto_currency_id_works_for_dot() { - let coin_id = get_coin_market_cap_crypto_currency_id("DOT").unwrap(); - assert_eq!(&coin_id, "6636"); - } - - #[test] - fn crypto_currency_id_works_for_teer() { - let coin_id = get_coin_market_cap_crypto_currency_id("TEER").unwrap(); - assert_eq!(&coin_id, "13323"); - } - - #[test] - fn crypto_currency_id_works_for_ksm() { - let coin_id = get_coin_market_cap_crypto_currency_id("KSM").unwrap(); - assert_eq!(&coin_id, "5034"); - } - - #[test] - fn crypto_currency_id_works_for_btc() { - let coin_id = get_coin_market_cap_crypto_currency_id("BTC").unwrap(); - assert_eq!(&coin_id, "1"); - } - - #[test] - fn crypto_currency_id_fails_for_undefined_crypto_currency() { - let coin_id = get_coin_market_cap_crypto_currency_id("Undefined"); - assert_matches!(coin_id, Err(Error::InvalidCryptoCurrencyId)); - } - - #[test] - fn get_exchange_rate_for_undefined_fiat_currency_fails() { - let coin_market_cap_client = create_client(); - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CH".to_string() }; - let result = coin_market_cap_client.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::InvalidFiatCurrencyId)); - } - - fn create_client() -> TestClient { - TestClient::new(CoinMarketCapSource {}, Arc::new(MetricsExporterMock::default())) - } -} diff --git a/tee-worker/app-libs/oracle/src/oracle_sources/mod.rs b/tee-worker/app-libs/oracle/src/oracle_sources/mod.rs deleted file mode 100644 index d2d88153c3..0000000000 --- a/tee-worker/app-libs/oracle/src/oracle_sources/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -pub mod coin_gecko; -pub mod coin_market_cap; -pub mod weather_oracle_source; diff --git a/tee-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs b/tee-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs deleted file mode 100644 index 9f199be5dc..0000000000 --- a/tee-worker/app-libs/oracle/src/oracle_sources/weather_oracle_source.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - error::Error, - traits::OracleSource, - types::{ExchangeRate, TradingPair, WeatherInfo}, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, - RestGet, RestPath, -}; -use serde::{Deserialize, Serialize}; -use std::{ - string::{String, ToString}, - time::Duration, - vec::Vec, -}; -use url::Url; - -const WEATHER_URL: &str = "https://api.open-meteo.com"; -const WEATHER_PARAM_LONGITUDE: &str = "longitude"; -const WEATHER_PARAM_LATITUDE: &str = "latitude"; -// const WEATHER_PARAM_HOURLY: &str = "hourly"; // TODO: Add to Query -const WEATHER_PATH: &str = "v1/forecast"; -const WEATHER_TIMEOUT: Duration = Duration::from_secs(3u64); -const WEATHER_ROOT_CERTIFICATE: &str = include_str!("../certificates/open_meteo_root.pem"); - -// TODO: Change f32 types to appropriate Substrate Fixed Type -#[derive(Default)] -pub struct WeatherOracleSource; - -impl> OracleSource for WeatherOracleSource { - type OracleRequestResult = Result; // TODO: Change from f32 type - - fn metrics_id(&self) -> String { - "weather".to_string() - } - - fn request_timeout(&self) -> Option { - Some(WEATHER_TIMEOUT) - } - - fn base_url(&self) -> Result { - Url::parse(WEATHER_URL).map_err(|e| Error::Other(format!("{:?}", e).into())) - } - - /// The server's root certificate. A valid certificate is required to open a tls connection - fn root_certificates_content(&self) -> Vec { - vec![WEATHER_ROOT_CERTIFICATE.to_string()] - } - - fn execute_exchange_rate_request( - &self, - _rest_client: &mut RestClient>, - _trading_pair: TradingPair, - ) -> Result { - Err(Error::NoValidData("None".into(), "None".into())) - } - - // TODO: Make this take a variant perhaps or a Closure so that it is more generic - fn execute_request( - rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult { - let weather_info: WeatherInfo = source_info.into(); - let query = weather_info.weather_query; - - // TODO: - // This part is opinionated towards a hard coded query need to make more generic - let response = rest_client - .get_with::( - WEATHER_PATH.into(), - &[ - (WEATHER_PARAM_LATITUDE, &query.latitude), - (WEATHER_PARAM_LONGITUDE, &query.longitude), - //(WEATHER_PARAM_HOURLY), &query.hourly), - ], - ) - .map_err(Error::RestClient)?; - - let open_meteo_weather_struct = response.0; - - Ok(open_meteo_weather_struct.longitude) - } -} - -#[derive(Serialize, Deserialize, Debug)] -struct OpenMeteoWeatherStruct { - latitude: f32, - longitude: f32, - //hourly: String, -} - -#[derive(Serialize, Deserialize, Debug)] -struct OpenMeteo(pub OpenMeteoWeatherStruct); - -impl RestPath for OpenMeteo { - fn get_path(path: String) -> Result { - Ok(path) - } -} diff --git a/tee-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs b/tee-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs deleted file mode 100644 index 0198a5fe1b..0000000000 --- a/tee-worker/app-libs/oracle/src/oracles/exchange_rate_oracle.rs +++ /dev/null @@ -1,154 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - metrics_exporter::ExportMetrics, - traits::OracleSource, - types::{ExchangeRate, TradingInfo, TradingPair}, - Error, -}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use log::*; -use std::{ - sync::Arc, - thread, - time::{Duration, Instant}, -}; -use url::Url; - -#[allow(unused)] -pub struct ExchangeRateOracle { - oracle_source: OracleSourceType, - metrics_exporter: Arc, -} - -impl ExchangeRateOracle { - pub fn new(oracle_source: OracleSourceType, metrics_exporter: Arc) -> Self { - ExchangeRateOracle { oracle_source, metrics_exporter } - } -} - -pub trait GetExchangeRate { - /// Get the cryptocurrency/fiat_currency exchange rate - fn get_exchange_rate(&self, trading_pair: TradingPair) -> Result<(ExchangeRate, Url), Error>; -} - -impl GetExchangeRate - for ExchangeRateOracle -where - OracleSourceType: OracleSource, - MetricsExporter: ExportMetrics, -{ - fn get_exchange_rate(&self, trading_pair: TradingPair) -> Result<(ExchangeRate, Url), Error> { - let source_id = self.oracle_source.metrics_id(); - self.metrics_exporter.increment_number_requests(source_id.clone()); - - let base_url = self.oracle_source.base_url()?; - let root_certificates = self.oracle_source.root_certificates_content(); - let request_timeout = self.oracle_source.request_timeout(); - - debug!("Get exchange rate from URI: {}, trading pair: {:?}", base_url, trading_pair); - - let http_client = HttpClient::new( - SendWithCertificateVerification::new(root_certificates), - true, - request_timeout, - None, - None, - ); - let mut rest_client = RestClient::new(http_client, base_url.clone()); - - // Due to possible failures that may be temporarily this function tries to fetch the exchange rates `number_of_tries` times. - // If it still fails for the last attempt, then only in that case will it be considered a non-recoverable error. - let number_of_tries = 3; - let timer_start = Instant::now(); - - let mut tries = 0; - let result = loop { - tries += 1; - let exchange_result = self - .oracle_source - .execute_exchange_rate_request(&mut rest_client, trading_pair.clone()); - - match exchange_result { - Ok(exchange_rate) => { - self.metrics_exporter.record_response_time(source_id.clone(), timer_start); - self.metrics_exporter.update_exchange_rate( - source_id, - exchange_rate, - trading_pair, - ); - - debug!("Successfully executed exchange rate request"); - break Ok((exchange_rate, base_url)) - }, - Err(e) => - if tries < number_of_tries { - error!( - "Getting exchange rate from {} failed with {}, trying again in {:?}.", - &base_url, e, request_timeout - ); - debug!("Check that the API endpoint is available, for coingecko: https://status.coingecko.com/"); - thread::sleep( - request_timeout.unwrap_or_else(|| Duration::from_secs(number_of_tries)), - ); - } else { - error!( - "Getting exchange rate from {} failed {} times, latest error is: {}.", - &base_url, number_of_tries, &e - ); - break Err(e) - }, - } - }; - result - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::mock::{MetricsExporterMock, OracleSourceMock}; - - type TestOracle = ExchangeRateOracle; - - #[test] - fn get_exchange_rate_updates_metrics() { - let metrics_exporter = Arc::new(MetricsExporterMock::default()); - let test_client = TestOracle::new(OracleSourceMock {}, metrics_exporter.clone()); - - let trading_pair = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "USD".to_string() }; - let _bit_usd = test_client.get_exchange_rate(trading_pair.clone()).unwrap(); - - assert_eq!(1, metrics_exporter.get_number_request()); - assert_eq!(1, metrics_exporter.get_response_times().len()); - assert_eq!(1, metrics_exporter.get_exchange_rates().len()); - - let (metric_trading_pair, exchange_rate) = - metrics_exporter.get_exchange_rates().first().unwrap().clone(); - - assert_eq!(trading_pair, metric_trading_pair); - assert_eq!(ExchangeRate::from_num(42.3f32), exchange_rate); - } -} diff --git a/tee-worker/app-libs/oracle/src/oracles/mod.rs b/tee-worker/app-libs/oracle/src/oracles/mod.rs deleted file mode 100644 index d6100d2469..0000000000 --- a/tee-worker/app-libs/oracle/src/oracles/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -pub mod exchange_rate_oracle; -pub mod weather_oracle; diff --git a/tee-worker/app-libs/oracle/src/oracles/weather_oracle.rs b/tee-worker/app-libs/oracle/src/oracles/weather_oracle.rs deleted file mode 100644 index 66809f7f3a..0000000000 --- a/tee-worker/app-libs/oracle/src/oracles/weather_oracle.rs +++ /dev/null @@ -1,83 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{metrics_exporter::ExportMetrics, traits::OracleSource, types::WeatherInfo, Error}; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use log::*; -use std::sync::Arc; -use url::Url; - -#[allow(unused)] -pub struct WeatherOracle { - oracle_source: OracleSourceType, - metrics_exporter: Arc, -} - -impl WeatherOracle -where - OracleSourceType: OracleSource, -{ - pub fn new(oracle_source: OracleSourceType, metrics_exporter: Arc) -> Self { - WeatherOracle { oracle_source, metrics_exporter } - } - - pub fn get_base_url(&self) -> Result { - self.oracle_source.base_url() - } -} - -pub trait GetLongitude { - type LongitudeResult; - fn get_longitude(&self, weather_info: WeatherInfo) -> Self::LongitudeResult; -} - -impl GetLongitude - for WeatherOracle -where - OracleSourceType: OracleSource>, - MetricsExporter: ExportMetrics, -{ - type LongitudeResult = Result; - - fn get_longitude(&self, weather_info: WeatherInfo) -> Self::LongitudeResult { - let query = weather_info.weather_query.clone(); - - let base_url = self.oracle_source.base_url()?; - let root_certificates = self.oracle_source.root_certificates_content(); - - debug!("Get longitude from URI: {}, query: {:?}", base_url, query); - - let http_client = HttpClient::new( - SendWithCertificateVerification::new(root_certificates), - true, - self.oracle_source.request_timeout(), - None, - None, - ); - let mut rest_client = RestClient::new(http_client, base_url); - >::execute_request( - &mut rest_client, - weather_info, - ) - } -} diff --git a/tee-worker/app-libs/oracle/src/test.rs b/tee-worker/app-libs/oracle/src/test.rs deleted file mode 100644 index 8d083a18a0..0000000000 --- a/tee-worker/app-libs/oracle/src/test.rs +++ /dev/null @@ -1,125 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Integration tests for concrete exchange rate oracle implementations. -//! Uses real HTTP requests, so the sites must be available for these tests. - -use crate::{ - error::Error, - mock::MetricsExporterMock, - oracle_sources::{ - coin_gecko::CoinGeckoSource, coin_market_cap::CoinMarketCapSource, - weather_oracle_source::WeatherOracleSource, - }, - oracles::{ - exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - weather_oracle::{GetLongitude, WeatherOracle}, - }, - traits::OracleSource, - types::{TradingInfo, TradingPair, WeatherInfo, WeatherQuery}, -}; -use core::assert_matches::assert_matches; -use std::sync::Arc; -use substrate_fixed::transcendental::ZERO; - -type TestOracle = ExchangeRateOracle; -type TestWeatherOracle = WeatherOracle; - -#[test] -#[ignore = "requires API key for CoinMarketCap"] -fn get_exchange_rate_from_coin_market_cap_works() { - test_suite_exchange_rates::(); -} - -#[test] -#[ignore = "requires external coin gecko service, disabled temporarily"] -fn get_exchange_rate_from_coin_gecko_works() { - test_suite_exchange_rates::(); -} - -#[test] -fn get_longitude_from_open_meteo_works() { - let oracle = create_weather_oracle::(); - let weather_query = - WeatherQuery { latitude: "52.52".into(), longitude: "13.41".into(), hourly: "none".into() }; - // Todo: hourly param is temperature_2m to get temp or relativehumidity_2m to get humidity - let weather_info = WeatherInfo { weather_query }; - let expected_longitude = 13.41f32; - let response_longitude = - oracle.get_longitude(weather_info).expect("Can grab longitude from oracle"); - assert!((response_longitude - expected_longitude) < 0.5); -} - -#[test] -fn get_exchange_rate_for_undefined_coin_market_cap_crypto_currency_fails() { - get_exchange_rate_for_undefined_crypto_currency_fails::(); -} - -#[test] -fn get_exchange_rate_for_undefined_coin_gecko_crypto_currency_fails() { - get_exchange_rate_for_undefined_crypto_currency_fails::(); -} - -fn create_weather_oracle>( -) -> TestWeatherOracle { - let oracle_source = OracleSourceType::default(); - WeatherOracle::new(oracle_source, Arc::new(MetricsExporterMock::default())) -} - -fn create_exchange_rate_oracle>( -) -> TestOracle { - let oracle_source = OracleSourceType::default(); - ExchangeRateOracle::new(oracle_source, Arc::new(MetricsExporterMock::default())) -} - -fn get_exchange_rate_for_undefined_crypto_currency_fails< - OracleSourceType: OracleSource, ->() { - let oracle = create_exchange_rate_oracle::(); - let trading_pair = TradingPair { - crypto_currency: "invalid_coin".to_string(), - fiat_currency: "USD".to_string(), - }; - let result = oracle.get_exchange_rate(trading_pair); - assert_matches!(result, Err(Error::InvalidCryptoCurrencyId)); -} - -fn test_suite_exchange_rates>() { - let oracle = create_exchange_rate_oracle::(); - let dot_to_usd = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - let dot_usd = oracle.get_exchange_rate(dot_to_usd).unwrap().0; - assert!(dot_usd > 0f32); - let btc_to_usd = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "USD".to_string() }; - let bit_usd = oracle.get_exchange_rate(btc_to_usd).unwrap().0; - assert!(bit_usd > 0f32); - let dot_to_chf = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "CHF".to_string() }; - let dot_chf = oracle.get_exchange_rate(dot_to_chf).unwrap().0; - assert!(dot_chf > 0f32); - let bit_to_chf = - TradingPair { crypto_currency: "BTC".to_string(), fiat_currency: "CHF".to_string() }; - let bit_chf = oracle.get_exchange_rate(bit_to_chf).unwrap().0; - - // Ensure that get_exchange_rate returns a positive rate - assert!(dot_usd > ZERO); - - // Ensure that get_exchange_rate returns a valid value by checking - // that the values obtained for DOT/BIT from different exchange rates are the same - assert_eq!((dot_usd / bit_usd).round(), (dot_chf / bit_chf).round()); -} diff --git a/tee-worker/app-libs/oracle/src/traits.rs b/tee-worker/app-libs/oracle/src/traits.rs deleted file mode 100644 index 1ca1d21428..0000000000 --- a/tee-worker/app-libs/oracle/src/traits.rs +++ /dev/null @@ -1,55 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::{ - types::{ExchangeRate, TradingPair}, - Error, -}; -use core::time::Duration; -use itc_rest_client::{ - http_client::{HttpClient, SendWithCertificateVerification}, - rest_client::RestClient, -}; -use std::{string::String, vec::Vec}; -use url::Url; - -pub trait OracleSource: Default { - type OracleRequestResult; - - fn metrics_id(&self) -> String; - - fn request_timeout(&self) -> Option; - - fn base_url(&self) -> Result; - - /// The server's root certificate(s). A valid certificate is required to open a tls connection - fn root_certificates_content(&self) -> Vec; - - fn execute_exchange_rate_request( - &self, - rest_client: &mut RestClient>, - trading_pair: TradingPair, - ) -> Result; - - fn execute_request( - rest_client: &mut RestClient>, - source_info: OracleSourceInfo, - ) -> Self::OracleRequestResult; -} diff --git a/tee-worker/app-libs/oracle/src/types.rs b/tee-worker/app-libs/oracle/src/types.rs deleted file mode 100644 index ef969ccb90..0000000000 --- a/tee-worker/app-libs/oracle/src/types.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use codec::{Decode, Encode}; -use std::string::String; -use substrate_fixed::types::U32F32; - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct WeatherInfo { - pub weather_query: WeatherQuery, -} - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct WeatherQuery { - pub longitude: String, - pub latitude: String, - pub hourly: String, -} - -impl WeatherQuery { - pub fn key(self) -> String { - format!("{}/{}", self.latitude, self.longitude) - } -} - -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct TradingInfo { - pub trading_pair: TradingPair, - pub exchange_rate: ExchangeRate, -} -/// Market identifier for order -#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] -pub struct TradingPair { - pub crypto_currency: String, - pub fiat_currency: String, -} - -impl TradingPair { - pub fn key(self) -> String { - format!("{}/{}", self.crypto_currency, self.fiat_currency) - } -} - -/// TODO Fix https://github.com/integritee-network/pallets/issues/71 and get it from https://github.com/integritee-network/pallets.git -/// Teeracle types -pub type ExchangeRate = U32F32; -// pub type Coordinate = U32F32; diff --git a/tee-worker/cli/Cargo.toml b/tee-worker/cli/Cargo.toml index f3ad923d46..d82a66de76 100644 --- a/tee-worker/cli/Cargo.toml +++ b/tee-worker/cli/Cargo.toml @@ -63,7 +63,6 @@ sp-core-hashing = "6.0.0" [features] default = [] evm = ["ita-stf/evm_std", "pallet-evm"] -teeracle = [] sidechain = [] offchain-worker = [] production = [] diff --git a/tee-worker/cli/src/commands.rs b/tee-worker/cli/src/commands.rs index e01a79d930..17b5ea42c4 100644 --- a/tee-worker/cli/src/commands.rs +++ b/tee-worker/cli/src/commands.rs @@ -19,9 +19,6 @@ extern crate chrono; use crate::{base_cli::BaseCommand, trusted_cli::TrustedCli, Cli, CliResult, CliResultOk}; use clap::Subcommand; -#[cfg(feature = "teeracle")] -use crate::oracle::OracleCommand; - use crate::attesteer::AttesteerCommand; #[derive(Subcommand)] @@ -33,11 +30,6 @@ pub enum Commands { #[clap(after_help = "stf subcommands depend on the stf crate this has been built against")] Trusted(TrustedCli), - /// Subcommands for the oracle. - #[cfg(feature = "teeracle")] - #[clap(subcommand)] - Oracle(OracleCommand), - /// Subcommand for the attesteer. #[clap(subcommand)] Attesteer(AttesteerCommand), @@ -47,11 +39,6 @@ pub fn match_command(cli: &Cli) -> CliResult { match &cli.command { Commands::Base(cmd) => cmd.run(cli), Commands::Trusted(trusted_cli) => trusted_cli.run(cli), - #[cfg(feature = "teeracle")] - Commands::Oracle(cmd) => { - cmd.run(cli); - Ok(CliResultOk::None) - }, Commands::Attesteer(cmd) => { cmd.run(cli); Ok(CliResultOk::None) diff --git a/tee-worker/cli/src/lib.rs b/tee-worker/cli/src/lib.rs index 5c39e1ff19..c7b2bb5b63 100644 --- a/tee-worker/cli/src/lib.rs +++ b/tee-worker/cli/src/lib.rs @@ -35,8 +35,6 @@ mod command_utils; mod error; #[cfg(feature = "evm")] mod evm; -#[cfg(feature = "teeracle")] -mod oracle; mod trusted_base_cli; mod trusted_cli; mod trusted_command_utils; @@ -61,7 +59,6 @@ pub(crate) const ED25519_KEY_TYPE: KeyTypeId = KeyTypeId(*b"ed25"); #[clap(version = VERSION)] #[clap(author = "Trust Computing GmbH ")] #[clap(about = "cli tool to interact with litentry-parachain and workers", long_about = None)] -#[cfg_attr(feature = "teeracle", clap(about = "interact with litentry-parachain and teeracle", long_about = None))] #[cfg_attr(feature = "sidechain", clap(about = "interact with litentry-parachain and sidechain", long_about = None))] #[cfg_attr(feature = "offchain-worker", clap(about = "interact with litentry-parachain and offchain-worker", long_about = None))] #[clap(after_help = "stf subcommands depend on the stf crate this has been built against")] diff --git a/tee-worker/cli/src/oracle/commands/add_to_whitelist.rs b/tee-worker/cli/src/oracle/commands/add_to_whitelist.rs deleted file mode 100644 index 98afeb801d..0000000000 --- a/tee-worker/cli/src/oracle/commands/add_to_whitelist.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - command_utils::{get_chain_api, get_pair_from_str, mrenclave_from_base58}, - Cli, -}; -use itp_node_api::api_client::{ADD_TO_WHITELIST, TEERACLE}; -use substrate_api_client::{ - ac_compose_macros::{compose_call, compose_extrinsic}, - SubmitAndWatch, XtStatus, -}; - -/// Add a trusted market data source to the on-chain whitelist. -#[derive(Debug, Clone, Parser)] -pub struct AddToWhitelistCmd { - /// Sender's on-chain AccountId in ss58check format. - /// - /// It has to be a sudo account. - from: String, - - /// Market data URL - source: String, - - /// MRENCLAVE of the oracle worker base58 encoded. - mrenclave: String, -} - -impl AddToWhitelistCmd { - pub fn run(&self, cli: &Cli) { - let mut api = get_chain_api(cli); - let mrenclave = mrenclave_from_base58(&self.mrenclave); - let from = get_pair_from_str(&self.from); - - let market_data_source = self.source.clone(); - - api.set_signer(from.into()); - - let call = compose_call!( - api.metadata(), - TEERACLE, - ADD_TO_WHITELIST, - market_data_source, - mrenclave - ); - - // compose the extrinsic - let xt = compose_extrinsic!(api, "Sudo", "sudo", call); - - let report = api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).unwrap(); - println!("[+] Add to whitelist got finalized. Hash: {:?}\n", report.extrinsic_hash); - } -} diff --git a/tee-worker/cli/src/oracle/commands/listen_to_exchange.rs b/tee-worker/cli/src/oracle/commands/listen_to_exchange.rs deleted file mode 100644 index 181be4febd..0000000000 --- a/tee-worker/cli/src/oracle/commands/listen_to_exchange.rs +++ /dev/null @@ -1,79 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{command_utils::get_chain_api, Cli}; -use itp_node_api::api_client::ParentchainApi; -use itp_time_utils::{duration_now, remaining_time}; -use log::{debug, info, trace}; -use my_node_runtime::{Hash, RuntimeEvent}; -use std::time::Duration; -use substrate_api_client::SubscribeEvents; - -/// Listen to exchange rate events. -#[derive(Debug, Clone, Parser)] -pub struct ListenToExchangeRateEventsCmd { - /// Listen for `duration` in seconds. - duration: u64, -} - -impl ListenToExchangeRateEventsCmd { - pub fn run(&self, cli: &Cli) { - let api = get_chain_api(cli); - let duration = Duration::from_secs(self.duration); - - let count = count_exchange_rate_update_events(&api, duration); - - println!("Number of ExchangeRateUpdated events received : "); - println!(" EVENTS_COUNT: {}", count); - } -} - -pub fn count_exchange_rate_update_events(api: &ParentchainApi, duration: Duration) -> u32 { - let stop = duration_now() + duration; - - //subscribe to events - let mut subscription = api.subscribe_events().unwrap(); - let mut count = 0; - - while remaining_time(stop).unwrap_or_default() > Duration::ZERO { - let events_result = subscription.next_events::().unwrap(); - if let Ok(events) = events_result { - for event_record in &events { - info!("received event {:?}", event_record.event); - if let RuntimeEvent::Teeracle(event) = &event_record.event { - match &event { - my_node_runtime::pallet_teeracle::Event::ExchangeRateUpdated( - data_source, - trading_pair, - exchange_rate, - ) => { - count += 1; - debug!("Received ExchangeRateUpdated event"); - println!( - "ExchangeRateUpdated: TRADING_PAIR : {}, SRC : {}, VALUE :{:?}", - trading_pair, data_source, exchange_rate - ); - }, - _ => trace!("ignoring teeracle event: {:?}", event), - } - } - } - } - } - debug!("Received {} ExchangeRateUpdated event(s) in total", count); - count -} diff --git a/tee-worker/cli/src/oracle/commands/listen_to_oracle.rs b/tee-worker/cli/src/oracle/commands/listen_to_oracle.rs deleted file mode 100644 index 87cc334040..0000000000 --- a/tee-worker/cli/src/oracle/commands/listen_to_oracle.rs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{command_utils::get_chain_api, Cli}; -use itp_node_api::api_client::ParentchainApi; -use itp_time_utils::{duration_now, remaining_time}; -use log::{debug, info}; -use my_node_runtime::{Hash, RuntimeEvent}; -use std::time::Duration; -use substrate_api_client::{ac_node_api::EventRecord, SubscribeEvents}; - -/// Listen to exchange rate events. -#[derive(Debug, Clone, Parser)] -pub struct ListenToOracleEventsCmd { - /// Listen for `duration` in seconds. - duration: u64, -} - -type EventCount = u32; -type Event = EventRecord; - -impl ListenToOracleEventsCmd { - pub fn run(&self, cli: &Cli) { - let api = get_chain_api(cli); - let duration = Duration::from_secs(self.duration); - let count = count_oracle_update_events(&api, duration); - println!("Number of Oracle events received : "); - println!(" EVENTS_COUNT: {}", count); - } -} - -fn count_oracle_update_events(api: &ParentchainApi, duration: Duration) -> EventCount { - let stop = duration_now() + duration; - - //subscribe to events - let mut subscription = api.subscribe_events().unwrap(); - let mut count = 0; - - while remaining_time(stop).unwrap_or_default() > Duration::ZERO { - let events_result = subscription.next_events::(); - let event_count = match events_result { - Some(Ok(event_records)) => { - debug!("Could not successfully decode event_bytes {:?}", event_records); - report_event_count(event_records) - }, - _ => 0, - }; - count += event_count; - } - debug!("Received {} ExchangeRateUpdated event(s) in total", count); - count -} - -fn report_event_count(event_records: Vec) -> EventCount { - let mut count = 0; - event_records.iter().for_each(|event_record| { - info!("received event {:?}", event_record.event); - if let RuntimeEvent::Teeracle(event) = &event_record.event { - match &event { - my_node_runtime::pallet_teeracle::Event::OracleUpdated( - oracle_data_name, - data_source, - ) => { - count += 1; - debug!("Received OracleUpdated event"); - println!( - "OracleUpdated: ORACLE_NAME : {}, SRC : {}", - oracle_data_name, data_source - ); - }, - // Can just remove this and ignore handling this case - _ => debug!("ignoring teeracle event: {:?}", event), - } - } - }); - count -} diff --git a/tee-worker/cli/src/oracle/commands/mod.rs b/tee-worker/cli/src/oracle/commands/mod.rs deleted file mode 100644 index 22b0a326c6..0000000000 --- a/tee-worker/cli/src/oracle/commands/mod.rs +++ /dev/null @@ -1,25 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -mod add_to_whitelist; -mod listen_to_exchange; -mod listen_to_oracle; - -pub use self::{ - add_to_whitelist::AddToWhitelistCmd, listen_to_exchange::ListenToExchangeRateEventsCmd, - listen_to_oracle::ListenToOracleEventsCmd, -}; diff --git a/tee-worker/cli/src/oracle/mod.rs b/tee-worker/cli/src/oracle/mod.rs deleted file mode 100644 index e12f117cd4..0000000000 --- a/tee-worker/cli/src/oracle/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Add cli commands for the oracle -//! -//! Todo: This shall be a standalone crate in app-libs/oracle. However, this needs: -//! https://github.com/integritee-network/worker/issues/852 - -use crate::Cli; -use commands::{AddToWhitelistCmd, ListenToExchangeRateEventsCmd, ListenToOracleEventsCmd}; - -mod commands; - -/// Oracle subcommands for the cli. -#[derive(Debug, clap::Subcommand)] -pub enum OracleCommand { - /// Add a market source to the teeracle's whitelist. - AddToWhitelist(AddToWhitelistCmd), - - /// Listen to exchange rate events - ListenToExchangeRateEvents(ListenToExchangeRateEventsCmd), - - /// Listen to all oracles event updates - ListenToOracleEvents(ListenToOracleEventsCmd), -} - -impl OracleCommand { - pub fn run(&self, cli: &Cli) { - match self { - OracleCommand::AddToWhitelist(cmd) => cmd.run(cli), - OracleCommand::ListenToExchangeRateEvents(cmd) => cmd.run(cli), - OracleCommand::ListenToOracleEvents(cmd) => cmd.run(cli), - } - } -} diff --git a/tee-worker/core-primitives/enclave-api/ffi/src/lib.rs b/tee-worker/core-primitives/enclave-api/ffi/src/lib.rs index 2dbb8fb016..f70a36d54b 100644 --- a/tee-worker/core-primitives/enclave-api/ffi/src/lib.rs +++ b/tee-worker/core-primitives/enclave-api/ffi/src/lib.rs @@ -215,30 +215,6 @@ extern "C" { response_len: u32, ) -> sgx_status_t; - pub fn update_market_data_xt( - eid: sgx_enclave_id_t, - retval: *mut sgx_status_t, - crypto_currency: *const u8, - crypto_currency_size: u32, - fiat_currency: *const u8, - fiat_currency_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, - ) -> sgx_status_t; - - pub fn update_weather_data_xt( - eid: sgx_enclave_id_t, - retval: *mut sgx_status_t, - weather_info_longitude: *const u8, - weather_info_longitude_size: u32, - weather_info_latitude: *const u8, - weather_info_latitude_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, - ) -> sgx_status_t; - pub fn run_state_provisioning_server( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, diff --git a/tee-worker/core-primitives/enclave-api/src/lib.rs b/tee-worker/core-primitives/enclave-api/src/lib.rs index 38c810624f..463608e111 100644 --- a/tee-worker/core-primitives/enclave-api/src/lib.rs +++ b/tee-worker/core-primitives/enclave-api/src/lib.rs @@ -19,7 +19,6 @@ pub mod enclave_test; pub mod error; pub mod remote_attestation; pub mod sidechain; -pub mod teeracle_api; pub mod utils; #[cfg(feature = "implement-ffi")] diff --git a/tee-worker/core-primitives/enclave-api/src/teeracle_api.rs b/tee-worker/core-primitives/enclave-api/src/teeracle_api.rs deleted file mode 100644 index 530e2ff127..0000000000 --- a/tee-worker/core-primitives/enclave-api/src/teeracle_api.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::EnclaveResult; - -pub trait TeeracleApi: Send + Sync + 'static { - /// Update the currency market data for the token oracle. - fn update_market_data_xt( - &self, - crypto_currency: &str, - fiat_currency: &str, - ) -> EnclaveResult>; - - /// Update weather data for the corresponding coordinates. - fn update_weather_data_xt(&self, longitude: &str, latitude: &str) -> EnclaveResult>; -} - -#[cfg(feature = "implement-ffi")] -mod impl_ffi { - use super::TeeracleApi; - use crate::{error::Error, Enclave, EnclaveResult}; - use codec::Encode; - use frame_support::ensure; - use itp_enclave_api_ffi as ffi; - use log::*; - use sgx_types::*; - impl TeeracleApi for Enclave { - fn update_market_data_xt( - &self, - crypto_currency: &str, - fiat_currency: &str, - ) -> EnclaveResult> { - info!( - "TeeracleApi update_market_data_xt in with crypto {} and fiat {}", - crypto_currency, fiat_currency - ); - let mut retval = sgx_status_t::SGX_SUCCESS; - let response_max_len = 8192; - let mut response: Vec = vec![0u8; response_max_len as usize]; - let mut response_len: u32 = 0; - - let crypto_curr = crypto_currency.encode(); - let fiat_curr = fiat_currency.encode(); - - let res = unsafe { - ffi::update_market_data_xt( - self.eid, - &mut retval, - crypto_curr.as_ptr(), - crypto_curr.len() as u32, - fiat_curr.as_ptr(), - fiat_curr.len() as u32, - response.as_mut_ptr(), - response_max_len, - &mut response_len as *mut u32, - ) - }; - - ensure!(res == sgx_status_t::SGX_SUCCESS, Error::Sgx(res)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - - Ok(Vec::from(&response[..response_len as usize])) - } - fn update_weather_data_xt( - &self, - longitude: &str, - latitude: &str, - ) -> EnclaveResult> { - info!( - "TeeracleApi update_weather_data_xt in with latitude: {}, longitude: {}", - latitude, longitude - ); - let mut retval = sgx_status_t::SGX_SUCCESS; - let response_max_len = 8192; - let mut response: Vec = vec![0u8; response_max_len as usize]; - let mut response_len: u32 = 0; - - let longitude_encoded: Vec = longitude.encode(); - let latitude_encoded: Vec = latitude.encode(); - - let res = unsafe { - ffi::update_weather_data_xt( - self.eid, - &mut retval, - longitude_encoded.as_ptr(), - longitude_encoded.len() as u32, - latitude_encoded.as_ptr(), - latitude_encoded.len() as u32, - response.as_mut_ptr(), - response_max_len, - &mut response_len as *mut u32, - ) - }; - - ensure!(res == sgx_status_t::SGX_SUCCESS, Error::Sgx(res)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - Ok(Vec::from(&response[..response_len as usize])) - } - } -} diff --git a/tee-worker/core-primitives/enclave-metrics/src/lib.rs b/tee-worker/core-primitives/enclave-metrics/src/lib.rs index 045c9f0c59..c2501acab5 100644 --- a/tee-worker/core-primitives/enclave-metrics/src/lib.rs +++ b/tee-worker/core-primitives/enclave-metrics/src/lib.rs @@ -28,10 +28,6 @@ use codec::{Decode, Encode}; use core::time::Duration; use lc_stf_task_sender::RequestType; use std::{boxed::Box, string::String}; -use substrate_fixed::types::U32F32; - -// FIXME: Copied from ita-oracle because of cyclic deps. Should be removed after integritee-network/pallets#71 -pub type ExchangeRate = U32F32; #[derive(Encode, Decode, Debug)] pub enum EnclaveMetric { @@ -39,7 +35,6 @@ pub enum EnclaveMetric { TopPoolSizeSet(u64), TopPoolSizeIncrement, TopPoolSizeDecrement, - ExchangeRateOracle(ExchangeRateOracleMetric), StfTaskExecutionTime(Box, f64), SuccessfulTrustedOperationIncrement(String), FailedTrustedOperationIncrement(String), @@ -49,22 +44,4 @@ pub enum EnclaveMetric { SidechainSlotStfExecutionTime(Duration), SidechainSlotBlockCompositionTime(Duration), SidechainBlockBroadcastingTime(Duration), - // OracleMetric(OracleMetric), -} - -#[derive(Encode, Decode, Debug)] -pub enum ExchangeRateOracleMetric { - /// Exchange Rate from CoinGecko - (Source, TradingPair, ExchangeRate) - ExchangeRate(String, String, ExchangeRate), - /// Response time of the request in [ms]. (Source, ResponseTime) - ResponseTime(String, u128), - /// Increment the number of requests (Source) - NumberRequestsIncrement(String), -} - -#[derive(Encode, Decode, Debug)] -pub enum OracleMetric { - OracleSpecificMetric(MetricsInfo), - ResponseTime(String, u128), - NumberRequestsIncrement(String), } diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index e6e7c3757e..cbec7b12a8 100644 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -24,14 +24,11 @@ pub mod chain; pub mod pallet_teebag; // TODO: part of P-487 - these will be removed anyway so I haven't spent time adjust them - -// pub mod pallet_teeracle; pub mod pallet_teerex; pub use account::*; pub use chain::*; pub use pallet_teebag::*; -// pub use pallet_teeracle::*; pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs deleted file mode 100644 index 3f1ad2d198..0000000000 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teeracle.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub const TEERACLE: &str = "Teeracle"; -pub const ADD_TO_WHITELIST: &str = "add_to_whitelist"; diff --git a/tee-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs b/tee-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs deleted file mode 100644 index 0d10003514..0000000000 --- a/tee-worker/core-primitives/node-api/metadata/src/pallet_teeracle.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, NodeMetadata}; - -/// Pallet' name: -pub const TEERACLE: &str = "Teeracle"; - -pub trait TeeracleCallIndexes { - fn add_to_whitelist_call_indexes(&self) -> Result<[u8; 2]>; - fn remove_from_whitelist_call_indexes(&self) -> Result<[u8; 2]>; - fn update_exchange_rate_call_indexes(&self) -> Result<[u8; 2]>; - fn update_oracle_call_indexes(&self) -> Result<[u8; 2]>; -} - -impl TeeracleCallIndexes for NodeMetadata { - fn add_to_whitelist_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "add_to_whitelist") - } - - fn remove_from_whitelist_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "remove_from_whitelist") - } - - fn update_exchange_rate_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "update_exchange_rate") - } - - fn update_oracle_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEERACLE, "update_oracle") - } -} diff --git a/tee-worker/core-primitives/settings/Cargo.toml b/tee-worker/core-primitives/settings/Cargo.toml index 2d36658a7e..3aadf1e5e5 100644 --- a/tee-worker/core-primitives/settings/Cargo.toml +++ b/tee-worker/core-primitives/settings/Cargo.toml @@ -13,4 +13,3 @@ production = [ ] sidechain = [] offchain-worker = [] -teeracle = [] diff --git a/tee-worker/core-primitives/settings/src/lib.rs b/tee-worker/core-primitives/settings/src/lib.rs index 81a6bd440a..484a3e97fa 100644 --- a/tee-worker/core-primitives/settings/src/lib.rs +++ b/tee-worker/core-primitives/settings/src/lib.rs @@ -19,14 +19,8 @@ #![no_std] -#[cfg(any( - all(feature = "sidechain", feature = "offchain-worker"), - all(feature = "sidechain", feature = "teeracle"), - all(feature = "teeracle", feature = "offchain-worker") -))] -compile_error!( - "feature \"sidechain\" , \"offchain-worker\" or \"teeracle\" cannot be enabled at the same time" -); +#[cfg(any(all(feature = "sidechain", feature = "offchain-worker"),))] +compile_error!("feature \"sidechain\" or \"offchain-worker\" cannot be enabled at the same time"); pub mod worker_mode; @@ -104,17 +98,3 @@ pub mod sidechain { pub static SLOT_DURATION: Duration = Duration::from_millis(6000); } - -/// Settings concerning the enclave -pub mod enclave {} - -/// Settings for the Teeracle -pub mod teeracle { - use core::time::Duration; - // Send extrinsic to update market exchange rate on the parentchain once per day - pub static DEFAULT_MARKET_DATA_UPDATE_INTERVAL: Duration = ONE_DAY; - - pub static ONE_DAY: Duration = Duration::from_secs(86400); - - pub static THIRTY_MINUTES: Duration = Duration::from_secs(1800); -} diff --git a/tee-worker/core-primitives/settings/src/worker_mode.rs b/tee-worker/core-primitives/settings/src/worker_mode.rs index f1563a2232..20f9e35670 100644 --- a/tee-worker/core-primitives/settings/src/worker_mode.rs +++ b/tee-worker/core-primitives/settings/src/worker_mode.rs @@ -31,13 +31,6 @@ impl ProvideWorkerMode for WorkerModeProvider { } } -#[cfg(feature = "teeracle")] -impl ProvideWorkerMode for WorkerModeProvider { - fn worker_mode() -> WorkerMode { - WorkerMode::Teeracle - } -} - #[cfg(feature = "sidechain")] impl ProvideWorkerMode for WorkerModeProvider { fn worker_mode() -> WorkerMode { @@ -46,7 +39,7 @@ impl ProvideWorkerMode for WorkerModeProvider { } // Default to `Sidechain` worker mode when no cargo features are set. -#[cfg(not(any(feature = "sidechain", feature = "teeracle", feature = "offchain-worker")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] impl ProvideWorkerMode for WorkerModeProvider { fn worker_mode() -> WorkerMode { WorkerMode::Sidechain diff --git a/tee-worker/core-primitives/top-pool-author/Cargo.toml b/tee-worker/core-primitives/top-pool-author/Cargo.toml index 191ae19ea0..b5f8d264e4 100644 --- a/tee-worker/core-primitives/top-pool-author/Cargo.toml +++ b/tee-worker/core-primitives/top-pool-author/Cargo.toml @@ -75,4 +75,3 @@ test = ["itp-test/sgx", "itp-top-pool/mocks"] mocks = ["lazy_static"] sidechain = [] offchain-worker = [] -teeracle = [] diff --git a/tee-worker/core-primitives/top-pool-author/src/author.rs b/tee-worker/core-primitives/top-pool-author/src/author.rs index 08cbd61ff7..a123d72491 100644 --- a/tee-worker/core-primitives/top-pool-author/src/author.rs +++ b/tee-worker/core-primitives/top-pool-author/src/author.rs @@ -68,15 +68,10 @@ pub type AuthorTopFilter = crate::top_filter::IndirectCallsOnlyFilter = crate::top_filter::DenyAllFilter; -#[cfg(feature = "teeracle")] // Teeracle currently does not process any trusted operations -pub type AuthorTopFilter = crate::top_filter::DenyAllFilter; -#[cfg(feature = "teeracle")] -pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; - -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker", feature = "teeracle")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] pub type AuthorTopFilter = crate::top_filter::CallsOnlyFilter; -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker", feature = "teeracle")))] +#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; /// Currently we treat all RPC operations as externals. diff --git a/tee-worker/enclave-runtime/Cargo.lock b/tee-worker/enclave-runtime/Cargo.lock index 9844b6a377..475206c1fc 100644 --- a/tee-worker/enclave-runtime/Cargo.lock +++ b/tee-worker/enclave-runtime/Cargo.lock @@ -888,7 +888,6 @@ dependencies = [ "frame-system", "hex 0.4.3", "ipfs-unixfs", - "ita-oracle", "ita-parentchain-interface", "ita-sgx-runtime", "ita-stf", @@ -1871,23 +1870,6 @@ dependencies = [ "sha2 0.9.9", ] -[[package]] -name = "ita-oracle" -version = "0.9.0" -dependencies = [ - "itc-rest-client", - "itp-enclave-metrics", - "itp-ocall-api", - "lazy_static", - "log", - "parity-scale-codec", - "serde 1.0.193", - "sgx_tstd", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", - "thiserror", - "url", -] - [[package]] name = "ita-parentchain-interface" version = "0.9.0" diff --git a/tee-worker/enclave-runtime/Cargo.toml b/tee-worker/enclave-runtime/Cargo.toml index 6ac3c843b0..6b79f96a1a 100644 --- a/tee-worker/enclave-runtime/Cargo.toml +++ b/tee-worker/enclave-runtime/Cargo.toml @@ -31,11 +31,6 @@ offchain-worker = [ "itp-settings/offchain-worker", "itp-top-pool-author/offchain-worker", ] -teeracle = [ - "ita-oracle", - "itp-settings/teeracle", - "itp-top-pool-author/teeracle", -] test = [ "ita-stf/test", "itc-parentchain/test", @@ -98,7 +93,6 @@ multibase = { default-features = false, git = "https://github.com/whalelephant/r teerex-primitives = { default-features = false, git = "https://github.com/integritee-network/pallets.git", branch = "sdk-v0.12.0-polkadot-v0.9.42" } # local deps -ita-oracle = { path = "../app-libs/oracle", default-features = false, optional = true, features = ["sgx"] } ita-parentchain-interface = { path = "../app-libs/parentchain-interface", default-features = false, features = ["sgx"] } ita-sgx-runtime = { path = "../app-libs/sgx-runtime", default-features = false } ita-stf = { path = "../app-libs/stf", default-features = false, features = ["sgx"] } diff --git a/tee-worker/enclave-runtime/Enclave.edl b/tee-worker/enclave-runtime/Enclave.edl index 04c02fea61..80d878511b 100644 --- a/tee-worker/enclave-runtime/Enclave.edl +++ b/tee-worker/enclave-runtime/Enclave.edl @@ -140,20 +140,6 @@ enclave { [out] uint32_t* unchecked_extrinsic_size ); - public sgx_status_t update_market_data_xt( - [in, size=crypto_currency_size] uint8_t* crypto_currency, uint32_t crypto_currency_size, - [in, size=fiat_currency_size] uint8_t* fiat_currency, uint32_t fiat_currency_size, - [out, size=unchecked_extrinsic_max_size] uint8_t* unchecked_extrinsic, uint32_t unchecked_extrinsic_max_size, - [out] uint32_t* unchecked_extrinsic_size - ); - - public sgx_status_t update_weather_data_xt( - [in, size=weather_info_logitude_size] uint8_t* weather_info_logitude, uint32_t weather_info_logitude_size, - [in, size=weather_info_latitude_size] uint8_t* weather_info_latitude, uint32_t weather_info_latitude_size, - [out, size=unchecked_extrinsic_max_size] uint8_t* unchecked_extrinsic, uint32_t unchecked_extrinsic_max_size, - [out] uint32_t* unchecked_extrinsic_size - ); - public sgx_status_t dump_ias_ra_cert_to_disk(); public sgx_status_t dump_dcap_ra_cert_to_disk([in] const sgx_target_info_t* quoting_enclave_target_info, uint32_t quote_size); diff --git a/tee-worker/enclave-runtime/src/empty_impls.rs b/tee-worker/enclave-runtime/src/empty_impls.rs index e401fa8d05..e011e4d19c 100644 --- a/tee-worker/enclave-runtime/src/empty_impls.rs +++ b/tee-worker/enclave-runtime/src/empty_impls.rs @@ -22,35 +22,3 @@ pub extern "C" fn test_main_entrance() -> sgx_types::size_t { unreachable!("Tests are not available when compiled in production mode.") } - -/// Empty Teeracle market data implementation. -#[cfg(not(feature = "teeracle"))] -#[no_mangle] -#[allow(clippy::unreachable)] -pub unsafe extern "C" fn update_market_data_xt( - _crypto_currency_ptr: *const u8, - _crypto_currency_size: u32, - _fiat_currency_ptr: *const u8, - _fiat_currency_size: u32, - _unchecked_extrinsic: *mut u8, - _unchecked_extrinsic_max_size: u32, - _unchecked_extrinsic_size: *mut u32, -) -> sgx_types::sgx_status_t { - unreachable!("Cannot update market data, teeracle feature is not enabled.") -} - -/// Empty Teeracle Weather data implementation. -#[cfg(not(feature = "teeracle"))] -#[no_mangle] -#[allow(clippy::unreachable)] -pub unsafe extern "C" fn update_weather_data_xt( - _weather_info_longitude: *const u8, - _weather_info_longitude_size: u32, - _weather_info_latitude: *const u8, - _weather_info_latitude_size: u32, - _unchecked_extrinsic: *mut u8, - _unchecked_extrinsic_max_size: u32, - _unchecked_extrinsic_size: *mut u32, -) -> sgx_types::sgx_status_t { - unreachable!("Cannot update weather data, teeracle feature is not enabled.") -} diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs index f297c4960e..aeeec12a5b 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs @@ -100,8 +100,6 @@ impl IntegriteeParachainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => - Arc::new(IntegriteeParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs index b5ae349479..207115d47f 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs @@ -99,8 +99,6 @@ impl IntegriteeSolochainHandler { extrinsics_factory.clone(), )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - WorkerMode::Teeracle => - Arc::new(IntegriteeParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs index bf24f6fdd4..e4a08cce6d 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs @@ -104,8 +104,6 @@ impl TargetAParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetAParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs index f5cf2ae8ff..e26ce6833d 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs @@ -97,8 +97,6 @@ impl TargetASolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetAParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs index be44224c65..36d83a0e06 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs @@ -104,8 +104,6 @@ impl TargetBParachainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetBParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let parachain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs b/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs index 842baa8129..015ff2cea6 100644 --- a/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs +++ b/tee-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs @@ -97,8 +97,6 @@ impl TargetBSolochainHandler { )?, WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - WorkerMode::Teeracle => - Arc::new(TargetBParentchainBlockImportDispatcher::new_empty_dispatcher()), }; let solochain_handler = Self { diff --git a/tee-worker/enclave-runtime/src/lib.rs b/tee-worker/enclave-runtime/src/lib.rs index 208dcb50f5..c87c3e3b22 100644 --- a/tee-worker/enclave-runtime/src/lib.rs +++ b/tee-worker/enclave-runtime/src/lib.rs @@ -71,7 +71,7 @@ use itp_component_container::ComponentGetter; use itp_import_queue::PushToQueue; use itp_node_api::metadata::NodeMetadata; use itp_nonce_cache::{MutateNonce, Nonce}; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; +use itp_settings::worker_mode::{ProvideWorkerMode, WorkerModeProvider}; use itp_sgx_crypto::key_repository::AccessPubkey; use itp_storage::{StorageProof, StorageProofChecker}; use itp_types::{ShardIdentifier, SignedBlock}; @@ -102,9 +102,6 @@ mod sync; mod tls_ra; pub mod top_pool_execution; -#[cfg(feature = "teeracle")] -pub mod teeracle; - #[cfg(feature = "test")] pub mod test; @@ -590,10 +587,6 @@ fn dispatch_parentchain_blocks_for_import id: &ParentchainId, is_syncing: bool, ) -> Result<()> { - if WorkerModeProvider::worker_mode() == WorkerMode::Teeracle { - trace!("Not importing any parentchain blocks"); - return Ok(()) - } trace!( "[{:?}] Dispatching Import of {} blocks and {} events", id, diff --git a/tee-worker/enclave-runtime/src/teeracle/mod.rs b/tee-worker/enclave-runtime/src/teeracle/mod.rs deleted file mode 100644 index c38dd27c2e..0000000000 --- a/tee-worker/enclave-runtime/src/teeracle/mod.rs +++ /dev/null @@ -1,279 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - error::{Error, Result}, - initialization::global_components::GLOBAL_OCALL_API_COMPONENT, - utils::{ - get_extrinsic_factory_from_integritee_solo_or_parachain, - get_node_metadata_repository_from_integritee_solo_or_parachain, - }, -}; -use codec::{Decode, Encode}; -use core::slice; -use ita_oracle::{ - create_coin_gecko_oracle, create_coin_market_cap_oracle, create_open_meteo_weather_oracle, - metrics_exporter::ExportMetrics, - oracles::{ - exchange_rate_oracle::{ExchangeRateOracle, GetExchangeRate}, - weather_oracle::{GetLongitude, WeatherOracle}, - }, - traits::OracleSource, - types::{TradingInfo, TradingPair, WeatherInfo, WeatherQuery}, -}; -use itp_component_container::ComponentGetter; -use itp_extrinsics_factory::CreateExtrinsics; -use itp_node_api::metadata::{pallet_teeracle::TeeracleCallIndexes, provider::AccessNodeMetadata}; -use itp_types::OpaqueCall; -use itp_utils::write_slice_and_whitespace_pad; -use log::*; -use sgx_types::sgx_status_t; -use sp_runtime::OpaqueExtrinsic; -use std::{string::String, vec::Vec}; - -fn update_weather_data_internal(weather_info: WeatherInfo) -> Result> { - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - - let mut extrinsic_calls: Vec = Vec::new(); - - let open_meteo_weather_oracle = create_open_meteo_weather_oracle(ocall_api); - - match get_longitude(weather_info, open_meteo_weather_oracle) { - Ok(opaque_call) => extrinsic_calls.push(opaque_call), - Err(e) => { - error!("[-] Failed to get the newest longitude from OpenMeteo. {:?}", e); - }, - }; - let extrinsics = extrinsics_factory.create_extrinsics(extrinsic_calls.as_slice(), None)?; - Ok(extrinsics) -} - -fn get_longitude( - weather_info: WeatherInfo, - oracle: WeatherOracle, -) -> Result -where - OracleSourceType: OracleSource< - WeatherInfo, - OracleRequestResult = std::result::Result, - >, - MetricsExporter: ExportMetrics, -{ - let longitude = - oracle.get_longitude(weather_info.clone()).map_err(|e| Error::Other(e.into()))?; - - let base_url = oracle.get_base_url().map_err(|e| Error::Other(e.into()))?; - let source_base_url = base_url.as_str(); - - println!("Update the longitude: {}, for source {}", longitude, source_base_url); - - let node_metadata_repository = - get_node_metadata_repository_from_integritee_solo_or_parachain()?; - - let call_ids = node_metadata_repository - .get_from_metadata(|m| m.update_oracle_call_indexes()) - .map_err(Error::NodeMetadataProvider)? - .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - let call = OpaqueCall::from_tuple(&( - call_ids, - weather_info.weather_query.key().as_bytes().to_vec(), - source_base_url.as_bytes().to_vec(), - longitude.encode(), - )); - - Ok(call) -} - -#[no_mangle] -pub unsafe extern "C" fn update_weather_data_xt( - weather_info_longitude: *const u8, - weather_info_longitude_size: u32, - weather_info_latitude: *const u8, - weather_info_latitude_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, -) -> sgx_status_t { - let mut weather_info_longitude_slice = - slice::from_raw_parts(weather_info_longitude, weather_info_longitude_size as usize); - let longitude = match String::decode(&mut weather_info_longitude_slice) { - Ok(val) => val, - Err(e) => { - error!("Could not decode longitude: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let mut weather_info_latitude_slice = - slice::from_raw_parts(weather_info_latitude, weather_info_latitude_size as usize); - let latitude = match String::decode(&mut weather_info_latitude_slice) { - Ok(val) => val, - Err(e) => { - error!("Could not decode latitude: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let weather_query = WeatherQuery { longitude, latitude, hourly: " ".into() }; - let weather_info = WeatherInfo { weather_query }; - - let extrinsics = match update_weather_data_internal(weather_info) { - Ok(xts) => xts, - Err(e) => { - error!("Updating weather info failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let extrinsic_slice = - slice::from_raw_parts_mut(unchecked_extrinsic, unchecked_extrinsic_max_size as usize); - - // Save created extrinsic as slice in the return value unchecked_extrinsic. - *unchecked_extrinsic_size = - match write_slice_and_whitespace_pad(extrinsic_slice, extrinsics.encode()) { - Ok(l) => l as u32, - Err(e) => { - error!("Copying encoded extrinsics into return slice failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - sgx_status_t::SGX_SUCCESS -} - -/// For now get the crypto/fiat currency exchange rate from coingecko and CoinMarketCap. -#[no_mangle] -pub unsafe extern "C" fn update_market_data_xt( - crypto_currency_ptr: *const u8, - crypto_currency_size: u32, - fiat_currency_ptr: *const u8, - fiat_currency_size: u32, - unchecked_extrinsic: *mut u8, - unchecked_extrinsic_max_size: u32, - unchecked_extrinsic_size: *mut u32, -) -> sgx_status_t { - let mut crypto_currency_slice = - slice::from_raw_parts(crypto_currency_ptr, crypto_currency_size as usize); - #[allow(clippy::unwrap_used)] - let crypto_currency: String = Decode::decode(&mut crypto_currency_slice).unwrap(); - - let mut fiat_currency_slice = - slice::from_raw_parts(fiat_currency_ptr, fiat_currency_size as usize); - #[allow(clippy::unwrap_used)] - let fiat_currency: String = Decode::decode(&mut fiat_currency_slice).unwrap(); - - let extrinsics = match update_market_data_internal(crypto_currency, fiat_currency) { - Ok(xts) => xts, - Err(e) => { - error!("Update market data failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - if extrinsics.is_empty() { - error!("Updating market data yielded no extrinsics"); - return sgx_status_t::SGX_ERROR_UNEXPECTED - } - let extrinsic_slice = - slice::from_raw_parts_mut(unchecked_extrinsic, unchecked_extrinsic_max_size as usize); - - // Save created extrinsic as slice in the return value unchecked_extrinsic. - *unchecked_extrinsic_size = - match write_slice_and_whitespace_pad(extrinsic_slice, extrinsics.encode()) { - Ok(l) => l as u32, - Err(e) => { - error!("Copying encoded extrinsics into return slice failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - sgx_status_t::SGX_SUCCESS -} - -fn update_market_data_internal( - crypto_currency: String, - fiat_currency: String, -) -> Result> { - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - - let mut extrinsic_calls: Vec = Vec::new(); - - // Get the exchange rate - let trading_pair = TradingPair { crypto_currency, fiat_currency }; - - let coin_gecko_oracle = create_coin_gecko_oracle(ocall_api.clone()); - - match get_exchange_rate(trading_pair.clone(), coin_gecko_oracle) { - Ok(opaque_call) => extrinsic_calls.push(opaque_call), - Err(e) => { - error!("[-] Failed to get the newest exchange rate from CoinGecko. {:?}", e); - }, - }; - - let coin_market_cap_oracle = create_coin_market_cap_oracle(ocall_api); - match get_exchange_rate(trading_pair, coin_market_cap_oracle) { - Ok(oc) => extrinsic_calls.push(oc), - Err(e) => { - error!("[-] Failed to get the newest exchange rate from CoinMarketCap. {:?}", e); - }, - }; - - let extrinsics = extrinsics_factory.create_extrinsics(extrinsic_calls.as_slice(), None)?; - Ok(extrinsics) -} - -fn get_exchange_rate( - trading_pair: TradingPair, - oracle: ExchangeRateOracle, -) -> Result -where - OracleSourceType: OracleSource, - MetricsExporter: ExportMetrics, -{ - let (rate, base_url) = oracle - .get_exchange_rate(trading_pair.clone()) - .map_err(|e| Error::Other(e.into()))?; - - let source_base_url = base_url.as_str(); - - println!( - "Update the exchange rate: {} = {:?} for source {}", - trading_pair.clone().key(), - rate, - source_base_url, - ); - - let node_metadata_repository = - get_node_metadata_repository_from_integritee_solo_or_parachain()?; - - let call_ids = node_metadata_repository - .get_from_metadata(|m| m.update_exchange_rate_call_indexes()) - .map_err(Error::NodeMetadataProvider)? - .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - let call = OpaqueCall::from_tuple(&( - call_ids, - source_base_url.as_bytes().to_vec(), - trading_pair.key().as_bytes().to_vec(), - Some(rate), - )); - - Ok(call) -} diff --git a/tee-worker/enclave-runtime/src/test/mod.rs b/tee-worker/enclave-runtime/src/test/mod.rs index 6f3d7a252e..b3a25415a3 100644 --- a/tee-worker/enclave-runtime/src/test/mod.rs +++ b/tee-worker/enclave-runtime/src/test/mod.rs @@ -29,6 +29,3 @@ pub mod sidechain_event_tests; mod state_getter_tests; pub mod tests_main; pub mod top_pool_tests; - -#[cfg(feature = "teeracle")] -pub mod teeracle_tests; diff --git a/tee-worker/enclave-runtime/src/test/teeracle_tests.rs b/tee-worker/enclave-runtime/src/test/teeracle_tests.rs deleted file mode 100644 index bd9a4c8391..0000000000 --- a/tee-worker/enclave-runtime/src/test/teeracle_tests.rs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use codec::alloc::string::ToString; -use ita_oracle::{ - create_coin_gecko_oracle, create_coin_market_cap_oracle, - oracles::exchange_rate_oracle::GetExchangeRate, types::TradingPair, -}; -use itp_test::mock::metrics_ocall_mock::MetricsOCallMock; -use std::sync::Arc; - -pub(super) fn test_verify_get_exchange_rate_from_coin_gecko_works() { - // Get the exchange rate - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - - let coin_gecko_oracle = create_coin_gecko_oracle(Arc::new(MetricsOCallMock::default())); - - let result = coin_gecko_oracle.get_exchange_rate(trading_pair.clone()); - assert!(result.is_ok()); -} - -/// Get exchange rate from coin market cap. Requires API key (therefore not suited for unit testing). -#[allow(unused)] -pub(super) fn test_verify_get_exchange_rate_from_coin_market_cap_works() { - // Get the exchange rate - let trading_pair = - TradingPair { crypto_currency: "DOT".to_string(), fiat_currency: "USD".to_string() }; - - let coin_market_cap_oracle = - create_coin_market_cap_oracle(Arc::new(MetricsOCallMock::default())); - - let result = coin_market_cap_oracle.get_exchange_rate(trading_pair.clone()); - assert!(result.is_ok()); -} diff --git a/tee-worker/enclave-runtime/src/test/tests_main.rs b/tee-worker/enclave-runtime/src/test/tests_main.rs index a4e0b0e94b..1cedf32c1f 100644 --- a/tee-worker/enclave-runtime/src/test/tests_main.rs +++ b/tee-worker/enclave-runtime/src/test/tests_main.rs @@ -172,23 +172,9 @@ pub extern "C" fn test_main_entrance() -> size_t { // ipfs::test_verification_ok_for_correct_content, // ipfs::test_verification_fails_for_incorrect_content, // test_ocall_read_write_ipfs, - - // Teeracle tests - run_teeracle_tests, ) } -#[cfg(feature = "teeracle")] -fn run_teeracle_tests() { - use super::teeracle_tests::*; - test_verify_get_exchange_rate_from_coin_gecko_works(); - // Disabled - requires API key, cannot run locally - //test_verify_get_exchange_rate_from_coin_market_cap_works(); -} - -#[cfg(not(feature = "teeracle"))] -fn run_teeracle_tests() {} - #[cfg(feature = "evm")] fn run_evm_tests() { evm_pallet_tests::test_evm_call(); diff --git a/tee-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs b/tee-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs index 33f72e9095..e5fbed0a09 100644 --- a/tee-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs +++ b/tee-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs @@ -55,8 +55,7 @@ enum ProvisioningPayload { impl From for ProvisioningPayload { fn from(m: WorkerMode) -> Self { match m { - WorkerMode::OffChainWorker | WorkerMode::Teeracle => - ProvisioningPayload::ShieldingKeyAndLightClient, + WorkerMode::OffChainWorker => ProvisioningPayload::ShieldingKeyAndLightClient, WorkerMode::Sidechain => ProvisioningPayload::Everything, } } diff --git a/tee-worker/service/Cargo.toml b/tee-worker/service/Cargo.toml index c391b81179..62d93b85a5 100644 --- a/tee-worker/service/Cargo.toml +++ b/tee-worker/service/Cargo.toml @@ -91,7 +91,6 @@ production = [ "litentry-macros/production", "litentry-primitives/production", ] -teeracle = ["itp-settings/teeracle"] dcap = [] attesteer = ["dcap"] # Must be enabled to build a binary and link it with the enclave successfully. diff --git a/tee-worker/service/src/cli.yml b/tee-worker/service/src/cli.yml index db53ab0174..fa267c5598 100644 --- a/tee-worker/service/src/cli.yml +++ b/tee-worker/service/src/cli.yml @@ -155,17 +155,6 @@ subcommands: long: request-state short: r help: Run the worker and request key and state provisioning from another worker. - - teeracle-interval: - required: false - long: teeracle-interval - short: i - help: Set the teeracle exchange rate update interval. Example of accepted syntax <5 seconds 15 minutes 2 hours 1 days> or short <5s15m2h1d> - takes_value: true - - reregister-teeracle-interval: - required: false - long: reregister - help: Set the teeracle reregistration interval. Example of accepted syntax <5 seconds 15 minutes 2 hours 1 days> or short <5s15m2h1d> - takes_value: true - request-state: about: join a shard by requesting key provisioning from another worker args: diff --git a/tee-worker/service/src/config.rs b/tee-worker/service/src/config.rs index 5c7b569d21..6232af4a79 100644 --- a/tee-worker/service/src/config.rs +++ b/tee-worker/service/src/config.rs @@ -17,7 +17,6 @@ use clap::ArgMatches; use itc_rest_client::rest_client::Url; -use itp_settings::teeracle::{DEFAULT_MARKET_DATA_UPDATE_INTERVAL, ONE_DAY, THIRTY_MINUTES}; use parse_duration::parse; use serde::{Deserialize, Serialize}; use std::{ @@ -312,10 +311,6 @@ pub struct RunConfig { request_state: bool, /// Shard identifier base58 encoded. Defines the shard that this worker operates on. Default is mrenclave. shard: Option, - /// Optional teeracle update interval - teeracle_update_interval: Option, - /// Optional teeracle reregistration interval - reregister_teeracle_interval: Option, /// Marblerun's Prometheus endpoint base URL marblerun_base_url: Option, } @@ -337,19 +332,6 @@ impl RunConfig { self.shard.as_deref() } - pub fn teeracle_update_interval(&self) -> Duration { - self.teeracle_update_interval.unwrap_or(DEFAULT_MARKET_DATA_UPDATE_INTERVAL) - } - - /// The periodic registration period of the teeracle. - /// - /// Defaults to 23h30m, as this is slightly below the currently configured automatic - /// deregistration period on the Integritee chains. - pub fn reregister_teeracle_interval(&self) -> Duration { - // Todo: Derive this from chain https://github.com/integritee-network/worker/issues/1351 - self.reregister_teeracle_interval.unwrap_or(ONE_DAY - THIRTY_MINUTES) - } - pub fn marblerun_base_url(&self) -> &str { // This conflicts with the default port of a substrate node, but it is indeed the // default port of marblerun too: @@ -364,12 +346,6 @@ impl From<&ArgMatches<'_>> for RunConfig { let dev = m.is_present("dev"); let request_state = m.is_present("request-state"); let shard = m.value_of("shard").map(|s| s.to_string()); - let teeracle_update_interval = m.value_of("teeracle-interval").map(|i| { - parse(i).unwrap_or_else(|e| panic!("teeracle-interval parsing error {:?}", e)) - }); - let reregister_teeracle_interval = m.value_of("reregister-teeracle-interval").map(|i| { - parse(i).unwrap_or_else(|e| panic!("teeracle-interval parsing error {:?}", e)) - }); let marblerun_base_url = m.value_of("marblerun-url").map(|i| { Url::parse(i) @@ -377,15 +353,7 @@ impl From<&ArgMatches<'_>> for RunConfig { .to_string() }); - Self { - skip_ra, - dev, - request_state, - shard, - teeracle_update_interval, - reregister_teeracle_interval, - marblerun_base_url, - } + Self { skip_ra, dev, request_state, shard, marblerun_base_url } } } @@ -525,7 +493,6 @@ mod test { assert_eq!(run_config.dev, false); assert_eq!(run_config.skip_ra, false); assert!(run_config.shard.is_none()); - assert!(run_config.teeracle_update_interval.is_none()); } #[test] @@ -538,11 +505,9 @@ mod test { ("dev", Default::default()), ("skip-ra", Default::default()), ("shard", Default::default()), - ("teeracle-interval", Default::default()), ]); // Workaround because MatchedArg is private. args.args.get_mut("shard").unwrap().vals = vec![shard_identifier.into()]; - args.args.get_mut("teeracle-interval").unwrap().vals = vec!["42s".into()]; let run_config = RunConfig::from(&args); @@ -550,7 +515,6 @@ mod test { assert_eq!(run_config.dev, true); assert_eq!(run_config.skip_ra, true); assert_eq!(run_config.shard.unwrap(), shard_identifier.to_string()); - assert_eq!(run_config.teeracle_update_interval.unwrap(), Duration::from_secs(42)); } #[test] @@ -584,17 +548,6 @@ mod test { assert_eq!(config.mu_ra_url_external(), format!("{}:{}", expected_worker_ip, mu_ra_port)); } - #[test] - fn teeracle_interval_parsing_panics_if_format_is_invalid() { - let teeracle_interval = "24s_invalid-format"; - let mut args = ArgMatches::default(); - args.args = HashMap::from([("teeracle-interval", Default::default())]); - args.args.get_mut("teeracle-interval").unwrap().vals = vec![teeracle_interval.into()]; - - let result = std::panic::catch_unwind(|| RunConfig::from(&args)); - assert!(result.is_err()); - } - #[test] fn external_addresses_are_returned_correctly_if_set() { let trusted_ext_addr = "wss://1.1.1.2:700"; diff --git a/tee-worker/service/src/main.rs b/tee-worker/service/src/main.rs index 6c5a888eee..feab3398ea 100644 --- a/tee-worker/service/src/main.rs +++ b/tee-worker/service/src/main.rs @@ -31,8 +31,6 @@ mod setup; mod sidechain_setup; mod sync_block_broadcaster; mod sync_state; -#[cfg(feature = "teeracle")] -mod teeracle; mod tests; mod utils; mod worker; diff --git a/tee-worker/service/src/main_impl.rs b/tee-worker/service/src/main_impl.rs index 6c10924238..33d05871ac 100644 --- a/tee-worker/service/src/main_impl.rs +++ b/tee-worker/service/src/main_impl.rs @@ -1,6 +1,3 @@ -#[cfg(feature = "teeracle")] -use crate::teeracle::{schedule_periodic_reregistration_thread, start_periodic_market_update}; - #[cfg(not(feature = "dcap"))] use crate::utils::check_files; use crate::{ @@ -36,7 +33,6 @@ use itp_enclave_api::{ enclave_base::EnclaveBase, remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, sidechain::Sidechain, - teeracle_api::TeeracleApi, }; use itp_node_api::{ api_client::{AccountApi, PalletTeebagApi, ParentchainApi}, @@ -336,13 +332,7 @@ fn start_worker( quote_size: Option, ) where T: GetTokioHandle, - E: EnclaveBase - + DirectRequest - + Sidechain - + RemoteAttestation - + TlsRemoteAttestation - + TeeracleApi - + Clone, + E: EnclaveBase + DirectRequest + Sidechain + RemoteAttestation + TlsRemoteAttestation + Clone, D: BlockPruner + FetchBlocks + Sync + Send + 'static, InitializationHandler: TrackInitialization + IsInitialized + Sync + Send + 'static, WorkerModeProvider: ProvideWorkerMode, @@ -350,13 +340,11 @@ fn start_worker( let run_config = config.run_config().clone().expect("Run config missing"); let skip_ra = run_config.skip_ra(); - #[cfg(feature = "teeracle")] - let flavor_str = "teeracle"; #[cfg(feature = "sidechain")] let flavor_str = "sidechain"; #[cfg(feature = "offchain-worker")] let flavor_str = "offchain-worker"; - #[cfg(not(any(feature = "offchain-worker", feature = "sidechain", feature = "teeracle")))] + #[cfg(not(any(feature = "offchain-worker", feature = "sidechain")))] let flavor_str = "offchain-worker"; println!("Litentry Worker for {} v{}", flavor_str, VERSION); @@ -571,23 +559,6 @@ fn start_worker( initialization_handler.registered_on_parentchain(); match WorkerModeProvider::worker_mode() { - WorkerMode::Teeracle => { - // ------------------------------------------------------------------------ - // initialize teeracle interval - #[cfg(feature = "teeracle")] - schedule_periodic_reregistration_thread( - send_register_xt, - run_config.reregister_teeracle_interval(), - ); - - #[cfg(feature = "teeracle")] - start_periodic_market_update( - &litentry_rpc_api, - run_config.teeracle_update_interval(), - enclave.as_ref(), - &tokio_handle, - ); - }, WorkerMode::OffChainWorker => { println!("*** [+] Finished initializing light client, syncing parentchain..."); @@ -717,18 +688,17 @@ fn init_target_parentchain( let (parentchain_handler, last_synched_header) = init_parentchain(enclave, &node_api, tee_account_id, parentchain_id); - if WorkerModeProvider::worker_mode() != WorkerMode::Teeracle { - println!( - "*** [+] [{:?}] Finished initializing light client, syncing parentchain...", - parentchain_id - ); + println!( + "*** [+] [{:?}] Finished initializing light client, syncing parentchain...", + parentchain_id + ); - // Syncing all parentchain blocks, this might take a while.. - let last_synched_header = - parentchain_handler.sync_parentchain(last_synched_header, 0, true).unwrap(); + // Syncing all parentchain blocks, this might take a while.. + let last_synched_header = + parentchain_handler.sync_parentchain(last_synched_header, 0, true).unwrap(); + + start_parentchain_header_subscription_thread(parentchain_handler, last_synched_header); - start_parentchain_header_subscription_thread(parentchain_handler, last_synched_header) - } println!("[{:?}] initializing proxied shard vault account now", parentchain_id); enclave.init_proxied_shard_vault(shard, &parentchain_id).unwrap(); diff --git a/tee-worker/service/src/prometheus_metrics.rs b/tee-worker/service/src/prometheus_metrics.rs index 18930aaa2c..b1a4b6880b 100644 --- a/tee-worker/service/src/prometheus_metrics.rs +++ b/tee-worker/service/src/prometheus_metrics.rs @@ -17,9 +17,6 @@ //! Service for prometheus metrics, hosted on a http server. -#[cfg(feature = "teeracle")] -use crate::teeracle::teeracle_metrics::update_teeracle_metrics; - use crate::{ account_funding::EnclaveAccountInfo, error::{Error, ServiceResult}, @@ -229,12 +226,6 @@ impl ReceiveEnclaveMetrics for EnclaveMetricsReceiver { ENCLAVE_SIDECHAIN_SLOT_BLOCK_COMPOSITION_TIME.observe(time.as_secs_f64()), EnclaveMetric::SidechainBlockBroadcastingTime(time) => ENCLAVE_SIDECHAIN_BLOCK_BROADCASTING_TIME.observe(time.as_secs_f64()), - #[cfg(feature = "teeracle")] - EnclaveMetric::ExchangeRateOracle(m) => update_teeracle_metrics(m)?, - #[cfg(not(feature = "teeracle"))] - EnclaveMetric::ExchangeRateOracle(_) => { - error!("Received Teeracle metric, but Teeracle feature is not enabled, ignoring metric item.") - }, } Ok(()) } diff --git a/tee-worker/service/src/teeracle/mod.rs b/tee-worker/service/src/teeracle/mod.rs deleted file mode 100644 index 415f7c461d..0000000000 --- a/tee-worker/service/src/teeracle/mod.rs +++ /dev/null @@ -1,142 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::ServiceResult, teeracle::schedule_periodic::schedule_periodic}; -use codec::{Decode, Encode}; -use itp_enclave_api::teeracle_api::TeeracleApi; -use itp_node_api::api_client::ParentchainApi; -use itp_types::parentchain::Hash; -use litentry_hex_utils::hex_encode; -use log::*; -use sp_runtime::OpaqueExtrinsic; -use std::time::Duration; -use substrate_api_client::{SubmitAndWatch, XtStatus}; -use teeracle_metrics::{increment_number_of_request_failures, set_extrinsics_inclusion_success}; -use tokio::runtime::Handle; - -pub(crate) mod schedule_periodic; -pub(crate) mod teeracle_metrics; - -/// Schedule periodic reregistration of the enclave. -/// -/// The `send_register_xt` needs to create a fresh registration extrinsic every time it is called -/// (updated nonce, fresh IAS-RA or DCAP-Quote). -/// -/// Currently, this is only used for the teeracle, but could also be used for other flavors in the -/// future. -pub(crate) fn schedule_periodic_reregistration_thread( - send_register_xt: impl Fn() -> Option + std::marker::Send + 'static, - period: Duration, -) { - println!("Schedule periodic enclave reregistration every: {:?}", period); - - std::thread::Builder::new() - .name("enclave_reregistration_thread".to_owned()) - .spawn(move || { - schedule_periodic( - || { - trace!("Reregistering the enclave."); - if let Some(block_hash) = send_register_xt() { - println!( - "✅ Successfully reregistered the enclave. Block hash: {}.", - block_hash - ) - } else { - error!("❌ Could not reregister the enclave.") - } - }, - period, - ); - }) - .unwrap(); -} - -/// Executes a periodic teeracle data update and sends the new data to the parentchain. -/// -/// Note: Puts the current thread to sleep for `period`. -pub(crate) fn start_periodic_market_update( - api: &ParentchainApi, - period: Duration, - enclave_api: &E, - tokio_handle: &Handle, -) { - let updates_to_run = || { - if let Err(e) = execute_oracle_update(api, tokio_handle, || { - // Get market data for usd (hardcoded) - enclave_api.update_market_data_xt("TEER", "USD") - }) { - error!("Error running market update {:?}", e) - } - - // TODO: Refactor and add this back according to ISSUE: https://github.com/integritee-network/worker/issues/1300 - // if let Err(e) = execute_oracle_update(api, tokio_handle, || { - // enclave_api.update_weather_data_xt("54.32", "15.37") - // }) { - // error!("Error running weather update {:?}", e) - // } - }; - info!("Teeracle will update now"); - updates_to_run(); - - info!("Schedule teeracle updates every {:?}", period); - schedule_periodic(updates_to_run, period); -} - -fn execute_oracle_update( - node_api: &ParentchainApi, - tokio_handle: &Handle, - get_oracle_xt: F, -) -> ServiceResult<()> -where - F: Fn() -> Result, itp_enclave_api::error::Error>, -{ - let oracle_xt = get_oracle_xt().map_err(|e| { - increment_number_of_request_failures(); - e - })?; - - let extrinsics = >::decode(&mut oracle_xt.as_slice())?; - - // Send the extrinsics to the parentchain and wait for InBlock confirmation. - for call in extrinsics.into_iter() { - let node_api_clone = node_api.clone(); - tokio_handle.spawn(async move { - let encoded_extrinsic = call.encode(); - debug!("Hex encoded extrinsic to be sent: {}", hex_encode(&encoded_extrinsic)); - - println!("[>] Update oracle data (send the extrinsic)"); - let extrinsic_hash = match node_api_clone.submit_and_watch_opaque_extrinsic_until( - &encoded_extrinsic.into(), - XtStatus::InBlock, - ) { - Err(e) => { - error!("Failed to send extrinsic: {:?}", e); - set_extrinsics_inclusion_success(false); - return - }, - Ok(report) => { - set_extrinsics_inclusion_success(true); - report.extrinsic_hash - }, - }; - - println!("[<] Extrinsic got included into a block. Hash: {:?}\n", extrinsic_hash); - }); - } - - Ok(()) -} diff --git a/tee-worker/service/src/teeracle/schedule_periodic.rs b/tee-worker/service/src/teeracle/schedule_periodic.rs deleted file mode 100644 index cde09af452..0000000000 --- a/tee-worker/service/src/teeracle/schedule_periodic.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use std::{ - thread, - time::{Duration, Instant}, -}; - -/// Schedules a periodic task in the current thread. -/// -/// In case the task takes longer than is scheduled by the interval duration, -/// the interval timing will drift. The task is responsible for -/// ensuring it does not use up more time than is scheduled. -pub(super) fn schedule_periodic(task: T, period: Duration) -where - T: Fn(), -{ - let mut interval_start = Instant::now(); - loop { - let elapsed = interval_start.elapsed(); - - if elapsed >= period { - // update interval time - interval_start = Instant::now(); - task(); - } else { - // sleep for the rest of the interval - let sleep_time = period - elapsed; - thread::sleep(sleep_time); - } - } -} diff --git a/tee-worker/service/src/teeracle/teeracle_metrics.rs b/tee-worker/service/src/teeracle/teeracle_metrics.rs deleted file mode 100644 index 8fe62c2092..0000000000 --- a/tee-worker/service/src/teeracle/teeracle_metrics.rs +++ /dev/null @@ -1,76 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, ServiceResult}; -use itp_enclave_metrics::ExchangeRateOracleMetric; -use lazy_static::lazy_static; -use prometheus::{ - register_gauge_vec, register_int_counter, register_int_counter_vec, register_int_gauge, - register_int_gauge_vec, GaugeVec, IntCounter, IntCounterVec, IntGauge, IntGaugeVec, -}; - -lazy_static! { - /// Register Teeracle specific metrics - - static ref EXCHANGE_RATE: GaugeVec = - register_gauge_vec!("integritee_teeracle_exchange_rate", "Exchange rates partitioned into source and trading pair", &["source", "trading_pair"]) - .unwrap(); - static ref RESPONSE_TIME: IntGaugeVec = - register_int_gauge_vec!("integritee_teeracle_response_times", "Response times in ms for requests that the oracle makes", &["source"]) - .unwrap(); - static ref NUMBER_OF_REQUESTS: IntCounterVec = - register_int_counter_vec!("integritee_teeracle_number_of_requests", "Number of requests made per source", &["source"]) - .unwrap(); - - static ref NUMBER_OF_REQUEST_FAILURES: IntCounter = - register_int_counter!("integritee_teeracle_request_failures", "Number of requests that failed") - .unwrap(); - - static ref EXTRINSIC_INCLUSION_SUCCESS: IntGauge = - register_int_gauge!("integritee_teeracle_extrinsic_inclusion_success", "1 if extrinsics was successfully finalized, 0 if not") - .unwrap(); -} - -pub(super) fn increment_number_of_request_failures() { - NUMBER_OF_REQUEST_FAILURES.inc(); -} - -pub(super) fn set_extrinsics_inclusion_success(is_successful: bool) { - let success_values = i64::from(is_successful); - EXTRINSIC_INCLUSION_SUCCESS.set(success_values); -} - -pub fn update_teeracle_metrics(metric: ExchangeRateOracleMetric) -> ServiceResult<()> { - match metric { - ExchangeRateOracleMetric::ExchangeRate(source, trading_pair, exchange_rate) => - EXCHANGE_RATE - .get_metric_with_label_values(&[source.as_str(), trading_pair.as_str()]) - .map(|m| m.set(exchange_rate.to_num())) - .map_err(|e| Error::Custom(e.into()))?, - - ExchangeRateOracleMetric::ResponseTime(source, t) => RESPONSE_TIME - .get_metric_with_label_values(&[source.as_str()]) - .map(|m| m.set(t as i64)) - .map_err(|e| Error::Custom(e.into()))?, - - ExchangeRateOracleMetric::NumberRequestsIncrement(source) => NUMBER_OF_REQUESTS - .get_metric_with_label_values(&[source.as_str()]) - .map(|m| m.inc()) - .map_err(|e| Error::Custom(e.into()))?, - }; - Ok(()) -} From f8f16d2b8dba53e9e637545466dc4aa6dbabb40b Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Wed, 14 Feb 2024 22:00:41 +0530 Subject: [PATCH 35/64] refactor: remove redundant assertion handling code (#2490) * refactor: create common function for credential * refactor: remove redundant code --- .../receiver/src/handler/assertion.rs | 387 +++++++++--------- .../lc-vc-task-receiver/src/lib.rs | 12 +- .../lc-vc-task-receiver/src/vc_handling.rs | 248 ----------- 3 files changed, 197 insertions(+), 450 deletions(-) delete mode 100644 tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index 3c44d2bf4e..334428bf51 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -65,200 +65,7 @@ where fn on_process(&self) -> Result { // create the initial credential // TODO: maybe we can further simplify this - let mut credential = match self.req.assertion.clone() { - Assertion::A1 => { - #[cfg(test)] - { - std::thread::sleep(core::time::Duration::from_secs(5)); - } - lc_assertion_build::a1::build(&self.req) - }, - Assertion::A2(guild_id) => lc_assertion_build::a2::build( - &self.req, - guild_id, - &self.context.data_provider_config, - ), - - Assertion::A3(guild_id, channel_id, role_id) => lc_assertion_build::a3::build( - &self.req, - guild_id, - channel_id, - role_id, - &self.context.data_provider_config, - ), - - Assertion::A4(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::LIT, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A6 => - lc_assertion_build::a6::build(&self.req, &self.context.data_provider_config), - - Assertion::A7(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::DOT, - min_balance, - &self.context.data_provider_config, - ), - - // no need to pass `networks` again because it's the same as the `get_supported_web3networks` - Assertion::A8(_networks) => - lc_assertion_build::a8::build(&self.req, &self.context.data_provider_config), - - Assertion::A10(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::WBTC, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A11(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::ETH, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A13(owner) => - lc_assertion_build::a13::build(&self.req, self.context.ocall_api.clone(), &owner), - - Assertion::A14 => - lc_assertion_build::a14::build(&self.req, &self.context.data_provider_config), - - Assertion::Achainable(param) => lc_assertion_build::achainable::build( - &self.req, - param, - &self.context.data_provider_config, - ), - - Assertion::A20 => - lc_assertion_build::a20::build(&self.req, &self.context.data_provider_config), - - Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( - &self.req, - course_type, - &self.context.data_provider_config, - ), - - Assertion::GenericDiscordRole(role_type) => - lc_assertion_build::generic_discord_role::build( - &self.req, - role_type, - &self.context.data_provider_config, - ), - - Assertion::BnbDomainHolding => - lc_assertion_build::nodereal::bnb_domain::bnb_domain_holding_amount::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::BnbDigitDomainClub(digit_domain_type) => - lc_assertion_build::nodereal::bnb_domain::bnb_digit_domain_club_amount::build( - &self.req, - digit_domain_type, - &self.context.data_provider_config, - ), - - Assertion::VIP3MembershipCard(level) => lc_assertion_build::vip3::card::build( - &self.req, - level, - &self.context.data_provider_config, - ), - - Assertion::WeirdoGhostGangHolder => - lc_assertion_build::nodereal::nft_holder::weirdo_ghost_gang_holder::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::LITStaking => lc_assertion_build::lit_staking::build(&self.req), - - Assertion::EVMAmountHolding(token_type) => - lc_assertion_build::nodereal::amount_holding::evm_amount_holding::build( - &self.req, - token_type, - &self.context.data_provider_config, - ), - - Assertion::BRC20AmountHolder => lc_assertion_build::brc20::amount_holder::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::CryptoSummary => lc_assertion_build::nodereal::crypto_summary::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::TokenHoldingAmount(token_type) => - lc_assertion_build_v2::token_holding_amount::build( - &self.req, - token_type, - &self.context.data_provider_config, - ), - - Assertion::PlatformUser(platform_user_type) => - lc_assertion_build_v2::platform_user::build( - &self.req, - platform_user_type, - &self.context.data_provider_config, - ), - }?; - - // post-process the credential - let signer = self.context.enclave_signer.as_ref(); - let enclave_account = signer.get_enclave_account().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - credential.parachain_block_number = self.req.parachain_block_number; - credential.sidechain_block_number = self.req.sidechain_block_number; - - credential.credential_subject.endpoint = - self.context.data_provider_config.credential_endpoint.to_string(); - - credential.credential_subject.assertion_text = format!("{:?}", self.req.assertion); - - credential.issuer.id = - Identity::Substrate(enclave_account.into()).to_did().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - let json_string = credential.to_json().map_err(|_| { - VCMPError::RequestVCFailed(self.req.assertion.clone(), ErrorDetail::ParseError) - })?; - let payload = json_string.as_bytes(); - let (enclave_account, sig) = signer.sign(payload).map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - debug!("Credential Payload signature: {:?}", sig); - - credential.add_proof(&sig, &enclave_account); - credential.validate().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - let credential_str = credential.to_json().map_err(|_| { - VCMPError::RequestVCFailed(self.req.assertion.clone(), ErrorDetail::ParseError) - })?; - debug!("Credential: {}, length: {}", credential_str, credential_str.len()); - Ok(credential_str.as_bytes().to_vec()) + create_credential_str(&self.req, &self.context) } fn on_success( @@ -318,3 +125,195 @@ fn build_holding_time( ) -> Result { lc_assertion_build::holding_time::build(req, htype, min_balance, data_provider_config) } + +pub fn create_credential_str< + ShieldingKeyRepository, + A: AuthorApi, + S: StfEnclaveSigning, + H: HandleState, + O: EnclaveOnChainOCallApi, +>( + req: &AssertionBuildRequest, + context: &Arc>, +) -> Result, VCMPError> +where + ShieldingKeyRepository: AccessKey, + ::KeyType: ShieldingCryptoEncrypt + 'static, +{ + let mut credential = match req.assertion.clone() { + Assertion::A1 => { + #[cfg(test)] + { + std::thread::sleep(core::time::Duration::from_secs(5)); + } + lc_assertion_build::a1::build(req) + }, + Assertion::A2(guild_id) => + lc_assertion_build::a2::build(req, guild_id, &context.data_provider_config), + + Assertion::A3(guild_id, channel_id, role_id) => lc_assertion_build::a3::build( + req, + guild_id, + channel_id, + role_id, + &context.data_provider_config, + ), + + Assertion::A4(min_balance) => build_holding_time( + req, + AmountHoldingTimeType::LIT, + min_balance, + &context.data_provider_config, + ), + + Assertion::A6 => lc_assertion_build::a6::build(req, &context.data_provider_config), + + Assertion::A7(min_balance) => build_holding_time( + req, + AmountHoldingTimeType::DOT, + min_balance, + &context.data_provider_config, + ), + + // no need to pass `networks` again because it's the same as the `get_supported_web3networks` + Assertion::A8(_networks) => + lc_assertion_build::a8::build(req, &context.data_provider_config), + + Assertion::A10(min_balance) => build_holding_time( + req, + AmountHoldingTimeType::WBTC, + min_balance, + &context.data_provider_config, + ), + + Assertion::A11(min_balance) => build_holding_time( + req, + AmountHoldingTimeType::ETH, + min_balance, + &context.data_provider_config, + ), + + Assertion::A13(owner) => + lc_assertion_build::a13::build(req, context.ocall_api.clone(), &owner), + + Assertion::A14 => lc_assertion_build::a14::build(req, &context.data_provider_config), + + Assertion::Achainable(param) => + lc_assertion_build::achainable::build(req, param, &context.data_provider_config), + + Assertion::A20 => lc_assertion_build::a20::build(req, &context.data_provider_config), + + Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( + req, + course_type, + &context.data_provider_config, + ), + + Assertion::GenericDiscordRole(role_type) => + lc_assertion_build::generic_discord_role::build( + req, + role_type, + &context.data_provider_config, + ), + + Assertion::BnbDomainHolding => + lc_assertion_build::nodereal::bnb_domain::bnb_domain_holding_amount::build( + req, + &context.data_provider_config, + ), + + Assertion::BnbDigitDomainClub(digit_domain_type) => + lc_assertion_build::nodereal::bnb_domain::bnb_digit_domain_club_amount::build( + req, + digit_domain_type, + &context.data_provider_config, + ), + + Assertion::VIP3MembershipCard(level) => + lc_assertion_build::vip3::card::build(req, level, &context.data_provider_config), + + Assertion::WeirdoGhostGangHolder => + lc_assertion_build::nodereal::nft_holder::weirdo_ghost_gang_holder::build( + req, + &context.data_provider_config, + ), + + Assertion::LITStaking => lc_assertion_build::lit_staking::build(req), + + Assertion::EVMAmountHolding(token_type) => + lc_assertion_build::nodereal::amount_holding::evm_amount_holding::build( + req, + token_type, + &context.data_provider_config, + ), + + Assertion::BRC20AmountHolder => + lc_assertion_build::brc20::amount_holder::build(req, &context.data_provider_config), + + Assertion::CryptoSummary => + lc_assertion_build::nodereal::crypto_summary::build(req, &context.data_provider_config), + + Assertion::TokenHoldingAmount(token_type) => + lc_assertion_build_v2::token_holding_amount::build( + req, + token_type, + &context.data_provider_config, + ), + + Assertion::PlatformUser(platform_user_type) => lc_assertion_build_v2::platform_user::build( + req, + platform_user_type, + &context.data_provider_config, + ), + }?; + + // post-process the credential + let signer = context.enclave_signer.as_ref(); + let enclave_account = signer.get_enclave_account().map_err(|e| { + VCMPError::RequestVCFailed( + req.assertion.clone(), + ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), + ) + })?; + + credential.parachain_block_number = req.parachain_block_number; + credential.sidechain_block_number = req.sidechain_block_number; + + credential.credential_subject.endpoint = + context.data_provider_config.credential_endpoint.to_string(); + + credential.credential_subject.assertion_text = format!("{:?}", req.assertion); + + credential.issuer.id = Identity::Substrate(enclave_account.into()).to_did().map_err(|e| { + VCMPError::RequestVCFailed( + req.assertion.clone(), + ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), + ) + })?; + + let json_string = credential + .to_json() + .map_err(|_| VCMPError::RequestVCFailed(req.assertion.clone(), ErrorDetail::ParseError))?; + let payload = json_string.as_bytes(); + let (enclave_account, sig) = signer.sign(payload).map_err(|e| { + VCMPError::RequestVCFailed( + req.assertion.clone(), + ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), + ) + })?; + debug!("Credential Payload signature: {:?}", sig); + + credential.add_proof(&sig, &enclave_account); + credential.validate().map_err(|e| { + VCMPError::RequestVCFailed( + req.assertion.clone(), + ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), + ) + })?; + + let credential_str = credential + .to_json() + .map_err(|_| VCMPError::RequestVCFailed(req.assertion.clone(), ErrorDetail::ParseError))?; + debug!("Credential: {}, length: {}", credential_str, credential_str.len()); + Ok(credential_str.as_bytes().to_vec()) +} diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs index 0c48bda55f..395ca2f702 100644 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs +++ b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs @@ -16,7 +16,6 @@ compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the sam #[cfg(all(not(feature = "std"), feature = "sgx"))] pub use crate::sgx_reexport_prelude::*; -use crate::vc_handling::VCRequestHandler; use codec::{Decode, Encode}; use frame_support::{ensure, sp_runtime::traits::One}; use ita_sgx_runtime::{pallet_imt::get_eligible_identities, BlockNumber, Hash, Runtime}; @@ -40,9 +39,9 @@ use itp_top_pool_author::traits::AuthorApi; use itp_types::{ parentchain::ParentchainId, AccountId, BlockNumber as SidechainBlockNumber, ShardIdentifier, }; -use lc_stf_task_receiver::StfTaskContext; +use lc_stf_task_receiver::{handler::assertion::create_credential_str, StfTaskContext}; use lc_stf_task_sender::AssertionBuildRequest; -use lc_vc_task_sender::init_vc_task_sender_storage; +use lc_vc_task_sender::{init_vc_task_sender_storage, VCResponse}; use litentry_macros::if_production_or; use litentry_primitives::{ AesRequest, Assertion, DecryptableRequest, Identity, ParentchainBlockNumber, @@ -63,8 +62,6 @@ use std::{ }; use threadpool::ThreadPool; -mod vc_handling; - pub fn run_vc_handler_runner( context: Arc>, extrinsic_factory: Arc, @@ -278,10 +275,9 @@ where req_ext_hash, }; - let vc_request_handler = VCRequestHandler { req, context: context.clone() }; - let res = vc_request_handler - .process() + let credential_str = create_credential_str(&req, &context) .map_err(|e| format!("Failed to build assertion due to: {:?}", e))?; + let res = VCResponse { vc_payload: credential_str }; let call_index = node_metadata_repo .get_from_metadata(|m| m.vc_issued_call_indexes()) diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs deleted file mode 100644 index cf6485dd39..0000000000 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/vc_handling.rs +++ /dev/null @@ -1,248 +0,0 @@ -#![allow(clippy::result_large_err)] - -use crate::{Getter, TrustedCallSigned}; -use ita_sgx_runtime::Hash; -pub use ita_stf::aes_encrypt_default; -use itp_ocall_api::EnclaveOnChainOCallApi; -use itp_sgx_crypto::{key_repository::AccessKey, ShieldingCryptoDecrypt, ShieldingCryptoEncrypt}; -use itp_sgx_externalities::SgxExternalitiesTrait; -use itp_stf_executor::traits::StfEnclaveSigning; -use itp_stf_state_handler::handle_state::HandleState; -use itp_top_pool_author::traits::AuthorApi; -use lc_data_providers::DataProviderConfig; -use lc_stf_task_receiver::StfTaskContext; -use lc_stf_task_sender::AssertionBuildRequest; -use lc_vc_task_sender::VCResponse; -use litentry_primitives::{ - AmountHoldingTimeType, Assertion, ErrorDetail, ErrorString, Identity, ParameterString, - VCMPError, -}; -use std::{format, string::ToString, sync::Arc}; - -pub(crate) struct VCRequestHandler< - ShieldingKeyRepository, - A: AuthorApi, - S: StfEnclaveSigning, - H: HandleState, - O: EnclaveOnChainOCallApi, -> where - ShieldingKeyRepository: AccessKey, - ::KeyType: - ShieldingCryptoDecrypt + ShieldingCryptoEncrypt + 'static, -{ - pub(crate) req: AssertionBuildRequest, - pub(crate) context: Arc>, -} - -impl VCRequestHandler -where - ShieldingKeyRepository: AccessKey, - ::KeyType: - ShieldingCryptoDecrypt + ShieldingCryptoEncrypt + 'static, - A: AuthorApi, - S: StfEnclaveSigning, - H: HandleState, - H::StateT: SgxExternalitiesTrait, - O: EnclaveOnChainOCallApi, -{ - pub fn process(self) -> Result { - let mut credential = match self.req.assertion.clone() { - Assertion::A1 => lc_assertion_build::a1::build(&self.req), - - Assertion::A2(guild_id) => lc_assertion_build::a2::build( - &self.req, - guild_id, - &self.context.data_provider_config, - ), - - Assertion::A3(guild_id, channel_id, role_id) => lc_assertion_build::a3::build( - &self.req, - guild_id, - channel_id, - role_id, - &self.context.data_provider_config, - ), - - Assertion::A4(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::LIT, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A6 => - lc_assertion_build::a6::build(&self.req, &self.context.data_provider_config), - - Assertion::A7(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::DOT, - min_balance, - &self.context.data_provider_config, - ), - - // no need to pass `networks` again because it's the same as the `get_supported_web3networks` - Assertion::A8(_networks) => - lc_assertion_build::a8::build(&self.req, &self.context.data_provider_config), - - Assertion::A10(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::WBTC, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A11(min_balance) => build_holding_time( - &self.req, - AmountHoldingTimeType::ETH, - min_balance, - &self.context.data_provider_config, - ), - - Assertion::A13(owner) => - lc_assertion_build::a13::build(&self.req, self.context.ocall_api.clone(), &owner), - - Assertion::A14 => - lc_assertion_build::a14::build(&self.req, &self.context.data_provider_config), - - Assertion::Achainable(param) => lc_assertion_build::achainable::build( - &self.req, - param, - &self.context.data_provider_config, - ), - - Assertion::A20 => - lc_assertion_build::a20::build(&self.req, &self.context.data_provider_config), - - Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( - &self.req, - course_type, - &self.context.data_provider_config, - ), - - Assertion::GenericDiscordRole(role_type) => - lc_assertion_build::generic_discord_role::build( - &self.req, - role_type, - &self.context.data_provider_config, - ), - - Assertion::BnbDomainHolding => - lc_assertion_build::nodereal::bnb_domain::bnb_domain_holding_amount::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::BnbDigitDomainClub(digit_domain_type) => - lc_assertion_build::nodereal::bnb_domain::bnb_digit_domain_club_amount::build( - &self.req, - digit_domain_type, - &self.context.data_provider_config, - ), - - Assertion::VIP3MembershipCard(level) => lc_assertion_build::vip3::card::build( - &self.req, - level, - &self.context.data_provider_config, - ), - - Assertion::WeirdoGhostGangHolder => - lc_assertion_build::nodereal::nft_holder::weirdo_ghost_gang_holder::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::LITStaking => lc_assertion_build::lit_staking::build(&self.req), - - Assertion::EVMAmountHolding(token_type) => - lc_assertion_build::nodereal::amount_holding::evm_amount_holding::build( - &self.req, - token_type, - &self.context.data_provider_config, - ), - - Assertion::BRC20AmountHolder => lc_assertion_build::brc20::amount_holder::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::CryptoSummary => lc_assertion_build::nodereal::crypto_summary::build( - &self.req, - &self.context.data_provider_config, - ), - - Assertion::TokenHoldingAmount(token_type) => - lc_assertion_build_v2::token_holding_amount::build( - &self.req, - token_type, - &self.context.data_provider_config, - ), - - Assertion::PlatformUser(platform_user_type) => - lc_assertion_build_v2::platform_user::build( - &self.req, - platform_user_type, - &self.context.data_provider_config, - ), - }?; - - // post-process the credential - let signer = self.context.enclave_signer.as_ref(); - let enclave_account = signer.get_enclave_account().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - credential.parachain_block_number = self.req.parachain_block_number; - credential.sidechain_block_number = self.req.sidechain_block_number; - - credential.credential_subject.endpoint = - self.context.data_provider_config.credential_endpoint.to_string(); - - credential.credential_subject.assertion_text = format!("{:?}", self.req.assertion); - - credential.issuer.id = - Identity::Substrate(enclave_account.into()).to_did().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - let json_string = credential.to_json().map_err(|_| { - VCMPError::RequestVCFailed(self.req.assertion.clone(), ErrorDetail::ParseError) - })?; - let payload = json_string.as_bytes(); - let (enclave_account, sig) = signer.sign(payload).map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - credential.add_proof(&sig, &enclave_account); - credential.validate().map_err(|e| { - VCMPError::RequestVCFailed( - self.req.assertion.clone(), - ErrorDetail::StfError(ErrorString::truncate_from(format!("{e:?}").into())), - ) - })?; - - let credential_str = credential.to_json().map_err(|_| { - VCMPError::RequestVCFailed(self.req.assertion.clone(), ErrorDetail::ParseError) - })?; - - let vc_response = VCResponse { vc_payload: credential_str.as_bytes().to_vec() }; - - Ok(vc_response) - } -} - -fn build_holding_time( - req: &AssertionBuildRequest, - htype: AmountHoldingTimeType, - min_balance: ParameterString, - data_provider_config: &DataProviderConfig, -) -> Result { - lc_assertion_build::holding_time::build(req, htype, min_balance, data_provider_config) -} From d4426c8bfadfa984a0439d1c97caebdfdc5c1648 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Thu, 15 Feb 2024 12:50:15 +0100 Subject: [PATCH 36/64] Remove teerex/teeracle/sidechain pallet integration (#2491) * remove teeracle * remove teeracle * remove teeracle feature * remove extra edl * fix clippy * init * remove teerex * fix imp and vcmp * fix toml * fix compile error --- Cargo.lock | 6 +- bitacross-worker/Cargo.lock | 14 +-- bitacross-worker/Cargo.toml | 20 --- bitacross-worker/cli/Cargo.toml | 1 - .../cli/src/base_cli/commands/mod.rs | 1 - .../base_cli/commands/register_tcb_info.rs | 4 +- .../cli/src/base_cli/commands/shield_funds.rs | 92 -------------- bitacross-worker/cli/src/base_cli/mod.rs | 7 +- bitacross-worker/cli/src/trusted_operation.rs | 25 ++-- .../enclave-bridge-storage/Cargo.toml | 20 --- .../enclave-bridge-storage/src/lib.rs | 31 ----- .../node-api/api-client-extensions/src/lib.rs | 2 - .../src/pallet_teerex.rs | 105 ---------------- .../src/pallet_teerex_api_mock.rs | 70 ----------- .../node-api/metadata/src/pallet_teerex.rs | 114 ------------------ .../core-primitives/teerex-storage/Cargo.toml | 18 --- .../core-primitives/teerex-storage/src/lib.rs | 35 ------ bitacross-worker/enclave-runtime/Cargo.lock | 30 ----- bitacross-worker/enclave-runtime/Cargo.toml | 1 - pallets/identity-management/Cargo.toml | 8 +- pallets/identity-management/src/lib.rs | 8 +- pallets/identity-management/src/mock.rs | 44 +++---- pallets/identity-management/src/tests.rs | 5 +- pallets/vc-management/Cargo.toml | 5 +- pallets/vc-management/src/lib.rs | 2 +- pallets/vc-management/src/mock.rs | 42 +++---- runtime/common/src/lib.rs | 20 +-- tee-worker/Cargo.lock | 14 +-- tee-worker/Cargo.toml | 1 - tee-worker/cli/Cargo.toml | 1 - tee-worker/cli/src/base_cli/commands/mod.rs | 1 - .../base_cli/commands/register_tcb_info.rs | 4 +- .../cli/src/base_cli/commands/shield_funds.rs | 92 -------------- tee-worker/cli/src/base_cli/mod.rs | 5 - tee-worker/cli/src/trusted_operation.rs | 25 ++-- .../enclave-bridge-storage/Cargo.toml | 20 --- .../enclave-bridge-storage/src/lib.rs | 31 ----- .../node-api/api-client-extensions/src/lib.rs | 4 - .../src/pallet_teerex.rs | 105 ---------------- .../src/pallet_teerex_api_mock.rs | 70 ----------- .../node-api/metadata/src/pallet_teerex.rs | 114 ------------------ .../core-primitives/teerex-storage/Cargo.toml | 18 --- .../core-primitives/teerex-storage/src/lib.rs | 35 ------ tee-worker/enclave-runtime/Cargo.lock | 30 ----- tee-worker/enclave-runtime/Cargo.toml | 1 - 45 files changed, 91 insertions(+), 1210 deletions(-) delete mode 100644 bitacross-worker/cli/src/base_cli/commands/shield_funds.rs delete mode 100644 bitacross-worker/core-primitives/enclave-bridge-storage/Cargo.toml delete mode 100644 bitacross-worker/core-primitives/enclave-bridge-storage/src/lib.rs delete mode 100644 bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs delete mode 100644 bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs delete mode 100644 bitacross-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs delete mode 100644 bitacross-worker/core-primitives/teerex-storage/Cargo.toml delete mode 100644 bitacross-worker/core-primitives/teerex-storage/src/lib.rs delete mode 100644 tee-worker/cli/src/base_cli/commands/shield_funds.rs delete mode 100644 tee-worker/core-primitives/enclave-bridge-storage/Cargo.toml delete mode 100644 tee-worker/core-primitives/enclave-bridge-storage/src/lib.rs delete mode 100644 tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs delete mode 100644 tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs delete mode 100644 tee-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs delete mode 100644 tee-worker/core-primitives/teerex-storage/Cargo.toml delete mode 100644 tee-worker/core-primitives/teerex-storage/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index 46b2331249..9da2d4f2de 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7449,7 +7449,7 @@ dependencies = [ "frame-system", "pallet-balances", "pallet-group", - "pallet-teerex", + "pallet-teebag", "pallet-timestamp", "parity-scale-codec", "scale-info", @@ -7457,7 +7457,6 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "teerex-primitives", "test-utils", ] @@ -8136,7 +8135,7 @@ dependencies = [ "frame-system", "pallet-balances", "pallet-group", - "pallet-teerex", + "pallet-teebag", "pallet-timestamp", "parity-scale-codec", "scale-info", @@ -8144,7 +8143,6 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "teerex-primitives", "test-utils", ] diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 7ce5e2fbdd..6fb011ab84 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -761,7 +761,6 @@ dependencies = [ "log 0.4.20", "pallet-balances", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "pallet-teerex", "parity-scale-codec", "rand 0.8.5", "rayon", @@ -5507,14 +5506,6 @@ dependencies = [ "thiserror 1.0.9", ] -[[package]] -name = "itp-teerex-storage" -version = "0.9.0" -dependencies = [ - "itp-storage", - "sp-std 5.0.0", -] - [[package]] name = "itp-test" version = "0.9.0" @@ -8886,13 +8877,12 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "pallet-teerex", + "pallet-teebag", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] @@ -9248,12 +9238,12 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "pallet-teebag", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index e722ab669c..1b734ef250 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -50,7 +50,6 @@ members = [ "core-primitives/substrate-sgx/environmental", "core-primitives/substrate-sgx/externalities", "core-primitives/substrate-sgx/sp-io", - "core-primitives/teerex-storage", "core-primitives/test", "core-primitives/time-utils", "core-primitives/top-pool", @@ -91,22 +90,3 @@ sgx_urts = { version = "1.1.6", git = "https://github.com/apache/incubator-teacl [patch.crates-io] ring = { git = "https://github.com/betrusted-io/ring-xous", branch = "0.16.20-cleanup" } - -#[patch."https://github.com/integritee-network/integritee-node"] -#my-node-runtime = { package = "integritee-node-runtime", git = "https://github.com/integritee-network//integritee-node", branch = "ab/integrate-pallet-teerex-refactoring" } - -#[patch."https://github.com/scs/substrate-api-client"] -#substrate-api-client = { path = "../../scs/substrate-api-client" } -#substrate-client-keystore = { path = "../../scs/substrate-api-client/client-keystore" } - -#[patch."https://github.com/integritee-network/pallets.git"] -#pallet-claims = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-enclave-bridge = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-teerex = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#pallet-sidechain = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#sgx-verify = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#test-utils = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#claims-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#enclave-bridge-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#teerex-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } -#common-primitives = { git = "https://github.com/integritee-network//pallets", branch = "ab/shard-config-upgradability-2" } diff --git a/bitacross-worker/cli/Cargo.toml b/bitacross-worker/cli/Cargo.toml index 75f82aae5b..d29ddd70de 100644 --- a/bitacross-worker/cli/Cargo.toml +++ b/bitacross-worker/cli/Cargo.toml @@ -55,7 +55,6 @@ ita-sgx-runtime = { path = "../app-libs/sgx-runtime" } lc-direct-call = { path = "../litentry/core/direct-call" } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } -pallet-teerex = { path = "../../pallets/teerex", default-features = false } scale-value = "0.6.0" sp-core-hashing = "6.0.0" diff --git a/bitacross-worker/cli/src/base_cli/commands/mod.rs b/bitacross-worker/cli/src/base_cli/commands/mod.rs index 313a32249c..033b15b253 100644 --- a/bitacross-worker/cli/src/base_cli/commands/mod.rs +++ b/bitacross-worker/cli/src/base_cli/commands/mod.rs @@ -3,5 +3,4 @@ pub mod faucet; pub mod listen; pub mod litentry; pub mod register_tcb_info; -pub mod shield_funds; pub mod transfer; diff --git a/bitacross-worker/cli/src/base_cli/commands/register_tcb_info.rs b/bitacross-worker/cli/src/base_cli/commands/register_tcb_info.rs index 7802794a09..4ab9b55a45 100644 --- a/bitacross-worker/cli/src/base_cli/commands/register_tcb_info.rs +++ b/bitacross-worker/cli/src/base_cli/commands/register_tcb_info.rs @@ -19,7 +19,7 @@ use crate::{ command_utils::{get_chain_api, *}, Cli, CliResult, CliResultOk, }; -use itp_node_api::api_client::TEEREX; +use itp_node_api::api_client::TEEBAG; use itp_types::{parentchain::Hash, OpaqueCall}; use itp_utils::ToHexPrefixed; use log::*; @@ -106,7 +106,7 @@ impl RegisterTcbInfoCommand { let call = OpaqueCall::from_tuple(&compose_call!( chain_api.metadata(), - TEEREX, + TEEBAG, "register_tcb_info", tcb_info, intel_signature, diff --git a/bitacross-worker/cli/src/base_cli/commands/shield_funds.rs b/bitacross-worker/cli/src/base_cli/commands/shield_funds.rs deleted file mode 100644 index ec45da50fb..0000000000 --- a/bitacross-worker/cli/src/base_cli/commands/shield_funds.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - command_utils::{get_accountid_from_str, get_chain_api, *}, - Cli, CliError, CliResult, CliResultOk, -}; -use base58::FromBase58; -use codec::{Decode, Encode}; -use itp_node_api::api_client::TEEREX; -use itp_sgx_crypto::ShieldingCryptoEncrypt; -use itp_stf_primitives::types::ShardIdentifier; -use litentry_primitives::ParentchainBalance as Balance; -use log::*; -use sp_core::sr25519 as sr25519_core; -use substrate_api_client::{ac_compose_macros::compose_extrinsic, SubmitAndWatch, XtStatus}; - -#[derive(Parser)] -pub struct ShieldFundsCommand { - /// Sender's parentchain AccountId in ss58check format. - from: String, - /// Recipient's incognito AccountId in ss58check format. - to: String, - /// Amount to be transferred. - amount: Balance, - /// Shard identifier. - shard: String, -} - -impl ShieldFundsCommand { - pub(crate) fn run(&self, cli: &Cli) -> CliResult { - let mut chain_api = get_chain_api(cli); - - let shard_opt = match self.shard.from_base58() { - Ok(s) => ShardIdentifier::decode(&mut &s[..]), - _ => panic!("shard argument must be base58 encoded"), - }; - - let shard = match shard_opt { - Ok(shard) => shard, - Err(e) => panic!("{}", e), - }; - - // Get the sender. - let from = get_pair_from_str(&self.from); - chain_api.set_signer(sr25519_core::Pair::from(from).into()); - - // Get the recipient. - let to = get_accountid_from_str(&self.to); - - let encryption_key = get_shielding_key(cli).unwrap(); - let encrypted_recevier = encryption_key.encrypt(&to.encode()).unwrap(); - - // Compose the extrinsic. - let xt = compose_extrinsic!( - chain_api, - TEEREX, - "shield_funds", - encrypted_recevier, - self.amount, - shard - ); - - match chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized) { - Ok(xt_report) => { - println!( - "[+] shield funds success. extrinsic hash: {:?} / status: {:?} / block hash: {:?}", - xt_report.extrinsic_hash, xt_report.status, xt_report.block_hash.unwrap() - ); - Ok(CliResultOk::H256 { hash: xt_report.block_hash.unwrap() }) - }, - Err(e) => { - error!("shield_funds extrinsic failed {:?}", e); - Err(CliError::Extrinsic { msg: format!("{:?}", e) }) - }, - } - } -} diff --git a/bitacross-worker/cli/src/base_cli/mod.rs b/bitacross-worker/cli/src/base_cli/mod.rs index dd82a8ee33..d74d538f2f 100644 --- a/bitacross-worker/cli/src/base_cli/mod.rs +++ b/bitacross-worker/cli/src/base_cli/mod.rs @@ -18,8 +18,7 @@ use crate::{ base_cli::commands::{ balance::BalanceCommand, faucet::FaucetCommand, listen::ListenCommand, - register_tcb_info::RegisterTcbInfoCommand, shield_funds::ShieldFundsCommand, - transfer::TransferCommand, + register_tcb_info::RegisterTcbInfoCommand, transfer::TransferCommand, }, command_utils::*, Cli, CliResult, CliResultOk, ED25519_KEY_TYPE, SR25519_KEY_TYPE, @@ -72,9 +71,6 @@ pub enum BaseCommand { /// Register TCB info for FMSPC RegisterTcbInfo(RegisterTcbInfoCommand), - /// Transfer funds from an parentchain account to an incognito account - ShieldFunds(ShieldFundsCommand), - // Litentry's commands below /// query sgx-runtime metadata and print the raw (hex-encoded) metadata to stdout /// we could have added a parameter like `--raw` to `PrintSgxMetadata`, but @@ -95,7 +91,6 @@ impl BaseCommand { BaseCommand::ListWorkers => list_workers(cli), BaseCommand::Listen(cmd) => cmd.run(cli), BaseCommand::RegisterTcbInfo(cmd) => cmd.run(cli), - BaseCommand::ShieldFunds(cmd) => cmd.run(cli), // Litentry's commands below BaseCommand::PrintSgxMetadataRaw => print_sgx_metadata_raw(cli), } diff --git a/bitacross-worker/cli/src/trusted_operation.rs b/bitacross-worker/cli/src/trusted_operation.rs index bfbb18c221..f3564f0e23 100644 --- a/bitacross-worker/cli/src/trusted_operation.rs +++ b/bitacross-worker/cli/src/trusted_operation.rs @@ -25,7 +25,7 @@ use base58::{FromBase58, ToBase58}; use codec::{Decode, Encode, Input}; use ita_stf::{Getter, StfError, TrustedCallSigned}; use itc_rpc_client::direct_client::{DirectApi, DirectClient}; -use itp_node_api::api_client::{ParentchainApi, TEEREX}; +use itp_node_api::api_client::{ParentchainApi, TEEBAG}; use itp_rpc::{Id, RpcRequest, RpcResponse, RpcReturnValue}; use itp_sgx_crypto::ShieldingCryptoEncrypt; use itp_stf_primitives::types::{ShardIdentifier, TrustedOperation}; @@ -33,8 +33,7 @@ use itp_types::{BlockNumber, DirectRequestStatus, RsaRequest, TrustedOperationSt use itp_utils::{FromHexPrefixed, ToHexPrefixed}; use litentry_primitives::ParentchainHash as Hash; use log::*; -use my_node_runtime::RuntimeEvent; -use pallet_teerex::Event as TeerexEvent; +use my_node_runtime::{pallet_teebag::Event as TeebagEvent, RuntimeEvent}; use sp_core::H256; use std::{ fmt::Debug, @@ -150,7 +149,7 @@ fn send_indirect_request( chain_api.set_signer(signer.into()); let request = RsaRequest::new(shard, call_encrypted); - let xt = compose_extrinsic!(&chain_api, TEEREX, "call_worker", request); + let xt = compose_extrinsic!(&chain_api, TEEBAG, "post_opaque_task", request); let block_hash = match chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::InBlock) { Ok(xt_report) => { @@ -176,14 +175,14 @@ fn send_indirect_request( let event_result = subscription.next_events::(); if let Some(Ok(event_records)) = event_result { for event_record in event_records { - if let RuntimeEvent::Teerex(TeerexEvent::ProcessedParentchainBlock( - _signer, - confirmed_block_hash, - trusted_calls_merkle_root, - confirmed_block_number, - )) = event_record.event + if let RuntimeEvent::Teebag(TeebagEvent::ParentchainBlockProcessed { + who: _signer, + block_number: confirmed_block_number, + block_hash: confirmed_block_hash, + task_merkle_root: trusted_calls_merkle_root, + }) = event_record.event { - info!("Confirmation of ProcessedParentchainBlock received"); + info!("Confirmation of ParentchainBlockProcessed received"); debug!("shard: {:?}", shard); debug!("confirmed parentchain block Hash: {:?}", block_hash); debug!("trusted calls merkle root: {:?}", trusted_calls_merkle_root); @@ -194,9 +193,9 @@ fn send_indirect_request( confirmed_block_hash, confirmed_block_number, ) { - error!("ProcessedParentchainBlock event: {:?}", e); + error!("ParentchainBlockProcessed event: {:?}", e); return Err(TrustedOperationError::Default { - msg: format!("ProcessedParentchainBlock event: {:?}", e), + msg: format!("ParentchainBlockProcessed event: {:?}", e), }) }; diff --git a/bitacross-worker/core-primitives/enclave-bridge-storage/Cargo.toml b/bitacross-worker/core-primitives/enclave-bridge-storage/Cargo.toml deleted file mode 100644 index 8b191f3458..0000000000 --- a/bitacross-worker/core-primitives/enclave-bridge-storage/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "itp-enclave-bridge-storage" -version = "0.9.0" -authors = ["Integritee AG "] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -#local deps -itp-storage = { path = "../storage", default-features = false } - -[features] -default = ["std"] -std = [ - "codec/std", - "sp-std/std", - "itp-storage/std", -] diff --git a/bitacross-worker/core-primitives/enclave-bridge-storage/src/lib.rs b/bitacross-worker/core-primitives/enclave-bridge-storage/src/lib.rs deleted file mode 100644 index 9077d756b6..0000000000 --- a/bitacross-worker/core-primitives/enclave-bridge-storage/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -use codec::Encode; -use itp_storage::{storage_map_key, StorageHasher}; -use sp_std::prelude::Vec; - -pub struct EnclaveBridgeStorage; - -// Separate the prefix from the rest because in our case we changed the storage prefix due to -// the rebranding. With the below implementation of the `TeerexStorageKeys`, we could simply -// define another struct `OtherStorage`, implement `StoragePrefix` for it, and get the -// `TeerexStorageKeys` implementation for free. -pub trait StoragePrefix { - fn prefix() -> &'static str; -} - -impl StoragePrefix for EnclaveBridgeStorage { - fn prefix() -> &'static str { - "EnclaveBridge" - } -} - -pub trait EnclaveBridgeStorageKeys { - fn shard_status(shard: T) -> Vec; -} - -impl EnclaveBridgeStorageKeys for S { - fn shard_status(shard: T) -> Vec { - storage_map_key(Self::prefix(), "ShardStatus", &shard, &StorageHasher::Blake2_128Concat) - } -} diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index 878d384af8..668cefd2ba 100644 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -22,11 +22,9 @@ pub use substrate_api_client::{api::Error as ApiClientError, rpc::TungsteniteRpc pub mod account; pub mod chain; pub mod pallet_teebag; -pub mod pallet_teerex; pub use account::*; pub use chain::*; pub use pallet_teebag::*; -pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs deleted file mode 100644 index 222e249402..0000000000 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::ApiResult; -use itp_api_client_types::{storage_key, traits::GetStorage, Api, Config, Request}; -use itp_types::{Enclave, IpfsHash, MrEnclave, ShardIdentifier}; -use sp_core::storage::StorageKey; - -pub const TEEREX: &str = "Teerex"; -pub const SIDECHAIN: &str = "Sidechain"; - -/// ApiClient extension that enables communication with the `teerex` pallet. -// Todo: make generic over `Config` type instead? -pub trait PalletTeerexApi { - type Hash; - - fn enclave(&self, index: u64, at_block: Option) -> ApiResult>; - fn enclave_count(&self, at_block: Option) -> ApiResult; - fn all_enclaves(&self, at_block: Option) -> ApiResult>; - fn worker_for_shard( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult>; - fn latest_ipfs_hash( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult>; - - // litentry - fn all_scheduled_mrenclaves(&self, at_block: Option) -> ApiResult>; -} - -impl PalletTeerexApi for Api -where - RuntimeConfig: Config, - Client: Request, -{ - type Hash = RuntimeConfig::Hash; - - fn enclave(&self, index: u64, at_block: Option) -> ApiResult> { - self.get_storage_map(TEEREX, "EnclaveRegistry", index, at_block) - } - - fn enclave_count(&self, at_block: Option) -> ApiResult { - Ok(self.get_storage(TEEREX, "EnclaveCount", at_block)?.unwrap_or(0u64)) - } - - fn all_enclaves(&self, at_block: Option) -> ApiResult> { - let count = self.enclave_count(at_block)?; - let mut enclaves = Vec::with_capacity(count as usize); - for n in 1..=count { - enclaves.push(self.enclave(n, at_block)?.expect("None enclave")) - } - Ok(enclaves) - } - - fn worker_for_shard( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult> { - self.get_storage_map(SIDECHAIN, "WorkerForShard", shard, at_block)? - .map_or_else(|| Ok(None), |w_index| self.enclave(w_index, at_block)) - } - - fn latest_ipfs_hash( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult> { - self.get_storage_map(TEEREX, "LatestIPFSHash", shard, at_block) - } - - fn all_scheduled_mrenclaves(&self, at_block: Option) -> ApiResult> { - let keys: Vec<_> = self - .get_keys(storage_key(TEEREX, "ScheduledEnclave"), at_block)? - .unwrap_or_default() - .iter() - .map(|key| { - let key = key.strip_prefix("0x").unwrap_or(key); - let raw_key = hex::decode(key).unwrap(); - self.get_storage_by_key::(StorageKey(raw_key).into(), at_block) - }) - .filter(|enclave| matches!(enclave, Ok(Some(_)))) - .map(|enclave| enclave.unwrap().unwrap()) - .collect(); - Ok(keys) - } -} diff --git a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs b/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs deleted file mode 100644 index df5bf3646f..0000000000 --- a/bitacross-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{pallet_teerex::PalletTeerexApi, ApiResult}; -use itp_types::{parentchain::Hash, AccountId, IpfsHash, MrEnclave, MultiEnclave, ShardIdentifier}; -use std::collections::HashMap; - -#[derive(Default)] -pub struct PalletTeerexApiMock { - registered_enclaves: HashMap>>, -} - -impl PalletTeerexApiMock { - pub fn with_enclaves(mut self, enclaves: Vec>>) -> Self { - enclaves.iter().map(|enclave| self.registered_enclaves.insert(enclave)); - self - } -} - -impl PalletTeerexApi for PalletTeerexApiMock { - fn enclave( - &self, - account: AccountId, - _at_block: Option, - ) -> ApiResult>>> { - Ok(self.registered_enclaves.get(index as usize).cloned()) - } - - fn enclave_count(&self, _at_block: Option) -> ApiResult { - Ok(self.registered_enclaves.len() as u64) - } - - fn all_enclaves(&self, _at_block: Option) -> ApiResult>>> { - Ok(self.registered_enclaves.clone()) - } - - fn primary_worker_for_shard( - &self, - _shard: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult>>> { - todo!() - } - - fn latest_ipfs_hash( - &self, - _shard: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { - todo!() - } - - fn all_scheduled_mrenclaves(&self, _at_block: Option) -> ApiResult> { - Ok(self.registered_enclaves.iter().map(|k| k.mr_enclave).collect()) - } -} diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs deleted file mode 100644 index d2cd618e80..0000000000 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate::{error::Result, NodeMetadata}; -use sp_core::storage::StorageKey; - -/// Pallet' name: -pub const TEEREX: &str = "Teerex"; - -pub trait TeerexCallIndexes { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]>; - - fn invoke_call_indexes(&self) -> Result<[u8; 2]>; - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]>; - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]>; - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]>; - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]>; - - // litentry - fn update_scheduled_enclave(&self) -> Result<[u8; 2]>; - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]>; -} - -pub trait TeerexStorageKey { - fn sovereign_enclaves_storage_map_key(&self, index: u64) -> Result; - - fn proxied_enclaves_storage_map_key(&self, index: u64) -> Result; -} - -impl TeerexCallIndexes for NodeMetadata { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_enclave") - } - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unregister_sovereign_enclave") - } - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unregister_proxied_enclave") - } - - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_quoting_enclave") - } - - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_tcb_info") - } - - /* Keep parachain extrinsic name untouched. Keep alignment with upstream worker */ - fn invoke_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "call_worker") - } - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "confirm_processed_parentchain_block") - } - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "shield_funds") - } - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unshield_funds") - } - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "publish_hash") - } - - fn update_scheduled_enclave(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "update_scheduled_enclave") - } - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "remove_scheduled_enclave") - } -} - -impl TeerexStorageKey for NodeMetadata { - fn sovereign_enclaves_storage_map_key(&self, index: u64) -> Result { - self.storage_map_key(TEEREX, "SovereignEnclaves", index) - } - fn proxied_enclaves_storage_map_key(&self, index: u64) -> Result { - self.storage_map_key(TEEREX, "ProxiedEnclaves", index) - } -} diff --git a/bitacross-worker/core-primitives/teerex-storage/Cargo.toml b/bitacross-worker/core-primitives/teerex-storage/Cargo.toml deleted file mode 100644 index ca9bafb791..0000000000 --- a/bitacross-worker/core-primitives/teerex-storage/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "itp-teerex-storage" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -#local deps -itp-storage = { path = "../storage", default-features = false } - -[features] -default = ["std"] -std = [ - "sp-std/std", - "itp-storage/std", -] diff --git a/bitacross-worker/core-primitives/teerex-storage/src/lib.rs b/bitacross-worker/core-primitives/teerex-storage/src/lib.rs deleted file mode 100644 index 706d92fcb1..0000000000 --- a/bitacross-worker/core-primitives/teerex-storage/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -use itp_storage::{storage_map_key, storage_value_key, StorageHasher}; -use sp_std::prelude::Vec; - -pub struct TeeRexStorage; - -// Separate the prefix from the rest because in our case we changed the storage prefix due to -// the rebranding. With the below implementation of the `TeerexStorageKeys`, we could simply -// define another struct `OtherStorage`, implement `StoragePrefix` for it, and get the -// `TeerexStorageKeys` implementation for free. -pub trait StoragePrefix { - fn prefix() -> &'static str; -} - -impl StoragePrefix for TeeRexStorage { - fn prefix() -> &'static str { - "Teerex" - } -} - -pub trait TeerexStorageKeys { - fn enclave_count() -> Vec; - fn enclave(index: u64) -> Vec; -} - -impl TeerexStorageKeys for S { - fn enclave_count() -> Vec { - storage_value_key(Self::prefix(), "EnclaveCount") - } - - fn enclave(index: u64) -> Vec { - storage_map_key(Self::prefix(), "EnclaveRegistry", &index, &StorageHasher::Blake2_128Concat) - } -} diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index f400009087..097731ae87 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -625,19 +625,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "common-primitives" -version = "0.1.0" -source = "git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42#eaf611b79bc9d56b20c155150e99b549bf98436b" -dependencies = [ - "derive_more", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "const-oid" version = "0.9.5" @@ -1017,7 +1004,6 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "teerex-primitives", "webpki", ] @@ -5008,22 +4994,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "teerex-primitives" -version = "0.1.0" -source = "git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42#eaf611b79bc9d56b20c155150e99b549bf98436b" -dependencies = [ - "common-primitives", - "derive_more", - "log", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "termcolor" version = "1.0.5" diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index aaabf0d4f9..e0d80a7fa1 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -89,7 +89,6 @@ base58 = { rev = "sgx_1.1.3", package = "rust-base58", default-features = false, cid = { default-features = false, git = "https://github.com/whalelephant/rust-cid", branch = "nstd" } multibase = { default-features = false, git = "https://github.com/whalelephant/rust-multibase", branch = "nstd" } -teerex-primitives = { default-features = false, git = "https://github.com/integritee-network/pallets.git", branch = "sdk-v0.12.0-polkadot-v0.9.42" } # local deps ita-parentchain-interface = { path = "../app-libs/parentchain-interface", default-features = false, features = ["sgx"] } diff --git a/pallets/identity-management/Cargo.toml b/pallets/identity-management/Cargo.toml index 1f281705d8..808ca921c5 100644 --- a/pallets/identity-management/Cargo.toml +++ b/pallets/identity-management/Cargo.toml @@ -9,7 +9,6 @@ version = '0.1.0' [dependencies] # third-party dependencies codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "max-encoded-len"] } -pallet-teerex = { path = "../teerex", default-features = false } scale-info = { version = "2.10.0", default-features = false, features = ["derive"] } # primitives @@ -27,12 +26,11 @@ test-utils = { path = "../test-utils", default-features = false, optional = true # local core-primitives = { path = "../../primitives/core", default-features = false } -teerex-primitives = { path = "../../primitives/teerex", default-features = false } +pallet-teebag = { path = "../teebag", default-features = false } [dev-dependencies] pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } pallet-group = { path = "../../pallets/group" } -pallet-teerex = { path = "../teerex" } pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } test-utils = { path = "../test-utils" } @@ -47,7 +45,6 @@ runtime-benchmarks = [ "test-utils", ] skip-ias-check = [ - "pallet-teerex/skip-ias-check", ] std = [ "codec/std", @@ -60,7 +57,6 @@ std = [ "frame-system/std", "frame-benchmarking?/std", "core-primitives/std", - "teerex-primitives/std", - "pallet-teerex/std", + "pallet-teebag/std", ] try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/identity-management/src/lib.rs b/pallets/identity-management/src/lib.rs index c876f045ee..a311a0e78a 100644 --- a/pallets/identity-management/src/lib.rs +++ b/pallets/identity-management/src/lib.rs @@ -21,9 +21,9 @@ //! However, there are so many error cases in TEE that I'm not even sure //! if it's a good idea to have a matching extrinsic for error propagation. //! -//! The reasons that we don't use pallet_teerex::call_worker directly are: -//! - call teerex::call_worker inside IMP won't trigger the handler, because it's not called as -//! extrinsics so won't be scraped +//! The reasons that we don't use pallet_teebag::post_opaque_task directly are: +//! - call pallet_teebag::post_opaque_task inside IMP won't trigger the handler, because it's not +//! called as extrinsics so won't be scraped //! - the origin is discarded in call_worker but we need it //! - to simplify the F/E usage, we only need to encrypt the needed parameters (see e.g. //! shield_funds) @@ -43,9 +43,9 @@ pub mod weights; pub use crate::weights::WeightInfo; pub use pallet::*; +use pallet_teebag::ShardIdentifier; use sp_core::H256; use sp_std::vec::Vec; -pub use teerex_primitives::ShardIdentifier; #[frame_support::pallet] pub mod pallet { diff --git a/pallets/identity-management/src/mock.rs b/pallets/identity-management/src/mock.rs index 7a4fcfdfbf..feae0dc9d1 100644 --- a/pallets/identity-management/src/mock.rs +++ b/pallets/identity-management/src/mock.rs @@ -45,7 +45,7 @@ type SystemAccountId = ::AccountId; pub struct EnsureEnclaveSigner(PhantomData); impl EnsureOrigin for EnsureEnclaveSigner where - T: frame_system::Config + pallet_teerex::Config + pallet_timestamp::Config, + T: frame_system::Config + pallet_teebag::Config + pallet_timestamp::Config, ::AccountId: From<[u8; 32]>, ::Hash: From<[u8; 32]>, { @@ -53,7 +53,7 @@ where fn try_origin(o: T::RuntimeOrigin) -> Result { o.into().and_then(|o| match o { frame_system::RawOrigin::Signed(who) - if pallet_teerex::Pallet::::ensure_registered_enclave(&who) == Ok(()) => + if pallet_teebag::EnclaveRegistry::::contains_key(&who) => Ok(who), r => Err(T::RuntimeOrigin::from(r)), }) @@ -61,17 +61,13 @@ where #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result { - use test_utils::ias::{ - consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}, - TestEnclave, - }; + use test_utils::ias::consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}; let signer: ::AccountId = test_utils::get_signer(TEST8_SIGNER_PUB); - if !pallet_teerex::EnclaveIndex::::contains_key(signer.clone()) { - assert_ok!(pallet_teerex::Pallet::::add_enclave( + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(pallet_teebag::Pallet::::add_enclave( &signer, - &teerex_primitives::Enclave::test_enclave(signer.clone()) - .with_mr_enclave(TEST8_MRENCLAVE), + &pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE), )); } Ok(frame_system::RawOrigin::Signed(signer).into()) @@ -87,8 +83,8 @@ frame_support::construct_runtime!( { System: frame_system, Balances: pallet_balances, + Teebag: pallet_teebag, Timestamp: pallet_timestamp, - Teerex: pallet_teerex, IdentityManagement: pallet_identity_management, IMPExtrinsicWhitelist: pallet_group, } @@ -150,16 +146,13 @@ impl pallet_balances::Config for Test { parameter_types! { pub const MomentsPerDay: u64 = 86_400_000; // [ms/d] - pub const MaxSilenceTime: u64 = 172_800_000; // 48h } -impl pallet_teerex::Config for Test { +impl pallet_teebag::Config for Test { type RuntimeEvent = RuntimeEvent; - type Currency = Balances; type MomentsPerDay = MomentsPerDay; - type MaxSilenceTime = MaxSilenceTime; - type WeightInfo = (); type SetAdminOrigin = EnsureRoot; + type MaxEnclaveIdentifier = ConstU32<3>; } impl pallet_identity_management::Config for Test { @@ -184,21 +177,24 @@ pub fn new_test_ext() -> sp_io::TestExternalities { let _ = IdentityManagement::add_delegatee(RuntimeOrigin::root(), eddie); System::set_block_number(1); use test_utils::ias::consts::{TEST8_CERT, TEST8_SIGNER_PUB, TEST8_TIMESTAMP, URL}; - let teerex_signer: SystemAccountId = test_utils::get_signer(TEST8_SIGNER_PUB); - assert_ok!(Teerex::set_admin(RuntimeOrigin::root(), teerex_signer.clone())); - assert_ok!(Teerex::set_skip_scheduled_enclave_check( - RuntimeOrigin::signed(teerex_signer.clone()), - true + let signer: SystemAccountId = test_utils::get_signer(TEST8_SIGNER_PUB); + assert_ok!(Teebag::set_admin(RuntimeOrigin::root(), signer.clone())); + assert_ok!(Teebag::set_mode( + RuntimeOrigin::signed(signer.clone()), + pallet_teebag::OperationalMode::Development )); Timestamp::set_timestamp(TEST8_TIMESTAMP); - if !pallet_teerex::EnclaveIndex::::contains_key(teerex_signer.clone()) { - assert_ok!(Teerex::register_enclave( - RuntimeOrigin::signed(teerex_signer), + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer), + pallet_teebag::WorkerType::Identity, + pallet_teebag::WorkerMode::Sidechain, TEST8_CERT.to_vec(), URL.to_vec(), None, None, + pallet_teebag::AttestationType::Ias, )); } }); diff --git a/pallets/identity-management/src/tests.rs b/pallets/identity-management/src/tests.rs index 4db8abf85d..142001b857 100644 --- a/pallets/identity-management/src/tests.rs +++ b/pallets/identity-management/src/tests.rs @@ -127,12 +127,15 @@ fn tee_callback_with_registered_enclave_works() { &[119, 115, 58, 47, 47, 49, 50, 55, 46, 48, 46, 48, 46, 49, 58, 57, 57, 57, 49]; new_test_ext().execute_with(|| { let alice: SystemAccountId = test_utils::get_signer(ALICE_PUBKEY); - assert_ok!(Teerex::register_enclave( + assert_ok!(Teebag::register_enclave( RuntimeOrigin::signed(alice.clone()), + Default::default(), + Default::default(), TEST8_CERT.to_vec(), URL.to_vec(), None, None, + Default::default(), )); assert_ok!(IdentityManagement::some_error( diff --git a/pallets/vc-management/Cargo.toml b/pallets/vc-management/Cargo.toml index 8566ffd22d..e349b51bd3 100644 --- a/pallets/vc-management/Cargo.toml +++ b/pallets/vc-management/Cargo.toml @@ -22,7 +22,7 @@ frame-system = { git = "https://github.com/paritytech/substrate", branch = "polk # local core-primitives = { path = "../../primitives/core", default-features = false } -teerex-primitives = { path = "../../primitives/teerex", default-features = false } +pallet-teebag = { path = "../teebag", default-features = false } # benchmarking frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false, optional = true } @@ -32,7 +32,6 @@ test-utils = { path = "../test-utils", default-features = false, optional = true frame-benchmarking = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } pallet-balances = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } pallet-group = { path = "../../pallets/group" } -pallet-teerex = { path = "../../pallets/teerex" } pallet-timestamp = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42" } test-utils = { path = "../test-utils" } @@ -57,7 +56,7 @@ std = [ "frame-system/std", "frame-benchmarking?/std", "core-primitives/std", - "teerex-primitives/std", + "pallet-teebag/std", "pallet-balances/std", "pallet-group/std", ] diff --git a/pallets/vc-management/src/lib.rs b/pallets/vc-management/src/lib.rs index d59b6f3791..14c86c447c 100644 --- a/pallets/vc-management/src/lib.rs +++ b/pallets/vc-management/src/lib.rs @@ -35,9 +35,9 @@ pub mod weights; pub use crate::weights::WeightInfo; pub use pallet::*; +use pallet_teebag::ShardIdentifier; use sp_core::H256; use sp_std::vec::Vec; -use teerex_primitives::ShardIdentifier; mod schema; pub use schema::*; diff --git a/pallets/vc-management/src/mock.rs b/pallets/vc-management/src/mock.rs index 9ac8374515..ceb086d59b 100644 --- a/pallets/vc-management/src/mock.rs +++ b/pallets/vc-management/src/mock.rs @@ -43,7 +43,7 @@ type SystemAccountId = ::AccountId; pub struct EnsureEnclaveSigner(PhantomData); impl EnsureOrigin for EnsureEnclaveSigner where - T: frame_system::Config + pallet_teerex::Config + pallet_timestamp::Config, + T: frame_system::Config + pallet_teebag::Config + pallet_timestamp::Config, ::AccountId: From<[u8; 32]>, ::Hash: From<[u8; 32]>, { @@ -51,7 +51,7 @@ where fn try_origin(o: T::RuntimeOrigin) -> Result { o.into().and_then(|o| match o { frame_system::RawOrigin::Signed(who) - if pallet_teerex::Pallet::::ensure_registered_enclave(&who) == Ok(()) => + if pallet_teebag::EnclaveRegistry::::contains_key(&who) => Ok(who), r => Err(T::RuntimeOrigin::from(r)), }) @@ -59,17 +59,13 @@ where #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result { - use test_utils::ias::{ - consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}, - TestEnclave, - }; + use test_utils::ias::consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}; let signer: ::AccountId = test_utils::get_signer(TEST8_SIGNER_PUB); - if !pallet_teerex::EnclaveIndex::::contains_key(signer.clone()) { - assert_ok!(pallet_teerex::Pallet::::add_enclave( + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(pallet_teebag::Pallet::::add_enclave( &signer, - &teerex_primitives::Enclave::test_enclave(signer.clone()) - .with_mr_enclave(TEST8_MRENCLAVE), + &pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE), )); } Ok(frame_system::RawOrigin::Signed(signer).into()) @@ -85,7 +81,7 @@ frame_support::construct_runtime!( { System: frame_system, Balances: pallet_balances, - Teerex: pallet_teerex, + Teebag: pallet_teebag, Timestamp: pallet_timestamp, VCManagement: pallet_vc_management, VCMPExtrinsicWhitelist: pallet_group, @@ -157,16 +153,13 @@ impl pallet_vc_management::Config for Test { parameter_types! { pub const MomentsPerDay: u64 = 86_400_000; // [ms/d] - pub const MaxSilenceTime: u64 = 172_800_000; // 48h } -impl pallet_teerex::Config for Test { +impl pallet_teebag::Config for Test { type RuntimeEvent = RuntimeEvent; - type Currency = Balances; type MomentsPerDay = MomentsPerDay; - type MaxSilenceTime = MaxSilenceTime; - type WeightInfo = (); type SetAdminOrigin = EnsureRoot; + type MaxEnclaveIdentifier = ConstU32<3>; } impl pallet_group::Config for Test { @@ -183,22 +176,25 @@ pub fn new_test_ext() -> sp_io::TestExternalities { System::set_block_number(1); let _ = VCManagement::set_admin(RuntimeOrigin::root(), alice.clone()); let _ = VCManagement::add_delegatee(RuntimeOrigin::root(), eddie); - assert_ok!(Teerex::set_admin(RuntimeOrigin::root(), alice.clone())); - assert_ok!(Teerex::set_skip_scheduled_enclave_check( + assert_ok!(Teebag::set_admin(RuntimeOrigin::root(), alice.clone())); + assert_ok!(Teebag::set_mode( RuntimeOrigin::signed(alice.clone()), - true + pallet_teebag::OperationalMode::Development )); use test_utils::ias::consts::{TEST8_CERT, TEST8_SIGNER_PUB, TEST8_TIMESTAMP, URL}; Timestamp::set_timestamp(TEST8_TIMESTAMP); - let teerex_signer: SystemAccountId = test_utils::get_signer(TEST8_SIGNER_PUB); - if !pallet_teerex::EnclaveIndex::::contains_key(teerex_signer.clone()) { - assert_ok!(Teerex::register_enclave( - RuntimeOrigin::signed(teerex_signer), + let signer: SystemAccountId = test_utils::get_signer(TEST8_SIGNER_PUB); + if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { + assert_ok!(Teebag::register_enclave( + RuntimeOrigin::signed(signer), + pallet_teebag::WorkerType::Identity, + pallet_teebag::WorkerMode::Sidechain, TEST8_CERT.to_vec(), URL.to_vec(), None, None, + pallet_teebag::AttestationType::Ias, )); } }); diff --git a/runtime/common/src/lib.rs b/runtime/common/src/lib.rs index 59bb72a002..c6161535ee 100644 --- a/runtime/common/src/lib.rs +++ b/runtime/common/src/lib.rs @@ -338,7 +338,7 @@ where pub struct EnsureEnclaveSigner(PhantomData); impl EnsureOrigin for EnsureEnclaveSigner where - T: frame_system::Config + pallet_teerex::Config + pallet_teebag::Config, + T: frame_system::Config + pallet_teebag::Config + pallet_teebag::Config, ::AccountId: From<[u8; 32]>, ::Hash: From<[u8; 32]>, { @@ -346,8 +346,7 @@ where fn try_origin(o: T::RuntimeOrigin) -> Result { o.into().and_then(|o| match o { frame_system::RawOrigin::Signed(who) - if pallet_teerex::Pallet::::ensure_registered_enclave(&who).is_ok() || - pallet_teebag::EnclaveRegistry::::contains_key(&who) => + if pallet_teebag::EnclaveRegistry::::contains_key(&who) => Ok(who), r => Err(T::RuntimeOrigin::from(r)), }) @@ -355,22 +354,11 @@ where #[cfg(feature = "runtime-benchmarks")] fn try_successful_origin() -> Result { - use test_utils::ias::{ - consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}, - TestEnclave, - }; + use test_utils::ias::consts::{TEST8_MRENCLAVE, TEST8_SIGNER_PUB}; let signer: ::AccountId = test_utils::get_signer(TEST8_SIGNER_PUB); - if !pallet_teerex::EnclaveIndex::::contains_key(signer.clone()) { - assert_ok!(pallet_teerex::Pallet::::add_enclave( - &signer, - &teerex_primitives::Enclave::test_enclave(signer.clone()) - .with_mr_enclave(TEST8_MRENCLAVE), - )); - } - + let enclave = pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE); if !pallet_teebag::EnclaveRegistry::::contains_key(signer.clone()) { - let enclave = pallet_teebag::Enclave::default().with_mrenclave(TEST8_MRENCLAVE); assert_ok!(pallet_teebag::Pallet::::add_enclave(&signer, &enclave)); } Ok(frame_system::RawOrigin::Signed(signer).into()) diff --git a/tee-worker/Cargo.lock b/tee-worker/Cargo.lock index e326ea1d60..1e44b39c41 100644 --- a/tee-worker/Cargo.lock +++ b/tee-worker/Cargo.lock @@ -5307,14 +5307,6 @@ dependencies = [ "thiserror 1.0.9", ] -[[package]] -name = "itp-teerex-storage" -version = "0.9.0" -dependencies = [ - "itp-storage", - "sp-std 5.0.0", -] - [[package]] name = "itp-test" version = "0.9.0" @@ -7087,7 +7079,6 @@ dependencies = [ "log 0.4.20", "pallet-balances", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "pallet-teerex", "parity-scale-codec", "rand 0.8.5", "rayon", @@ -9107,13 +9098,12 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", - "pallet-teerex", + "pallet-teebag", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] @@ -9487,12 +9477,12 @@ dependencies = [ "frame-benchmarking", "frame-support", "frame-system", + "pallet-teebag", "parity-scale-codec", "scale-info", "sp-core", "sp-runtime", "sp-std 5.0.0", - "teerex-primitives", ] [[package]] diff --git a/tee-worker/Cargo.toml b/tee-worker/Cargo.toml index 1e7b3af498..a7d31e4593 100644 --- a/tee-worker/Cargo.toml +++ b/tee-worker/Cargo.toml @@ -50,7 +50,6 @@ members = [ "core-primitives/substrate-sgx/environmental", "core-primitives/substrate-sgx/externalities", "core-primitives/substrate-sgx/sp-io", - "core-primitives/teerex-storage", "core-primitives/test", "core-primitives/time-utils", "core-primitives/top-pool", diff --git a/tee-worker/cli/Cargo.toml b/tee-worker/cli/Cargo.toml index d82a66de76..40eb67a1c9 100644 --- a/tee-worker/cli/Cargo.toml +++ b/tee-worker/cli/Cargo.toml @@ -56,7 +56,6 @@ ita-sgx-runtime = { path = "../app-libs/sgx-runtime" } litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } -pallet-teerex = { path = "../../pallets/teerex", default-features = false } scale-value = "0.6.0" sp-core-hashing = "6.0.0" diff --git a/tee-worker/cli/src/base_cli/commands/mod.rs b/tee-worker/cli/src/base_cli/commands/mod.rs index 313a32249c..033b15b253 100644 --- a/tee-worker/cli/src/base_cli/commands/mod.rs +++ b/tee-worker/cli/src/base_cli/commands/mod.rs @@ -3,5 +3,4 @@ pub mod faucet; pub mod listen; pub mod litentry; pub mod register_tcb_info; -pub mod shield_funds; pub mod transfer; diff --git a/tee-worker/cli/src/base_cli/commands/register_tcb_info.rs b/tee-worker/cli/src/base_cli/commands/register_tcb_info.rs index 7802794a09..4ab9b55a45 100644 --- a/tee-worker/cli/src/base_cli/commands/register_tcb_info.rs +++ b/tee-worker/cli/src/base_cli/commands/register_tcb_info.rs @@ -19,7 +19,7 @@ use crate::{ command_utils::{get_chain_api, *}, Cli, CliResult, CliResultOk, }; -use itp_node_api::api_client::TEEREX; +use itp_node_api::api_client::TEEBAG; use itp_types::{parentchain::Hash, OpaqueCall}; use itp_utils::ToHexPrefixed; use log::*; @@ -106,7 +106,7 @@ impl RegisterTcbInfoCommand { let call = OpaqueCall::from_tuple(&compose_call!( chain_api.metadata(), - TEEREX, + TEEBAG, "register_tcb_info", tcb_info, intel_signature, diff --git a/tee-worker/cli/src/base_cli/commands/shield_funds.rs b/tee-worker/cli/src/base_cli/commands/shield_funds.rs deleted file mode 100644 index ec45da50fb..0000000000 --- a/tee-worker/cli/src/base_cli/commands/shield_funds.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - command_utils::{get_accountid_from_str, get_chain_api, *}, - Cli, CliError, CliResult, CliResultOk, -}; -use base58::FromBase58; -use codec::{Decode, Encode}; -use itp_node_api::api_client::TEEREX; -use itp_sgx_crypto::ShieldingCryptoEncrypt; -use itp_stf_primitives::types::ShardIdentifier; -use litentry_primitives::ParentchainBalance as Balance; -use log::*; -use sp_core::sr25519 as sr25519_core; -use substrate_api_client::{ac_compose_macros::compose_extrinsic, SubmitAndWatch, XtStatus}; - -#[derive(Parser)] -pub struct ShieldFundsCommand { - /// Sender's parentchain AccountId in ss58check format. - from: String, - /// Recipient's incognito AccountId in ss58check format. - to: String, - /// Amount to be transferred. - amount: Balance, - /// Shard identifier. - shard: String, -} - -impl ShieldFundsCommand { - pub(crate) fn run(&self, cli: &Cli) -> CliResult { - let mut chain_api = get_chain_api(cli); - - let shard_opt = match self.shard.from_base58() { - Ok(s) => ShardIdentifier::decode(&mut &s[..]), - _ => panic!("shard argument must be base58 encoded"), - }; - - let shard = match shard_opt { - Ok(shard) => shard, - Err(e) => panic!("{}", e), - }; - - // Get the sender. - let from = get_pair_from_str(&self.from); - chain_api.set_signer(sr25519_core::Pair::from(from).into()); - - // Get the recipient. - let to = get_accountid_from_str(&self.to); - - let encryption_key = get_shielding_key(cli).unwrap(); - let encrypted_recevier = encryption_key.encrypt(&to.encode()).unwrap(); - - // Compose the extrinsic. - let xt = compose_extrinsic!( - chain_api, - TEEREX, - "shield_funds", - encrypted_recevier, - self.amount, - shard - ); - - match chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized) { - Ok(xt_report) => { - println!( - "[+] shield funds success. extrinsic hash: {:?} / status: {:?} / block hash: {:?}", - xt_report.extrinsic_hash, xt_report.status, xt_report.block_hash.unwrap() - ); - Ok(CliResultOk::H256 { hash: xt_report.block_hash.unwrap() }) - }, - Err(e) => { - error!("shield_funds extrinsic failed {:?}", e); - Err(CliError::Extrinsic { msg: format!("{:?}", e) }) - }, - } - } -} diff --git a/tee-worker/cli/src/base_cli/mod.rs b/tee-worker/cli/src/base_cli/mod.rs index 23de8b50f9..5deb57fe6a 100644 --- a/tee-worker/cli/src/base_cli/mod.rs +++ b/tee-worker/cli/src/base_cli/mod.rs @@ -22,7 +22,6 @@ use crate::{ listen::ListenCommand, litentry::{id_graph_hash::IDGraphHashCommand, link_identity::LinkIdentityCommand}, register_tcb_info::RegisterTcbInfoCommand, - shield_funds::ShieldFundsCommand, transfer::TransferCommand, }, command_utils::*, @@ -76,9 +75,6 @@ pub enum BaseCommand { /// Register TCB info for FMSPC RegisterTcbInfo(RegisterTcbInfoCommand), - /// Transfer funds from an parentchain account to an incognito account - ShieldFunds(ShieldFundsCommand), - // Litentry's commands below /// query sgx-runtime metadata and print the raw (hex-encoded) metadata to stdout /// we could have added a parameter like `--raw` to `PrintSgxMetadata`, but @@ -105,7 +101,6 @@ impl BaseCommand { BaseCommand::ListWorkers => list_workers(cli), BaseCommand::Listen(cmd) => cmd.run(cli), BaseCommand::RegisterTcbInfo(cmd) => cmd.run(cli), - BaseCommand::ShieldFunds(cmd) => cmd.run(cli), // Litentry's commands below BaseCommand::PrintSgxMetadataRaw => print_sgx_metadata_raw(cli), BaseCommand::LinkIdentity(cmd) => cmd.run(cli), diff --git a/tee-worker/cli/src/trusted_operation.rs b/tee-worker/cli/src/trusted_operation.rs index 0404216d85..fbe783b184 100644 --- a/tee-worker/cli/src/trusted_operation.rs +++ b/tee-worker/cli/src/trusted_operation.rs @@ -25,7 +25,7 @@ use base58::{FromBase58, ToBase58}; use codec::{Decode, Encode, Input}; use ita_stf::{Getter, StfError, TrustedCall, TrustedCallSigned}; use itc_rpc_client::direct_client::{DirectApi, DirectClient}; -use itp_node_api::api_client::{ParentchainApi, TEEREX}; +use itp_node_api::api_client::{ParentchainApi, TEEBAG}; use itp_rpc::{Id, RpcRequest, RpcResponse, RpcReturnValue}; use itp_sgx_crypto::ShieldingCryptoEncrypt; use itp_stf_primitives::types::{ShardIdentifier, TrustedOperation}; @@ -35,8 +35,7 @@ use litentry_primitives::{ aes_encrypt_default, AesRequest, ParentchainHash as Hash, RequestAesKey, }; use log::*; -use my_node_runtime::RuntimeEvent; -use pallet_teerex::Event as TeerexEvent; +use my_node_runtime::{pallet_teebag::Event as TeebagEvent, RuntimeEvent}; use sp_core::H256; use std::{ fmt::Debug, @@ -168,7 +167,7 @@ fn send_indirect_request( chain_api.set_signer(signer.into()); let request = RsaRequest::new(shard, call_encrypted); - let xt = compose_extrinsic!(&chain_api, TEEREX, "call_worker", request); + let xt = compose_extrinsic!(&chain_api, TEEBAG, "post_opaque_task", request); let block_hash = match chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::InBlock) { Ok(xt_report) => { @@ -194,14 +193,14 @@ fn send_indirect_request( let event_result = subscription.next_events::(); if let Some(Ok(event_records)) = event_result { for event_record in event_records { - if let RuntimeEvent::Teerex(TeerexEvent::ProcessedParentchainBlock( - _signer, - confirmed_block_hash, - trusted_calls_merkle_root, - confirmed_block_number, - )) = event_record.event + if let RuntimeEvent::Teebag(TeebagEvent::ParentchainBlockProcessed { + who: _signer, + block_number: confirmed_block_number, + block_hash: confirmed_block_hash, + task_merkle_root: trusted_calls_merkle_root, + }) = event_record.event { - info!("Confirmation of ProcessedParentchainBlock received"); + info!("Confirmation of ParentchainBlockProcessed received"); debug!("shard: {:?}", shard); debug!("confirmed parentchain block Hash: {:?}", block_hash); debug!("trusted calls merkle root: {:?}", trusted_calls_merkle_root); @@ -212,9 +211,9 @@ fn send_indirect_request( confirmed_block_hash, confirmed_block_number, ) { - error!("ProcessedParentchainBlock event: {:?}", e); + error!("ParentchainBlockProcessed event: {:?}", e); return Err(TrustedOperationError::Default { - msg: format!("ProcessedParentchainBlock event: {:?}", e), + msg: format!("ParentchainBlockProcessed event: {:?}", e), }) }; diff --git a/tee-worker/core-primitives/enclave-bridge-storage/Cargo.toml b/tee-worker/core-primitives/enclave-bridge-storage/Cargo.toml deleted file mode 100644 index 8b191f3458..0000000000 --- a/tee-worker/core-primitives/enclave-bridge-storage/Cargo.toml +++ /dev/null @@ -1,20 +0,0 @@ -[package] -name = "itp-enclave-bridge-storage" -version = "0.9.0" -authors = ["Integritee AG "] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -#local deps -itp-storage = { path = "../storage", default-features = false } - -[features] -default = ["std"] -std = [ - "codec/std", - "sp-std/std", - "itp-storage/std", -] diff --git a/tee-worker/core-primitives/enclave-bridge-storage/src/lib.rs b/tee-worker/core-primitives/enclave-bridge-storage/src/lib.rs deleted file mode 100644 index 9077d756b6..0000000000 --- a/tee-worker/core-primitives/enclave-bridge-storage/src/lib.rs +++ /dev/null @@ -1,31 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -use codec::Encode; -use itp_storage::{storage_map_key, StorageHasher}; -use sp_std::prelude::Vec; - -pub struct EnclaveBridgeStorage; - -// Separate the prefix from the rest because in our case we changed the storage prefix due to -// the rebranding. With the below implementation of the `TeerexStorageKeys`, we could simply -// define another struct `OtherStorage`, implement `StoragePrefix` for it, and get the -// `TeerexStorageKeys` implementation for free. -pub trait StoragePrefix { - fn prefix() -> &'static str; -} - -impl StoragePrefix for EnclaveBridgeStorage { - fn prefix() -> &'static str { - "EnclaveBridge" - } -} - -pub trait EnclaveBridgeStorageKeys { - fn shard_status(shard: T) -> Vec; -} - -impl EnclaveBridgeStorageKeys for S { - fn shard_status(shard: T) -> Vec { - storage_map_key(Self::prefix(), "ShardStatus", &shard, &StorageHasher::Blake2_128Concat) - } -} diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs index cbec7b12a8..668cefd2ba 100644 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs +++ b/tee-worker/core-primitives/node-api/api-client-extensions/src/lib.rs @@ -23,12 +23,8 @@ pub mod account; pub mod chain; pub mod pallet_teebag; -// TODO: part of P-487 - these will be removed anyway so I haven't spent time adjust them -pub mod pallet_teerex; - pub use account::*; pub use chain::*; pub use pallet_teebag::*; -pub use pallet_teerex::*; pub type ApiResult = Result; diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs deleted file mode 100644 index 222e249402..0000000000 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::ApiResult; -use itp_api_client_types::{storage_key, traits::GetStorage, Api, Config, Request}; -use itp_types::{Enclave, IpfsHash, MrEnclave, ShardIdentifier}; -use sp_core::storage::StorageKey; - -pub const TEEREX: &str = "Teerex"; -pub const SIDECHAIN: &str = "Sidechain"; - -/// ApiClient extension that enables communication with the `teerex` pallet. -// Todo: make generic over `Config` type instead? -pub trait PalletTeerexApi { - type Hash; - - fn enclave(&self, index: u64, at_block: Option) -> ApiResult>; - fn enclave_count(&self, at_block: Option) -> ApiResult; - fn all_enclaves(&self, at_block: Option) -> ApiResult>; - fn worker_for_shard( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult>; - fn latest_ipfs_hash( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult>; - - // litentry - fn all_scheduled_mrenclaves(&self, at_block: Option) -> ApiResult>; -} - -impl PalletTeerexApi for Api -where - RuntimeConfig: Config, - Client: Request, -{ - type Hash = RuntimeConfig::Hash; - - fn enclave(&self, index: u64, at_block: Option) -> ApiResult> { - self.get_storage_map(TEEREX, "EnclaveRegistry", index, at_block) - } - - fn enclave_count(&self, at_block: Option) -> ApiResult { - Ok(self.get_storage(TEEREX, "EnclaveCount", at_block)?.unwrap_or(0u64)) - } - - fn all_enclaves(&self, at_block: Option) -> ApiResult> { - let count = self.enclave_count(at_block)?; - let mut enclaves = Vec::with_capacity(count as usize); - for n in 1..=count { - enclaves.push(self.enclave(n, at_block)?.expect("None enclave")) - } - Ok(enclaves) - } - - fn worker_for_shard( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult> { - self.get_storage_map(SIDECHAIN, "WorkerForShard", shard, at_block)? - .map_or_else(|| Ok(None), |w_index| self.enclave(w_index, at_block)) - } - - fn latest_ipfs_hash( - &self, - shard: &ShardIdentifier, - at_block: Option, - ) -> ApiResult> { - self.get_storage_map(TEEREX, "LatestIPFSHash", shard, at_block) - } - - fn all_scheduled_mrenclaves(&self, at_block: Option) -> ApiResult> { - let keys: Vec<_> = self - .get_keys(storage_key(TEEREX, "ScheduledEnclave"), at_block)? - .unwrap_or_default() - .iter() - .map(|key| { - let key = key.strip_prefix("0x").unwrap_or(key); - let raw_key = hex::decode(key).unwrap(); - self.get_storage_by_key::(StorageKey(raw_key).into(), at_block) - }) - .filter(|enclave| matches!(enclave, Ok(Some(_)))) - .map(|enclave| enclave.unwrap().unwrap()) - .collect(); - Ok(keys) - } -} diff --git a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs b/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs deleted file mode 100644 index df5bf3646f..0000000000 --- a/tee-worker/core-primitives/node-api/api-client-extensions/src/pallet_teerex_api_mock.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{pallet_teerex::PalletTeerexApi, ApiResult}; -use itp_types::{parentchain::Hash, AccountId, IpfsHash, MrEnclave, MultiEnclave, ShardIdentifier}; -use std::collections::HashMap; - -#[derive(Default)] -pub struct PalletTeerexApiMock { - registered_enclaves: HashMap>>, -} - -impl PalletTeerexApiMock { - pub fn with_enclaves(mut self, enclaves: Vec>>) -> Self { - enclaves.iter().map(|enclave| self.registered_enclaves.insert(enclave)); - self - } -} - -impl PalletTeerexApi for PalletTeerexApiMock { - fn enclave( - &self, - account: AccountId, - _at_block: Option, - ) -> ApiResult>>> { - Ok(self.registered_enclaves.get(index as usize).cloned()) - } - - fn enclave_count(&self, _at_block: Option) -> ApiResult { - Ok(self.registered_enclaves.len() as u64) - } - - fn all_enclaves(&self, _at_block: Option) -> ApiResult>>> { - Ok(self.registered_enclaves.clone()) - } - - fn primary_worker_for_shard( - &self, - _shard: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult>>> { - todo!() - } - - fn latest_ipfs_hash( - &self, - _shard: &ShardIdentifier, - _at_block: Option, - ) -> ApiResult> { - todo!() - } - - fn all_scheduled_mrenclaves(&self, _at_block: Option) -> ApiResult> { - Ok(self.registered_enclaves.iter().map(|k| k.mr_enclave).collect()) - } -} diff --git a/tee-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs b/tee-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs deleted file mode 100644 index d2cd618e80..0000000000 --- a/tee-worker/core-primitives/node-api/metadata/src/pallet_teerex.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate::{error::Result, NodeMetadata}; -use sp_core::storage::StorageKey; - -/// Pallet' name: -pub const TEEREX: &str = "Teerex"; - -pub trait TeerexCallIndexes { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]>; - - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]>; - - fn invoke_call_indexes(&self) -> Result<[u8; 2]>; - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]>; - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]>; - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]>; - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]>; - - // litentry - fn update_scheduled_enclave(&self) -> Result<[u8; 2]>; - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]>; -} - -pub trait TeerexStorageKey { - fn sovereign_enclaves_storage_map_key(&self, index: u64) -> Result; - - fn proxied_enclaves_storage_map_key(&self, index: u64) -> Result; -} - -impl TeerexCallIndexes for NodeMetadata { - fn register_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_enclave") - } - - fn unregister_sovereign_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unregister_sovereign_enclave") - } - - fn unregister_proxied_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unregister_proxied_enclave") - } - - fn register_quoting_enclave_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_quoting_enclave") - } - - fn register_tcb_info_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "register_tcb_info") - } - - /* Keep parachain extrinsic name untouched. Keep alignment with upstream worker */ - fn invoke_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "call_worker") - } - - fn confirm_processed_parentchain_block_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "confirm_processed_parentchain_block") - } - - fn shield_funds_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "shield_funds") - } - - fn unshield_funds_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "unshield_funds") - } - - fn publish_hash_call_indexes(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "publish_hash") - } - - fn update_scheduled_enclave(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "update_scheduled_enclave") - } - - fn remove_scheduled_enclave(&self) -> Result<[u8; 2]> { - self.call_indexes(TEEREX, "remove_scheduled_enclave") - } -} - -impl TeerexStorageKey for NodeMetadata { - fn sovereign_enclaves_storage_map_key(&self, index: u64) -> Result { - self.storage_map_key(TEEREX, "SovereignEnclaves", index) - } - fn proxied_enclaves_storage_map_key(&self, index: u64) -> Result { - self.storage_map_key(TEEREX, "ProxiedEnclaves", index) - } -} diff --git a/tee-worker/core-primitives/teerex-storage/Cargo.toml b/tee-worker/core-primitives/teerex-storage/Cargo.toml deleted file mode 100644 index ca9bafb791..0000000000 --- a/tee-worker/core-primitives/teerex-storage/Cargo.toml +++ /dev/null @@ -1,18 +0,0 @@ -[package] -name = "itp-teerex-storage" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -#local deps -itp-storage = { path = "../storage", default-features = false } - -[features] -default = ["std"] -std = [ - "sp-std/std", - "itp-storage/std", -] diff --git a/tee-worker/core-primitives/teerex-storage/src/lib.rs b/tee-worker/core-primitives/teerex-storage/src/lib.rs deleted file mode 100644 index 706d92fcb1..0000000000 --- a/tee-worker/core-primitives/teerex-storage/src/lib.rs +++ /dev/null @@ -1,35 +0,0 @@ -#![cfg_attr(not(feature = "std"), no_std)] - -use itp_storage::{storage_map_key, storage_value_key, StorageHasher}; -use sp_std::prelude::Vec; - -pub struct TeeRexStorage; - -// Separate the prefix from the rest because in our case we changed the storage prefix due to -// the rebranding. With the below implementation of the `TeerexStorageKeys`, we could simply -// define another struct `OtherStorage`, implement `StoragePrefix` for it, and get the -// `TeerexStorageKeys` implementation for free. -pub trait StoragePrefix { - fn prefix() -> &'static str; -} - -impl StoragePrefix for TeeRexStorage { - fn prefix() -> &'static str { - "Teerex" - } -} - -pub trait TeerexStorageKeys { - fn enclave_count() -> Vec; - fn enclave(index: u64) -> Vec; -} - -impl TeerexStorageKeys for S { - fn enclave_count() -> Vec { - storage_value_key(Self::prefix(), "EnclaveCount") - } - - fn enclave(index: u64) -> Vec { - storage_map_key(Self::prefix(), "EnclaveRegistry", &index, &StorageHasher::Blake2_128Concat) - } -} diff --git a/tee-worker/enclave-runtime/Cargo.lock b/tee-worker/enclave-runtime/Cargo.lock index 475206c1fc..67e0bbaca8 100644 --- a/tee-worker/enclave-runtime/Cargo.lock +++ b/tee-worker/enclave-runtime/Cargo.lock @@ -567,19 +567,6 @@ dependencies = [ "generic-array 0.14.7", ] -[[package]] -name = "common-primitives" -version = "0.1.0" -source = "git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42#eaf611b79bc9d56b20c155150e99b549bf98436b" -dependencies = [ - "derive_more", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "const-oid" version = "0.9.5" @@ -960,7 +947,6 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "teerex-primitives", "webpki", ] @@ -5346,22 +5332,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" -[[package]] -name = "teerex-primitives" -version = "0.1.0" -source = "git+https://github.com/integritee-network/pallets.git?branch=sdk-v0.12.0-polkadot-v0.9.42#eaf611b79bc9d56b20c155150e99b549bf98436b" -dependencies = [ - "common-primitives", - "derive_more", - "log", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "termcolor" version = "1.0.5" diff --git a/tee-worker/enclave-runtime/Cargo.toml b/tee-worker/enclave-runtime/Cargo.toml index 6b79f96a1a..ffe4a27b10 100644 --- a/tee-worker/enclave-runtime/Cargo.toml +++ b/tee-worker/enclave-runtime/Cargo.toml @@ -90,7 +90,6 @@ base58 = { rev = "sgx_1.1.3", package = "rust-base58", default-features = false, cid = { default-features = false, git = "https://github.com/whalelephant/rust-cid", branch = "nstd" } multibase = { default-features = false, git = "https://github.com/whalelephant/rust-multibase", branch = "nstd" } -teerex-primitives = { default-features = false, git = "https://github.com/integritee-network/pallets.git", branch = "sdk-v0.12.0-polkadot-v0.9.42" } # local deps ita-parentchain-interface = { path = "../app-libs/parentchain-interface", default-features = false, features = ["sgx"] } From 36e5cd6b6b0676fbe40f4d83d6d454fd5d93c904 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Thu, 15 Feb 2024 15:22:24 +0100 Subject: [PATCH 37/64] publish custiodan wallets (#2493) Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- .../enclave-api/ffi/src/lib.rs | 2 + .../enclave-api/src/enclave_base.rs | 14 +++++++ .../node-api/metadata/src/metadata_mocks.rs | 12 ++++++ .../node-api/metadata/src/pallet_bitacross.rs | 10 +++++ .../core-primitives/sgx/crypto/src/ecdsa.rs | 6 +-- .../core-primitives/sgx/crypto/src/schnorr.rs | 7 ++-- bitacross-worker/enclave-runtime/Cargo.lock | 2 + bitacross-worker/enclave-runtime/Cargo.toml | 2 + bitacross-worker/enclave-runtime/Enclave.edl | 4 +- .../enclave-runtime/src/initialization/mod.rs | 42 ++++++++++++++++++- bitacross-worker/enclave-runtime/src/lib.rs | 10 +++++ bitacross-worker/service/src/main_impl.rs | 2 + .../src/tests/mocks/enclave_api_mock.rs | 4 ++ pallets/bitacross/src/custodial_wallet.rs | 7 ++-- pallets/bitacross/src/lib.rs | 18 ++++---- 15 files changed, 122 insertions(+), 20 deletions(-) diff --git a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs index f70a36d54b..f39b1b7da3 100644 --- a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs @@ -252,4 +252,6 @@ extern "C" { until: *const u32, ) -> sgx_status_t; + pub fn publish_wallets(eid: sgx_enclave_id_t, retval: *mut sgx_status_t) -> sgx_status_t; + } diff --git a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs index b0f9dcb047..71f632ecfe 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs @@ -80,6 +80,9 @@ pub trait EnclaveBase: Send + Sync + 'static { // litentry fn migrate_shard(&self, old_shard: Vec, new_shard: Vec) -> EnclaveResult<()>; + + /// Publish generated wallets on parachain + fn publish_wallets(&self) -> EnclaveResult<()>; } /// EnclaveApi implementation for Enclave struct @@ -379,6 +382,17 @@ mod impl_ffi { Ok(()) } + + fn publish_wallets(&self) -> EnclaveResult<()> { + let mut retval = sgx_status_t::SGX_SUCCESS; + + let result = unsafe { ffi::publish_wallets(self.eid, &mut retval) }; + + ensure!(result == sgx_status_t::SGX_SUCCESS, Error::Sgx(result)); + ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); + + Ok(()) + } } fn init_parentchain_components_ffi( diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs index 3235c73ca5..ab4bdedcce 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/metadata_mocks.rs @@ -68,6 +68,8 @@ pub struct NodeMetadataMock { bitacross_module: u8, bitacross_add_relayer: u8, bitacross_remove_relayer: u8, + btc_wallet_generated: u8, + eth_wallet_generated: u8, } impl NodeMetadataMock { @@ -105,6 +107,8 @@ impl NodeMetadataMock { bitacross_module: 69u8, bitacross_add_relayer: 0u8, bitacross_remove_relayer: 1u8, + btc_wallet_generated: 2u8, + eth_wallet_generated: 3u8, } } } @@ -205,4 +209,12 @@ impl BitAcrossCallIndexes for NodeMetadataMock { fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]> { Ok([self.bitacross_module, self.bitacross_remove_relayer]) } + + fn btc_wallet_generated_indexes(&self) -> Result<[u8; 2]> { + Ok([self.bitacross_module, self.btc_wallet_generated]) + } + + fn eth_wallet_generated_indexes(&self) -> Result<[u8; 2]> { + Ok([self.bitacross_module, self.eth_wallet_generated]) + } } diff --git a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs index b18f575c15..914cd18704 100644 --- a/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs +++ b/bitacross-worker/core-primitives/node-api/metadata/src/pallet_bitacross.rs @@ -22,6 +22,8 @@ const BITACROSS: &str = "Bitacross"; pub trait BitAcrossCallIndexes { fn add_relayer_call_indexes(&self) -> Result<[u8; 2]>; fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]>; + fn btc_wallet_generated_indexes(&self) -> Result<[u8; 2]>; + fn eth_wallet_generated_indexes(&self) -> Result<[u8; 2]>; } impl BitAcrossCallIndexes for NodeMetadata { @@ -32,4 +34,12 @@ impl BitAcrossCallIndexes for NodeMetadata { fn remove_relayer_call_indexes(&self) -> Result<[u8; 2]> { self.call_indexes(BITACROSS, "remove_relayer") } + + fn btc_wallet_generated_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(BITACROSS, "btc_wallet_generated") + } + + fn eth_wallet_generated_indexes(&self) -> Result<[u8; 2]> { + self.call_indexes(BITACROSS, "eth_wallet_generated") + } } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index 1bbee23e2a..3c81afe672 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -22,7 +22,7 @@ use k256::{ elliptic_curve::group::GroupEncoding, PublicKey, }; -use std::{string::ToString, vec::Vec}; +use std::string::ToString; /// File name of the sealed seed file. pub const SEALED_SIGNER_SEED_FILE: &str = "ecdsa_key_sealed.bin"; @@ -39,8 +39,8 @@ impl Pair { Self { private, public } } - pub fn public_bytes(&self) -> Vec { - self.public.as_affine().to_bytes().as_slice().to_vec() + pub fn public_bytes(&self) -> [u8; 33] { + self.public.as_affine().to_bytes().as_slice().try_into().unwrap() } pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs index fe9e91f1fc..e9caa01651 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs @@ -22,7 +22,7 @@ use k256::{ schnorr::{signature::Signer, Signature, SigningKey}, PublicKey, }; -use std::{string::ToString, vec::Vec}; +use std::string::ToString; /// File name of the sealed seed file. pub const SEALED_SIGNER_SEED_FILE: &str = "schnorr_key_sealed.bin"; @@ -39,8 +39,9 @@ impl Pair { Self { private, public } } - pub fn public_bytes(&self) -> Vec { - self.public.as_affine().to_bytes().as_slice().to_vec() + pub fn public_bytes(&self) -> [u8; 33] { + // safe to unwrap here + self.public.as_affine().to_bytes().as_slice().try_into().unwrap() } pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 097731ae87..285bb971e4 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -943,6 +943,7 @@ dependencies = [ "itc-offchain-worker-executor", "itc-parentchain", "itc-parentchain-block-import-dispatcher", + "itc-parentchain-light-client", "itc-parentchain-test", "itc-peer-top-broadcaster", "itc-tls-websocket-server", @@ -953,6 +954,7 @@ dependencies = [ "itp-import-queue", "itp-node-api", "itp-node-api-metadata", + "itp-node-api-metadata-provider", "itp-nonce-cache", "itp-ocall-api", "itp-primitives-cache", diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index e0d80a7fa1..8c80fc75aa 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -99,6 +99,7 @@ itc-direct-rpc-server = { path = "../core/direct-rpc-server", default-features = itc-offchain-worker-executor = { path = "../core/offchain-worker-executor", default-features = false, features = ["sgx"] } itc-parentchain = { path = "../core/parentchain/parentchain-crate", default-features = false, features = ["sgx"] } itc-parentchain-block-import-dispatcher = { path = "../core/parentchain/block-import-dispatcher", default-features = false, features = ["sgx"] } +itc-parentchain-light-client = { path = "../core/parentchain/light-client", default-features = false } itc-parentchain-test = { path = "../core/parentchain/test", default-features = false } itc-peer-top-broadcaster = { path = "../core/peer-top-broadcaster", default-features = false, features = ["sgx"] } itc-tls-websocket-server = { path = "../core/tls-websocket-server", default-features = false, features = ["sgx"] } @@ -109,6 +110,7 @@ itp-extrinsics-factory = { path = "../core-primitives/extrinsics-factory", defau itp-import-queue = { path = "../core-primitives/import-queue", default-features = false, features = ["sgx"] } itp-node-api = { path = "../core-primitives/node-api", default-features = false, features = ["sgx"] } itp-node-api-metadata = { path = "../core-primitives/node-api/metadata", default-features = false } +itp-node-api-metadata-provider = { path = "../core-primitives/node-api/metadata-provider", default-features = false } itp-nonce-cache = { path = "../core-primitives/nonce-cache", default-features = false, features = ["sgx"] } itp-ocall-api = { path = "../core-primitives/ocall-api", default-features = false } itp-primitives-cache = { path = "../core-primitives/primitives-cache", default-features = false, features = ["sgx"] } diff --git a/bitacross-worker/enclave-runtime/Enclave.edl b/bitacross-worker/enclave-runtime/Enclave.edl index 80d878511b..d6b3073fb2 100644 --- a/bitacross-worker/enclave-runtime/Enclave.edl +++ b/bitacross-worker/enclave-runtime/Enclave.edl @@ -43,6 +43,8 @@ enclave { [in, size=encoded_base_dir_size] uint8_t* encoded_base_dir_str, uint32_t encoded_base_dir_size ); + public sgx_status_t publish_wallets(); + public sgx_status_t init_enclave_sidechain_components( [in, size=fail_mode_size] uint8_t* fail_mode, uint32_t fail_mode_size, [in, size=fail_at_size] uint8_t* fail_at, uint32_t fail_at_size @@ -174,7 +176,7 @@ enclave { [in, size=shard_size] uint8_t* new_shard, uint32_t shard_size ); - + public sgx_status_t ignore_parentchain_block_import_validation_until( [in] uint32_t* until ); diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index c2692bd1c6..12180e7cc0 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -61,6 +61,7 @@ use itc_direct_rpc_server::{ create_determine_watch, rpc_connection_registry::ConnectionRegistry, rpc_ws_handler::RpcWsHandler, }; +use itc_parentchain_light_client::{concurrent_access::ValidatorAccess, ExtrinsicSender}; use itc_peer_top_broadcaster::init; use itc_tls_websocket_server::{ certificate_generation::ed25519_self_signed_certificate, create_ws_server, ConnectionToken, @@ -68,6 +69,9 @@ use itc_tls_websocket_server::{ }; use itp_attestation_handler::{AttestationHandler, IntelAttestationHandler}; use itp_component_container::{ComponentGetter, ComponentInitializer}; +use itp_extrinsics_factory::CreateExtrinsics; +use itp_node_api_metadata::pallet_bitacross::BitAcrossCallIndexes; +use itp_node_api_metadata_provider::AccessNodeMetadata; use itp_primitives_cache::GLOBAL_PRIMITIVES_CACHE; use itp_settings::files::{ LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, STATE_SNAPSHOTS_CACHE_SIZE, @@ -84,7 +88,7 @@ use itp_stf_state_handler::{ }; use itp_top_pool::pool::Options as PoolOptions; use itp_top_pool_author::author::{AuthorTopFilter, BroadcastedTopFilter}; -use itp_types::{parentchain::ParentchainId, ShardIdentifier}; +use itp_types::{parentchain::ParentchainId, OpaqueCall, ShardIdentifier}; use its_sidechain::{ block_composer::BlockComposer, slots::{FailSlotMode, FailSlotOnDemand}, @@ -237,6 +241,42 @@ pub(crate) fn init_enclave( Ok(()) } +pub(crate) fn publish_wallets() -> EnclaveResult<()> { + let metadata_repository = get_node_metadata_repository_from_integritee_solo_or_parachain()?; + let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; + let validator_accessor = get_validator_accessor_from_integritee_solo_or_parachain()?; + + let bitcoin_key_repository = GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT.get()?; + let bitcoin_key = bitcoin_key_repository.retrieve_key()?; + + let bitcoin_call = metadata_repository + .get_from_metadata(|m| m.btc_wallet_generated_indexes()) + .map_err(|e| Error::Other(e.into()))? + .map_err(|e| Error::Other(format!("{:?}", e).into()))?; + + let bitcoin_opaque_call = OpaqueCall::from_tuple(&(bitcoin_call, bitcoin_key.public_bytes())); + + let ethereum_key_repository = GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.get()?; + let ethereum_key = ethereum_key_repository.retrieve_key()?; + + let ethereum_call = metadata_repository + .get_from_metadata(|m| m.eth_wallet_generated_indexes()) + .map_err(|e| Error::Other(e.into()))? + .map_err(|e| Error::Other(format!("{:?}", e).into()))?; + + let ethereum_opaque_call = + OpaqueCall::from_tuple(&(ethereum_call, ethereum_key.public_bytes())); + + let xts = extrinsics_factory + .create_extrinsics(&[bitcoin_opaque_call, ethereum_opaque_call], None) + .map_err(|e| Error::Other(e.into()))?; + validator_accessor + .execute_mut_on_validator(|v| v.send_extrinsics(xts)) + .map_err(|e| Error::Other(e.into()))?; + + Ok(()) +} + fn initialize_state_observer( snapshot_repository: &EnclaveStateSnapshotRepository, ) -> EnclaveResult> { diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index 03461047f0..3bcadef0b7 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -367,6 +367,16 @@ fn sidechain_rpc_int(request: &str) -> Result { .unwrap_or_else(|| format!("Empty rpc response for request: {}", request))) } +#[no_mangle] +pub unsafe extern "C" fn publish_wallets() -> sgx_status_t { + if let Err(e) = initialization::publish_wallets() { + error!("Failed to publish generated wallets: {:?}", e); + return sgx_status_t::SGX_ERROR_UNEXPECTED + } + + sgx_status_t::SGX_SUCCESS +} + /// Initialize sidechain enclave components. /// /// Call this once at startup. Has to be called AFTER the light-client diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index b87ac9f1b9..0c90fe9d36 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -610,6 +610,8 @@ fn start_worker( is_development_mode, ) } + // Publish generated custiodian wallets + enclave.publish_wallets(); // ------------------------------------------------------------------------ // Subscribe to events and print them. diff --git a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs index e9b5d4c884..565f33df5a 100644 --- a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs +++ b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs @@ -99,6 +99,10 @@ impl EnclaveBase for EnclaveMock { fn migrate_shard(&self, _old_shard: Vec, _new_shard: Vec) -> EnclaveResult<()> { unimplemented!() } + + fn publish_wallets(&self) -> EnclaveResult<()> { + unimplemented!() + } } impl Sidechain for EnclaveMock { diff --git a/pallets/bitacross/src/custodial_wallet.rs b/pallets/bitacross/src/custodial_wallet.rs index f20424124a..02243d315d 100644 --- a/pallets/bitacross/src/custodial_wallet.rs +++ b/pallets/bitacross/src/custodial_wallet.rs @@ -15,14 +15,15 @@ // along with Litentry. If not, see . use codec::{Decode, Encode}; -use core_primitives::{Address20, Address33}; use scale_info::TypeInfo; +pub type PubKey = [u8; 33]; + /// custodial wallet that each tee worker generates and holds #[derive(Encode, Decode, Clone, Default, Debug, PartialEq, Eq, TypeInfo)] pub struct CustodialWallet { - pub btc: Option, - pub eth: Option, + pub btc: Option, + pub eth: Option, } impl CustodialWallet { diff --git a/pallets/bitacross/src/lib.rs b/pallets/bitacross/src/lib.rs index 11579d0ae6..c216c2f355 100644 --- a/pallets/bitacross/src/lib.rs +++ b/pallets/bitacross/src/lib.rs @@ -16,7 +16,7 @@ #![cfg_attr(not(feature = "std"), no_std)] -use core_primitives::{Address20, Address33, Identity}; +use core_primitives::Identity; use frame_support::{ dispatch::{DispatchResult, DispatchResultWithPostInfo}, ensure, @@ -70,8 +70,8 @@ pub mod pallet { AdminSet { new_admin: Option }, RelayerAdded { who: Identity }, RelayerRemoved { who: Identity }, - BtcWalletGenerated { address: Address33 }, - EthWalletGenerated { address: Address20 }, + BtcWalletGenerated { pub_key: PubKey }, + EthWalletGenerated { pub_key: PubKey }, } #[pallet::error] @@ -149,13 +149,13 @@ pub mod pallet { #[pallet::weight(({195_000_000}, DispatchClass::Normal, Pays::No))] pub fn btc_wallet_generated( origin: OriginFor, - address: Address33, + pub_key: PubKey, ) -> DispatchResultWithPostInfo { let tee_account = T::TEECallOrigin::ensure_origin(origin)?; Vault::::try_mutate(tee_account, |v| { ensure!(!v.has_btc(), Error::::BtcWalletAlreadyExist); - v.btc = Some(address); - Self::deposit_event(Event::BtcWalletGenerated { address }); + v.btc = Some(pub_key); + Self::deposit_event(Event::BtcWalletGenerated { pub_key }); Ok(Pays::No.into()) }) } @@ -164,13 +164,13 @@ pub mod pallet { #[pallet::weight(({195_000_000}, DispatchClass::Normal, Pays::No))] pub fn eth_wallet_generated( origin: OriginFor, - address: Address20, + pub_key: PubKey, ) -> DispatchResultWithPostInfo { let tee_account = T::TEECallOrigin::ensure_origin(origin)?; Vault::::try_mutate(tee_account, |v| { ensure!(!v.has_eth(), Error::::EthWalletAlreadyExist); - v.eth = Some(address); - Self::deposit_event(Event::EthWalletGenerated { address }); + v.eth = Some(pub_key); + Self::deposit_event(Event::EthWalletGenerated { pub_key }); Ok(Pays::No.into()) }) } From eefbd20db206351b43e36ea6371cf1d6793ef5da Mon Sep 17 00:00:00 2001 From: Ariel Birnbaum Date: Thu, 15 Feb 2024 17:03:11 +0100 Subject: [PATCH 38/64] P-261 Refactor Achainable type definitions (#2464) --- primitives/core/src/assertion.rs | 158 ++++++-------- .../commands/litentry/request_vc.rs | 193 ++++++------------ .../commands/litentry/request_vc_direct.rs | 60 +----- .../interfaces/vc/definitions.ts | 22 +- .../src/rpc_watch_extractor.rs | 1 - .../assertion-build/src/achainable/amount.rs | 6 +- .../core/data-providers/src/achainable.rs | 25 ++- .../common/utils/vc-helper.ts | 8 +- 8 files changed, 170 insertions(+), 303 deletions(-) diff --git a/primitives/core/src/assertion.rs b/primitives/core/src/assertion.rs index e1efc63f20..8de472e606 100644 --- a/primitives/core/src/assertion.rs +++ b/primitives/core/src/assertion.rs @@ -29,100 +29,68 @@ use sp_std::{vec, vec::Vec}; pub type ParameterString = BoundedVec>; -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableAmountHolding { - pub name: ParameterString, - pub chain: Web3Network, - pub amount: ParameterString, - pub date: ParameterString, - pub token: Option, +macro_rules! AchainableRequestParams { + ($type_name:ident, {$( $field_name:ident : $field_type:ty , )* }) => { + #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] + pub struct $type_name { + pub name: ParameterString, + pub chain: BoundedWeb3Network, + $( pub $field_name: $field_type ),* + } + }; } -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableAmountToken { - pub name: ParameterString, - - // Considering the uniformity of the structure, all relevant chain structures should be changed - // to BoundedWeb3Network. However, this would be a significant modification for the previous - // VC. Considering the tight timeline for this New Year compain, we will temporarily only - // change this AchainableAmountToken' chain field to BoundedWeb3Network. Afterwards, it needs - // to be modified to be consistent. - pub chain: BoundedWeb3Network, - - pub amount: ParameterString, - pub token: Option, -} +AchainableRequestParams!(AchainableAmountHolding, { + amount: ParameterString, + date: ParameterString, + token: Option, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableAmount { - pub name: ParameterString, - pub chain: Web3Network, - pub amount: ParameterString, -} +AchainableRequestParams!(AchainableAmountToken, { + amount: ParameterString, + token: Option, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableAmounts { - pub name: ParameterString, - pub chain: Web3Network, - pub amount1: ParameterString, - pub amount2: ParameterString, -} +AchainableRequestParams!(AchainableAmount, { + amount: ParameterString, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableBasic { - pub name: ParameterString, - pub chain: Web3Network, -} +AchainableRequestParams!(AchainableAmounts, { + amount1: ParameterString, + amount2: ParameterString, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableBetweenPercents { - pub name: ParameterString, - pub chain: Web3Network, - pub greater_than_or_equal_to: ParameterString, - pub less_than_or_equal_to: ParameterString, -} +AchainableRequestParams!(AchainableBasic, {}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableClassOfYear { - pub name: ParameterString, - pub chain: Web3Network, -} +AchainableRequestParams!(AchainableBetweenPercents, { + greater_than_or_equal_to: ParameterString, + less_than_or_equal_to: ParameterString, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableDateInterval { - pub name: ParameterString, - pub chain: Web3Network, - pub start_date: ParameterString, - pub end_date: ParameterString, -} +AchainableRequestParams!(AchainableClassOfYear, {}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableDatePercent { - pub name: ParameterString, - pub chain: Web3Network, - pub token: ParameterString, - pub date: ParameterString, - pub percent: ParameterString, -} -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableDate { - pub name: ParameterString, - pub chain: Web3Network, - pub date: ParameterString, -} -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableToken { - pub name: ParameterString, - pub chain: Web3Network, - pub token: ParameterString, -} +AchainableRequestParams!(AchainableDateInterval, { + start_date: ParameterString, + end_date: ParameterString, +}); -#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] -pub struct AchainableMirror { - pub name: ParameterString, - pub chain: Web3Network, - pub post_quantity: Option, -} +AchainableRequestParams!(AchainableDatePercent, { + token: ParameterString, + date: ParameterString, + percent: ParameterString, +}); + +AchainableRequestParams!(AchainableDate, { + date: ParameterString, +}); + +AchainableRequestParams!(AchainableToken, { + token: ParameterString, +}); + +AchainableRequestParams!(AchainableMirror, { + post_quantity: Option, +}); #[rustfmt::skip] #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] @@ -173,18 +141,18 @@ impl AchainableParams { pub fn chains(&self) -> Vec { match self { - AchainableParams::AmountHolding(arg) => vec![arg.chain], + AchainableParams::AmountHolding(arg) => arg.chain.to_vec(), AchainableParams::AmountToken(arg) => arg.chain.to_vec(), - AchainableParams::Amount(arg) => vec![arg.chain], - AchainableParams::Amounts(arg) => vec![arg.chain], - AchainableParams::Basic(arg) => vec![arg.chain], - AchainableParams::BetweenPercents(arg) => vec![arg.chain], - AchainableParams::ClassOfYear(arg) => vec![arg.chain], - AchainableParams::DateInterval(arg) => vec![arg.chain], - AchainableParams::DatePercent(arg) => vec![arg.chain], - AchainableParams::Date(arg) => vec![arg.chain], - AchainableParams::Token(arg) => vec![arg.chain], - AchainableParams::Mirror(arg) => vec![arg.chain], + AchainableParams::Amount(arg) => arg.chain.to_vec(), + AchainableParams::Amounts(arg) => arg.chain.to_vec(), + AchainableParams::Basic(arg) => arg.chain.to_vec(), + AchainableParams::BetweenPercents(arg) => arg.chain.to_vec(), + AchainableParams::ClassOfYear(arg) => arg.chain.to_vec(), + AchainableParams::DateInterval(arg) => arg.chain.to_vec(), + AchainableParams::DatePercent(arg) => arg.chain.to_vec(), + AchainableParams::Date(arg) => arg.chain.to_vec(), + AchainableParams::Token(arg) => arg.chain.to_vec(), + AchainableParams::Mirror(arg) => arg.chain.to_vec(), } } } diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs index 7497931eb7..5a3adb496f 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs @@ -228,101 +228,76 @@ pub enum PlatformUserCommand { KaratDaoUser, } -// I haven't found a good way to use common args for subcommands -#[derive(Args, Debug)] -pub struct AmountHoldingArg { - pub name: String, - pub chain: String, - pub amount: String, - pub date: String, - pub token: Option, -} - // positional args (to vec) + required arg + optional arg is a nightmare combination for clap parser, // additionally, only the last positional argument, or second to last positional argument may be set to `.num_args()` // // the best bet is to use a flag explicitly, be sure to use euqal form for `chain`, e.g.: // -- name -c=bsc,ethereum 10 // -- name -c=bsc,ethereum 10 token -#[derive(Args, Debug)] -pub struct AmountTokenArg { - pub name: String, - #[clap( - short, long, - num_args = 1.., - required = true, - value_delimiter = ',', - )] - pub chain: Vec, - pub amount: String, - pub token: Option, +macro_rules! AchainableCommandArgs { + ($type_name:ident, {$( $field_name:ident : $field_type:ty , )* }) => { + #[derive(Args, Debug)] + pub struct $type_name { + pub name: String, + #[clap( + short, long, + num_args = 1.., + required = true, + value_delimiter = ',', + )] + pub chain: Vec, + $( pub $field_name: $field_type ),* + } + }; } -#[derive(Args, Debug)] -pub struct AmountArg { - pub name: String, - pub chain: String, - pub amount: String, -} +AchainableCommandArgs!(AmountHoldingArg, { + amount: String, + date: String, + token: Option, +}); -#[derive(Args, Debug)] -pub struct AmountsArg { - pub name: String, - pub chain: String, - pub amount1: String, - pub amount2: String, -} +AchainableCommandArgs!(AmountTokenArg, { + amount: String, + token: Option, +}); -#[derive(Args, Debug)] -pub struct BasicArg { - pub name: String, - pub chain: String, -} +AchainableCommandArgs!(AmountArg, { + amount: String, +}); -#[derive(Args, Debug)] -pub struct BetweenPercentsArg { - pub name: String, - pub chain: String, - pub greater_than_or_equal_to: String, - pub less_than_or_equal_to: String, -} +AchainableCommandArgs!(AmountsArg, { + amount1: String, + amount2: String, +}); -#[derive(Args, Debug)] -pub struct ClassOfYearArg { - pub name: String, - pub chain: String, -} +AchainableCommandArgs!(BasicArg, {}); -#[derive(Args, Debug)] -pub struct DateIntervalArg { - pub name: String, - pub chain: String, - pub start_date: String, - pub end_date: String, -} +AchainableCommandArgs!(BetweenPercentsArg, { + greater_than_or_equal_to: String, + less_than_or_equal_to: String, +}); -#[derive(Args, Debug)] -pub struct DatePercentArg { - pub name: String, - pub chain: String, - pub token: String, - pub date: String, - pub percent: String, -} +AchainableCommandArgs!(ClassOfYearArg, {}); -#[derive(Args, Debug)] -pub struct DateArg { - pub name: String, - pub chain: String, - pub date: String, -} +AchainableCommandArgs!(DateIntervalArg, { + start_date: String, + end_date: String, +}); -#[derive(Args, Debug)] -pub struct TokenArg { - pub name: String, - pub chain: String, - pub token: String, -} +AchainableCommandArgs!(DatePercentArg, { + token: String, + date: String, + percent: String, +}); + +AchainableCommandArgs!(DateArg, { + date: String, +}); + +AchainableCommandArgs!(TokenArg, { + token: String, +}); impl RequestVcCommand { pub(crate) fn run(&self, cli: &Cli, trusted_cli: &TrustedCli) -> CliResult { @@ -369,11 +344,7 @@ impl RequestVcCommand { AchainableCommand::AmountHolding(arg) => Assertion::Achainable( AchainableParams::AmountHolding(AchainableAmountHolding { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount: to_para_str(&arg.amount), date: to_para_str(&arg.date), token: arg.token.as_ref().map(|s| to_para_str(s)), @@ -389,41 +360,25 @@ impl RequestVcCommand { AchainableCommand::Amount(arg) => Assertion::Achainable(AchainableParams::Amount(AchainableAmount { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount: to_para_str(&arg.amount), })), AchainableCommand::Amounts(arg) => Assertion::Achainable(AchainableParams::Amounts(AchainableAmounts { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount1: to_para_str(&arg.amount1), amount2: to_para_str(&arg.amount2), })), AchainableCommand::Basic(arg) => Assertion::Achainable(AchainableParams::Basic(AchainableBasic { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), })), AchainableCommand::BetweenPercents(arg) => Assertion::Achainable( AchainableParams::BetweenPercents(AchainableBetweenPercents { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), greater_than_or_equal_to: to_para_str(&arg.greater_than_or_equal_to), less_than_or_equal_to: to_para_str(&arg.less_than_or_equal_to), }), @@ -431,31 +386,19 @@ impl RequestVcCommand { AchainableCommand::ClassOfYear(arg) => Assertion::Achainable(AchainableParams::ClassOfYear(AchainableClassOfYear { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), })), AchainableCommand::DateInterval(arg) => Assertion::Achainable(AchainableParams::DateInterval(AchainableDateInterval { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), start_date: to_para_str(&arg.start_date), end_date: to_para_str(&arg.end_date), })), AchainableCommand::DatePercent(arg) => Assertion::Achainable(AchainableParams::DatePercent(AchainableDatePercent { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), date: to_para_str(&arg.date), percent: to_para_str(&arg.percent), token: to_para_str(&arg.token), @@ -463,21 +406,13 @@ impl RequestVcCommand { AchainableCommand::Date(arg) => Assertion::Achainable(AchainableParams::Date(AchainableDate { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), date: to_para_str(&arg.date), })), AchainableCommand::Token(arg) => Assertion::Achainable(AchainableParams::Token(AchainableToken { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), token: to_para_str(&arg.token), })), }, diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs index 75255f4d29..9c3bd08091 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs @@ -110,11 +110,7 @@ impl RequestVcDirectCommand { AchainableCommand::AmountHolding(arg) => Assertion::Achainable( AchainableParams::AmountHolding(AchainableAmountHolding { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount: to_para_str(&arg.amount), date: to_para_str(&arg.date), token: arg.token.as_ref().map(|s| to_para_str(s)), @@ -130,41 +126,25 @@ impl RequestVcDirectCommand { AchainableCommand::Amount(arg) => Assertion::Achainable(AchainableParams::Amount(AchainableAmount { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount: to_para_str(&arg.amount), })), AchainableCommand::Amounts(arg) => Assertion::Achainable(AchainableParams::Amounts(AchainableAmounts { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), amount1: to_para_str(&arg.amount1), amount2: to_para_str(&arg.amount2), })), AchainableCommand::Basic(arg) => Assertion::Achainable(AchainableParams::Basic(AchainableBasic { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), })), AchainableCommand::BetweenPercents(arg) => Assertion::Achainable( AchainableParams::BetweenPercents(AchainableBetweenPercents { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), greater_than_or_equal_to: to_para_str(&arg.greater_than_or_equal_to), less_than_or_equal_to: to_para_str(&arg.less_than_or_equal_to), }), @@ -172,31 +152,19 @@ impl RequestVcDirectCommand { AchainableCommand::ClassOfYear(arg) => Assertion::Achainable(AchainableParams::ClassOfYear(AchainableClassOfYear { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), })), AchainableCommand::DateInterval(arg) => Assertion::Achainable(AchainableParams::DateInterval(AchainableDateInterval { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), start_date: to_para_str(&arg.start_date), end_date: to_para_str(&arg.end_date), })), AchainableCommand::DatePercent(arg) => Assertion::Achainable(AchainableParams::DatePercent(AchainableDatePercent { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), date: to_para_str(&arg.date), percent: to_para_str(&arg.percent), token: to_para_str(&arg.token), @@ -204,21 +172,13 @@ impl RequestVcDirectCommand { AchainableCommand::Date(arg) => Assertion::Achainable(AchainableParams::Date(AchainableDate { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), date: to_para_str(&arg.date), })), AchainableCommand::Token(arg) => Assertion::Achainable(AchainableParams::Token(AchainableToken { name: to_para_str(&arg.name), - chain: arg - .chain - .as_str() - .try_into() - .expect("cannot convert to Web3Network"), + chain: to_chains(&arg.chain), token: to_para_str(&arg.token), })), }, diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts index f5889c3300..1976a90040 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts @@ -59,63 +59,63 @@ export default { }, AchainableAmountHolding: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", amount: "Bytes", date: "Bytes", token: "Option", }, AchainableAmountToken: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", amount: "Bytes", token: "Option", }, AchainableAmount: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", amount: "Bytes", }, AchainableAmounts: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", amount1: "Bytes", amount2: "Bytes", }, AchainableBasic: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", }, AchainableBetweenPercents: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", greaterThanOrEqualTo: "Bytes", lessThanOrEqualTo: "Bytes", }, AchainableClassOfYear: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", }, AchainableDateInterval: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", startDate: "Bytes", endDate: "Bytes", }, AchainableDatePercent: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", token: "Bytes", date: "Bytes", percent: "Bytes", }, AchainableDate: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", date: "Bytes", }, AchainableToken: { name: "Bytes", - chain: "Web3Network", + chain: "Vec", token: "Bytes", }, // Oneblock diff --git a/tee-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs b/tee-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs index 141ff21b54..461142f97c 100644 --- a/tee-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs +++ b/tee-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs @@ -78,7 +78,6 @@ pub mod tests { use crate::builders::{ rpc_response_builder::RpcResponseBuilder, rpc_return_value_builder::RpcReturnValueBuilder, }; - use codec::Encode; use itp_rpc::Id; use itp_types::{TrustedOperationStatus, H256}; diff --git a/tee-worker/litentry/core/assertion-build/src/achainable/amount.rs b/tee-worker/litentry/core/assertion-build/src/achainable/amount.rs index af9245bb9d..d1a75b3169 100644 --- a/tee-worker/litentry/core/assertion-build/src/achainable/amount.rs +++ b/tee-worker/litentry/core/assertion-build/src/achainable/amount.rs @@ -140,8 +140,10 @@ pub fn build_amount( if bname == AchainableNameAmount::BalanceUnderAmount { credential_unsigned.update_eth_holding_amount(balance); } else { - let (desc, subtype, content) = - get_assertion_content(&achainable_param.to_string(¶m.name)?, ¶m.chain); + let (desc, subtype, content) = get_assertion_content( + &achainable_param.to_string(¶m.name)?, + ¶m.chain[0], + ); credential_unsigned.add_subject_info(desc, subtype); credential_unsigned.update_content(flag, content); } diff --git a/tee-worker/litentry/core/data-providers/src/achainable.rs b/tee-worker/litentry/core/data-providers/src/achainable.rs index 16636ca8b5..1b432bff0f 100644 --- a/tee-worker/litentry/core/data-providers/src/achainable.rs +++ b/tee-worker/litentry/core/data-providers/src/achainable.rs @@ -235,7 +235,8 @@ impl TryFrom for Params { let token = if p.token.is_some() { Some(ap.to_string(&p.token.unwrap())?) } else { None }; - let p = ParamsBasicTypeWithAmountHolding::one(name, network, amount, date, token); + let p = + ParamsBasicTypeWithAmountHolding::one(name, &network[0], amount, date, token); Ok(Params::ParamsBasicTypeWithAmountHolding(p)) }, AchainableParams::AmountToken(p) => { @@ -255,7 +256,7 @@ impl TryFrom for Params { let network = &p.chain; let amount = ap.to_string(&p.amount)?; - let p = ParamsBasicTypeWithAmount::new(name, network, amount); + let p = ParamsBasicTypeWithAmount::new(name, &network[0], amount); Ok(Params::ParamsBasicTypeWithAmount(p)) }, AchainableParams::Amounts(p) => { @@ -264,14 +265,14 @@ impl TryFrom for Params { let amount1 = ap.to_string(&p.amount1)?; let amount2 = ap.to_string(&p.amount2)?; - let p = ParamsBasicTypeWithAmounts::new(name, network, amount1, amount2); + let p = ParamsBasicTypeWithAmounts::new(name, &network[0], amount1, amount2); Ok(Params::ParamsBasicTypeWithAmounts(p)) }, AchainableParams::Basic(p) => { let name = ap.to_string(&p.name)?; let network = &p.chain; - let p = ParamsBasicType::new(name, network); + let p = ParamsBasicType::new(name, &network[0]); Ok(Params::ParamsBasicType(p)) }, AchainableParams::BetweenPercents(p) => { @@ -282,7 +283,7 @@ impl TryFrom for Params { let p = ParamsBasicTypeWithBetweenPercents::new( name, - network, + &network[0], greater_than_or_equal_to, less_than_or_equal_to, ); @@ -299,7 +300,7 @@ impl TryFrom for Params { let date1 = "2015-01-01".into(); let date2 = "2023-01-01".into(); - let p = ParamsBasicTypeWithClassOfYear::new(name, network, date1, date2); + let p = ParamsBasicTypeWithClassOfYear::new(name, &network[0], date1, date2); Ok(Params::ParamsBasicTypeWithClassOfYear(p)) }, AchainableParams::DateInterval(p) => { @@ -308,7 +309,8 @@ impl TryFrom for Params { let start_date = ap.to_string(&p.start_date)?; let end_date = ap.to_string(&p.end_date)?; - let p = ParamsBasicTypeWithDateInterval::new(name, network, start_date, end_date); + let p = + ParamsBasicTypeWithDateInterval::new(name, &network[0], start_date, end_date); Ok(Params::ParamsBasicTypeWithDateInterval(p)) }, AchainableParams::DatePercent(p) => { @@ -318,7 +320,8 @@ impl TryFrom for Params { let date = ap.to_string(&p.date)?; let percent = ap.to_string(&p.percent)?; - let p = ParamsBasicTypeWithDatePercent::new(name, network, token, date, percent); + let p = + ParamsBasicTypeWithDatePercent::new(name, &network[0], token, date, percent); Ok(Params::ParamsBasicTypeWithDatePercent(p)) }, AchainableParams::Date(p) => { @@ -326,7 +329,7 @@ impl TryFrom for Params { let network = &p.chain; let date = ap.to_string(&p.date)?; - let p = ParamsBasicTypeWithDate::new(name, network, date); + let p = ParamsBasicTypeWithDate::new(name, &network[0], date); Ok(Params::ParamsBasicTypeWithDate(p)) }, AchainableParams::Token(p) => { @@ -334,7 +337,7 @@ impl TryFrom for Params { let network = &p.chain; let token = ap.to_string(&p.token)?; - let p = ParamsBasicTypeWithToken::new(name, network, token); + let p = ParamsBasicTypeWithToken::new(name, &network[0], token); Ok(Params::ParamsBasicTypeWithToken(p)) }, AchainableParams::Mirror(p) => { @@ -348,7 +351,7 @@ impl TryFrom for Params { None }; - let p = ParamsBasicTypeWithMirror::new(name, network, post_quantity); + let p = ParamsBasicTypeWithMirror::new(name, &network[0], post_quantity); Ok(Params::ParamsBasicTypeWithMirror(p)) }, } diff --git a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts index c5df98b20d..5adc06230d 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts @@ -78,7 +78,7 @@ export const defaultAssertions = [ Achainable: { Basic: { name: 'Uniswap V2/V3 user', - chain: 'Ethereum', + chain: ['Ethereum'], }, }, }, @@ -89,7 +89,7 @@ export const defaultAssertions = [ Achainable: { Amount: { name: 'Balance over {amount}', - chain: 'Ethereum', + chain: ['Ethereum'], amount: '0', }, @@ -102,7 +102,7 @@ export const defaultAssertions = [ Achainable: { ClassOfYear: { name: 'Account created between {dates}', - chain: 'Ethereum', + chain: ['Ethereum'], }, }, }, @@ -113,7 +113,7 @@ export const defaultAssertions = [ Achainable: { Amount: { name: 'Created over {amount} contracts', - chain: 'Ethereum', + chain: ['Ethereum'], amount: '0', }, }, From 2fd34d3bf57c0de10c6a1bb9867c7b69976eb3ab Mon Sep 17 00:00:00 2001 From: Jonathan Alvarez Date: Thu, 15 Feb 2024 15:28:26 -0500 Subject: [PATCH 39/64] feat(vc): include and populate credentialSchema property (#2473) Signed-off-by: Jonathan Alvarez --- .../src/achainable/uniswap_user.rs | 4 +- .../core/credentials/src/credential_schema.rs | 107 ++++++++++++++++++ .../litentry/core/credentials/src/lib.rs | 11 +- .../credentials/src/templates/credential.json | 4 +- .../receiver/src/handler/assertion.rs | 3 + 5 files changed, 119 insertions(+), 10 deletions(-) create mode 100644 tee-worker/litentry/core/credentials/src/credential_schema.rs diff --git a/tee-worker/litentry/core/credentials/src/achainable/uniswap_user.rs b/tee-worker/litentry/core/credentials/src/achainable/uniswap_user.rs index c34ef97db0..590c3e5a4e 100644 --- a/tee-worker/litentry/core/credentials/src/achainable/uniswap_user.rs +++ b/tee-worker/litentry/core/credentials/src/achainable/uniswap_user.rs @@ -21,8 +21,8 @@ use crate::{ use std::string::ToString; const UNISWAP_USER_DESCRIPTIONS: &str = - "You are a trader or liquidity provider of Uniswap V2 or V3. -Uniswap V2 Factory Contract: 0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f. + "You are a trader or liquidity provider of Uniswap V2 or V3. +Uniswap V2 Factory Contract: 0x5c69bee701ef814a2b6a3edd4b1652cb9cc5aa6f. Uniswap V3 Factory Contract: 0x1f98431c8ad98523631ae4a59f267346ea31f984."; const UNISWAP_USER_TYPE: &str = "Uniswap V2/V3 User"; diff --git a/tee-worker/litentry/core/credentials/src/credential_schema.rs b/tee-worker/litentry/core/credentials/src/credential_schema.rs new file mode 100644 index 0000000000..753093a5fc --- /dev/null +++ b/tee-worker/litentry/core/credentials/src/credential_schema.rs @@ -0,0 +1,107 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use litentry_primitives::{AchainableParams, Assertion}; +use std::string::{String, ToString}; + +const BASE_URL: &str = "https://raw.githubusercontent.com/litentry/vc-jsonschema/main/dist/schemas"; +const NOT_IMPLEMENTED: &str = + "https://raw.githubusercontent.com/litentry/vc-jsonschema/main/dist/schemas/0-base.json"; + +/// Returns the respective JSON Schema for the given assertion and its credential. +/// JSON Schemas can be found at https://github.com/litentry/vc-jsonschema +pub fn get_schema_url(assertion: &Assertion) -> String { + match assertion { + Assertion::A1 => format!("{BASE_URL}/1-basic-identity-verification/1-0-0.json"), + + Assertion::A2(_) => format!("{BASE_URL}/2-litentry-discord-member/1-0-0.json"), + + Assertion::A3(_, _, _) => format!("{BASE_URL}/3-active-discord-id-hubber/1-0-0.json"), + + Assertion::A4(_) => format!("{BASE_URL}/4-token-holding-time/1-0-0.json"), + + Assertion::A6 => format!("{BASE_URL}/6-twitter-follower-amount/1-0-0.json"), + + Assertion::A7(_) => format!("{BASE_URL}/4-token-holding-time/1-0-0.json"), + + Assertion::A8(_) => format!("{BASE_URL}/7-evm-substrate-transaction-count/1-0-0.json"), + + Assertion::A10(_) => format!("{BASE_URL}/4-token-holding-time/1-0-0.json"), + + Assertion::A11(_) => format!("{BASE_URL}/4-token-holding-time/1-0-0.json"), + + Assertion::A13(_) => format!("{BASE_URL}/8-decoded-2023-basic-special-badge/1-0-0.json"), + + Assertion::A14 => + format!("{BASE_URL}/9-polkadot-governance-participation-proof/1-0-0.json"), + + Assertion::Achainable(params) => match params { + AchainableParams::AmountHolding(_) => + format!("{BASE_URL}/17-token-holding-amount/1-0-0.json"), + + AchainableParams::AmountToken(_) => + format!("{BASE_URL}/17-token-holding-amount/1-0-0.json"), + + AchainableParams::Amount(_) => format!("{BASE_URL}/11-token-holder/1-0-0.json"), + + AchainableParams::Basic(_) => format!("{BASE_URL}/11-token-holder/1-0-0.json"), + + AchainableParams::ClassOfYear(_) => + format!("{BASE_URL}/10-account-class-of-year/1-0-0.json"), + + AchainableParams::Mirror(_) => format!("{BASE_URL}/22-mirror-contributor/1-0-0.json"), + + // The following assertions are Unused and produce no specific claims. They Generates + // generic JSON Credentials + AchainableParams::Amounts(_) => NOT_IMPLEMENTED.to_string(), + AchainableParams::BetweenPercents(_) => NOT_IMPLEMENTED.to_string(), + AchainableParams::Date(_) => NOT_IMPLEMENTED.to_string(), + AchainableParams::DateInterval(_) => NOT_IMPLEMENTED.to_string(), + AchainableParams::DatePercent(_) => NOT_IMPLEMENTED.to_string(), + AchainableParams::Token(_) => NOT_IMPLEMENTED.to_string(), + }, + + Assertion::A20 => format!("{BASE_URL}/12-idhub-evm-version-early-bird/1-0-0.json"), + + Assertion::Oneblock(_) => format!("{BASE_URL}/13-oneblock-student-phase-12/1-0-0.json"), + + Assertion::GenericDiscordRole(_) => + format!("{BASE_URL}/14-generic-discord-role/1-0-0.json"), + + Assertion::BnbDomainHolding => + format!("{BASE_URL}/15-bnb-domain-holding-amount/1-0-0.json"), + + Assertion::BnbDigitDomainClub(_) => + format!("{BASE_URL}/16-bnb-3d-4d-club-domain-holding-amount/1-0-0.json"), + + Assertion::VIP3MembershipCard(_) => format!("{BASE_URL}/19-vip3-card-holder/1-0-0.json"), + + Assertion::WeirdoGhostGangHolder => + format!("{BASE_URL}/18-weirdoghostgang-holder/1-0-0.json"), + + Assertion::LITStaking => format!("{BASE_URL}/17-token-holding-amount/1-0-0.json"), + + Assertion::TokenHoldingAmount(_) | Assertion::EVMAmountHolding(_) => + format!("{BASE_URL}/21-evm-holding-amount/1-0-0.json"), + + Assertion::BRC20AmountHolder => + format!("{BASE_URL}/20-token-holding-amount-list/1-0-0.json"), + + Assertion::CryptoSummary => format!("{BASE_URL}/23-crypto-summary/1-0-0.json"), + + Assertion::PlatformUser(_) => format!("{BASE_URL}/24-platform-user/1-0-0.json"), + } +} diff --git a/tee-worker/litentry/core/credentials/src/lib.rs b/tee-worker/litentry/core/credentials/src/lib.rs index b35050ff72..d28b66c9b3 100644 --- a/tee-worker/litentry/core/credentials/src/lib.rs +++ b/tee-worker/litentry/core/credentials/src/lib.rs @@ -82,6 +82,7 @@ pub mod oneblock; pub mod schema; use assertion_logic::{AssertionLogic, Op}; pub mod brc20; +pub mod credential_schema; pub mod generic_discord_role; pub mod nodereal; pub mod vip3; @@ -166,9 +167,9 @@ impl CredentialSubject { #[derive(Serialize, Deserialize, Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo)] #[serde(rename_all = "camelCase")] pub struct CredentialSchema { - /// Schema ID that is maintained by Parentchain VCMP + /// Schema ID that is maintained in https://github.com/litentry/vc-jsonschema pub id: String, - /// The schema type, generally it is + /// The schema type, generally it is JSON Schema Draft 7 (JSONSchema7/JSONSchema2018) #[serde(rename = "type")] pub types: String, } @@ -227,9 +228,8 @@ pub struct Credential { /// Digital proof with the signature of Issuer #[serde(skip_serializing_if = "Option::is_none")] pub proof: Option, - #[serde(skip_deserializing)] - #[serde(skip_serializing_if = "Option::is_none")] - pub credential_schema: Option, + /// The JSON Schema information the credential follows + pub credential_schema: CredentialSchema, } impl Credential { @@ -253,7 +253,6 @@ impl Credential { vc.credential_subject.id = subject.to_did().map_err(|err| Error::ParseError(err.to_string()))?; vc.issuance_date = now_as_iso8601(); - vc.credential_schema = None; vc.proof = None; vc.generate_id(); diff --git a/tee-worker/litentry/core/credentials/src/templates/credential.json b/tee-worker/litentry/core/credentials/src/templates/credential.json index c26af00bf1..effeba0b14 100644 --- a/tee-worker/litentry/core/credentials/src/templates/credential.json +++ b/tee-worker/litentry/core/credentials/src/templates/credential.json @@ -37,7 +37,7 @@ "verificationMethod":"" }, "credentialSchema":{ - "id":"http://apps.litentry.com/credential/schemas/2023", + "id":"https://github.com/litentry/vc-jsonschema/", "type":"JsonSchemaValidator2018" } -} \ No newline at end of file +} diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index 334428bf51..85cd5f3b2c 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -25,6 +25,7 @@ use itp_stf_executor::traits::StfEnclaveSigning; use itp_stf_state_handler::handle_state::HandleState; use itp_top_pool_author::traits::AuthorApi; use itp_types::ShardIdentifier; +use lc_credentials::credential_schema; use lc_data_providers::DataProviderConfig; use lc_stf_task_sender::AssertionBuildRequest; use litentry_primitives::{ @@ -284,6 +285,8 @@ where credential.credential_subject.assertion_text = format!("{:?}", req.assertion); + credential.credential_schema.id = credential_schema::get_schema_url(&req.assertion); + credential.issuer.id = Identity::Substrate(enclave_account.into()).to_did().map_err(|e| { VCMPError::RequestVCFailed( req.assertion.clone(), From 1c5f281217cf7158e9db2ea8ab683d4971a9daf9 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:05:02 +0100 Subject: [PATCH 40/64] Use ed25519::Public (#2495) --- bitacross-worker/enclave-runtime/src/attestation.rs | 6 +++--- pallets/teebag/src/lib.rs | 4 ++-- pallets/teebag/src/types.rs | 5 +++-- tee-worker/enclave-runtime/src/attestation.rs | 6 +++--- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/bitacross-worker/enclave-runtime/src/attestation.rs b/bitacross-worker/enclave-runtime/src/attestation.rs index 895a37a511..5faa2b6893 100644 --- a/bitacross-worker/enclave-runtime/src/attestation.rs +++ b/bitacross-worker/enclave-runtime/src/attestation.rs @@ -58,7 +58,7 @@ use itp_types::{AttestationType, OpaqueCall, WorkerType}; use itp_utils::write_slice_and_whitespace_pad; use log::*; use sgx_types::*; -use sp_core::Pair; +use sp_core::{ed25519::Public as Ed25519Public, Pair}; use sp_runtime::OpaqueExtrinsic; use std::{prelude::v1::*, slice, vec::Vec}; @@ -569,13 +569,13 @@ fn get_shielding_pubkey() -> EnclaveResult>> { Ok(shielding_pubkey) } -fn get_vc_pubkey() -> EnclaveResult>> { +fn get_vc_pubkey() -> EnclaveResult> { let vc_pubkey = GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT .get()? .retrieve_key() .and_then(|keypair| { // vc signing pubkey - keypair.derive_ed25519().map(|keypair| keypair.public().to_vec()) + keypair.derive_ed25519().map(|keypair| keypair.public()) }) .ok(); diff --git a/pallets/teebag/src/lib.rs b/pallets/teebag/src/lib.rs index 7d60354b30..3463520278 100644 --- a/pallets/teebag/src/lib.rs +++ b/pallets/teebag/src/lib.rs @@ -25,7 +25,7 @@ use frame_support::{ traits::Get, }; use frame_system::pallet_prelude::*; -use sp_core::H256; +use sp_core::{ed25519::Public as Ed25519Public, H256}; use sp_runtime::traits::{CheckedSub, SaturatedConversion}; use sp_std::{prelude::*, str}; @@ -417,7 +417,7 @@ pub mod pallet { attestation: Vec, worker_url: Vec, shielding_pubkey: Option>, - vc_pubkey: Option>, + vc_pubkey: Option, attestation_type: AttestationType, ) -> DispatchResultWithPostInfo { let sender = ensure_signed(origin)?; diff --git a/pallets/teebag/src/types.rs b/pallets/teebag/src/types.rs index 874214cf42..b67e9df6b8 100644 --- a/pallets/teebag/src/types.rs +++ b/pallets/teebag/src/types.rs @@ -14,6 +14,7 @@ // You should have received a copy of the GNU General Public License // along with Litentry. If not, see . +use crate::Ed25519Public; use codec::{Decode, Encode}; use scale_info::TypeInfo; #[cfg(feature = "std")] @@ -105,7 +106,7 @@ pub struct Enclave { pub last_seen_timestamp: u64, // unix epoch in milliseconds when it's last seen pub url: Vec, // utf8 encoded url pub shielding_pubkey: Option>, // JSON serialised enclave shielding pub key - pub vc_pubkey: Option>, + pub vc_pubkey: Option, pub sgx_build_mode: SgxBuildMode, pub attestation_type: AttestationType, } @@ -135,7 +136,7 @@ impl Enclave { self } - pub fn with_vc_pubkey(mut self, vc_pubkey: Option>) -> Self { + pub fn with_vc_pubkey(mut self, vc_pubkey: Option) -> Self { self.vc_pubkey = vc_pubkey; self } diff --git a/tee-worker/enclave-runtime/src/attestation.rs b/tee-worker/enclave-runtime/src/attestation.rs index 7eaf912e40..933ba536a9 100644 --- a/tee-worker/enclave-runtime/src/attestation.rs +++ b/tee-worker/enclave-runtime/src/attestation.rs @@ -58,7 +58,7 @@ use itp_types::{AttestationType, OpaqueCall, WorkerType}; use itp_utils::write_slice_and_whitespace_pad; use log::*; use sgx_types::*; -use sp_core::Pair; +use sp_core::{ed25519::Public as Ed25519Public, Pair}; use sp_runtime::OpaqueExtrinsic; use std::{prelude::v1::*, slice, vec::Vec}; @@ -572,13 +572,13 @@ fn get_shielding_pubkey() -> EnclaveResult>> { Ok(shielding_pubkey) } -fn get_vc_pubkey() -> EnclaveResult>> { +fn get_vc_pubkey() -> EnclaveResult> { let vc_pubkey = GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT .get()? .retrieve_key() .and_then(|keypair| { // vc signing pubkey - keypair.derive_ed25519().map(|keypair| keypair.public().to_vec()) + keypair.derive_ed25519().map(|keypair| keypair.public()) }) .ok(); From 81381a5399949ec40221627cb2e808b1ffc5d94f Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Fri, 16 Feb 2024 09:30:41 +0100 Subject: [PATCH 41/64] Bump runtime versions (#2494) --- bitacross-worker/app-libs/sgx-runtime/src/lib.rs | 2 +- runtime/litentry/src/lib.rs | 2 +- runtime/litmus/src/lib.rs | 2 +- runtime/rococo/src/lib.rs | 2 +- tee-worker/app-libs/sgx-runtime/src/lib.rs | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs index 89da384c87..e293dea94e 100644 --- a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs +++ b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs @@ -130,7 +130,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("node-template"), impl_name: create_runtime_str!("node-template"), authoring_version: 1, - spec_version: 102, + spec_version: 104, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/litentry/src/lib.rs b/runtime/litentry/src/lib.rs index fd3e2b13bd..9be9cf7a2a 100644 --- a/runtime/litentry/src/lib.rs +++ b/runtime/litentry/src/lib.rs @@ -142,7 +142,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("litentry-parachain"), authoring_version: 1, // same versioning-mechanism as polkadot: use last digit for minor updates - spec_version: 9173, + spec_version: 9174, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/litmus/src/lib.rs b/runtime/litmus/src/lib.rs index 5a11451966..a9e4b9d472 100644 --- a/runtime/litmus/src/lib.rs +++ b/runtime/litmus/src/lib.rs @@ -150,7 +150,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("litmus-parachain"), authoring_version: 1, // same versioning-mechanism as polkadot: use last digit for minor updates - spec_version: 9173, + spec_version: 9174, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/runtime/rococo/src/lib.rs b/runtime/rococo/src/lib.rs index e9b4a74d2b..608107e2ad 100644 --- a/runtime/rococo/src/lib.rs +++ b/runtime/rococo/src/lib.rs @@ -245,7 +245,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { impl_name: create_runtime_str!("rococo-parachain"), authoring_version: 1, // same versioning-mechanism as polkadot: use last digit for minor updates - spec_version: 9173, + spec_version: 9174, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, diff --git a/tee-worker/app-libs/sgx-runtime/src/lib.rs b/tee-worker/app-libs/sgx-runtime/src/lib.rs index 8130ab5e44..f6ffcf34c2 100644 --- a/tee-worker/app-libs/sgx-runtime/src/lib.rs +++ b/tee-worker/app-libs/sgx-runtime/src/lib.rs @@ -138,7 +138,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("node-template"), impl_name: create_runtime_str!("node-template"), authoring_version: 1, - spec_version: 103, + spec_version: 104, impl_version: 1, apis: RUNTIME_API_VERSIONS, transaction_version: 1, From a6b78ed68396280655271f9cd30e17535d54da81 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Fri, 16 Feb 2024 17:59:22 +0100 Subject: [PATCH 42/64] Remove sidechain feature and sidechain related code from bitacross (#2497) * remove sidechain feature * remove ProvideWorkerMode * clean ups * more cleanups * more cleanups * more cleanups * machete deps * remove sidechain CI clipply check * remove bitacross enclave sidechain CI clippy check --- .github/workflows/ci.yml | 6 - bitacross-worker/Cargo.lock | 786 ++------- bitacross-worker/Cargo.toml | 13 - .../app-libs/parentchain-interface/Cargo.toml | 4 - bitacross-worker/app-libs/stf/Cargo.toml | 4 - .../core/bc-relayer-registry/Cargo.toml | 37 +- .../core/bc-task-receiver/Cargo.toml | 25 +- .../bitacross/core/bc-task-sender/Cargo.toml | 2 - bitacross-worker/cli/Cargo.toml | 7 +- bitacross-worker/cli/src/lib.rs | 2 - .../enclave-api/ffi/src/lib.rs | 15 - .../enclave-api/src/direct_request.rs | 58 - .../enclave-api/src/enclave_base.rs | 25 +- .../core-primitives/enclave-api/src/lib.rs | 1 - .../enclave-api/src/sidechain.rs | 13 - .../enclave-metrics/Cargo.toml | 2 - .../enclave-metrics/src/lib.rs | 5 - .../core-primitives/ocall-api/src/lib.rs | 27 +- .../core-primitives/settings/Cargo.toml | 3 - .../core-primitives/settings/src/lib.rs | 9 - .../settings/src/worker_mode.rs | 47 - .../core-primitives/sgx/crypto/src/ecdsa.rs | 6 +- .../core-primitives/stf-executor/Cargo.toml | 8 - .../core-primitives/test/Cargo.toml | 3 - .../test/src/mock/onchain_mock.rs | 40 +- .../test/src/mock/sidechain_ocall_api_mock.rs | 49 +- .../top-pool-author/Cargo.toml | 1 - .../top-pool-author/src/api.rs | 3 - .../top-pool-author/src/author.rs | 17 +- .../top-pool-author/src/author_tests.rs | 1 - .../top-pool-author/src/test_fixtures.rs | 16 - .../core-primitives/top-pool/Cargo.toml | 2 - .../top-pool/src/basic_pool.rs | 5 - .../src/mocks/trusted_operation_pool_mock.rs | 4 +- .../core-primitives/top-pool/src/pool.rs | 10 +- .../top-pool/src/primitives.rs | 4 - .../core/direct-rpc-client/Cargo.toml | 3 - .../src/rpc_watch_extractor.rs | 1 - .../indirect-calls-executor/Cargo.toml | 4 - .../indirect-calls-executor/src/executor.rs | 6 +- bitacross-worker/core/rpc-client/Cargo.toml | 2 - bitacross-worker/core/rpc-client/src/mock.rs | 2 - bitacross-worker/core/rpc-server/Cargo.toml | 30 - bitacross-worker/core/rpc-server/src/lib.rs | 81 - bitacross-worker/core/rpc-server/src/mock.rs | 75 - bitacross-worker/core/rpc-server/src/tests.rs | 56 - bitacross-worker/enclave-runtime/Cargo.lock | 299 +--- bitacross-worker/enclave-runtime/Cargo.toml | 12 +- bitacross-worker/enclave-runtime/Enclave.edl | 31 +- .../enclave-runtime/src/attestation.rs | 12 +- bitacross-worker/enclave-runtime/src/error.rs | 1 - .../src/initialization/global_components.rs | 72 - .../enclave-runtime/src/initialization/mod.rs | 106 +- .../src/initialization/parentchain/common.rs | 65 +- .../parentchain/integritee_parachain.rs | 22 +- .../parentchain/integritee_solochain.rs | 22 +- .../src/initialization/parentchain/mod.rs | 21 +- .../parentchain/target_a_parachain.rs | 25 +- .../parentchain/target_a_solochain.rs | 25 +- .../parentchain/target_b_parachain.rs | 25 +- .../parentchain/target_b_solochain.rs | 25 +- bitacross-worker/enclave-runtime/src/lib.rs | 91 +- .../enclave-runtime/src/ocall/ffi.rs | 30 - .../enclave-runtime/src/ocall/mod.rs | 1 - .../src/ocall/sidechain_ocall.rs | 139 -- .../src/rpc/worker_api_direct.rs | 184 +- .../src/test/direct_rpc_tests.rs | 9 +- .../src/test/mocks/enclave_rpc_ocall_mock.rs | 40 - .../enclave-runtime/src/test/mocks/mod.rs | 3 - .../src/test/mocks/peer_updater_mock.rs | 24 - .../test/mocks/propose_to_import_call_mock.rs | 137 -- .../enclave-runtime/src/test/mocks/types.rs | 23 +- .../enclave-runtime/src/test/mod.rs | 2 - .../src/test/sidechain_aura_tests.rs | 287 --- .../src/test/sidechain_event_tests.rs | 190 -- .../enclave-runtime/src/test/tests_main.rs | 178 +- .../src/test/top_pool_tests.rs | 20 +- .../enclave-runtime/src/tls_ra/tests.rs | 13 +- .../src/tls_ra/tls_ra_server.rs | 11 +- .../enclave-runtime/src/top_pool_execution.rs | 414 ----- bitacross-worker/enclave-runtime/src/utils.rs | 103 +- .../litentry/core/direct-call/Cargo.toml | 2 - .../direct-call/src/handler/sign_bitcoin.rs | 4 +- .../direct-call/src/handler/sign_ethereum.rs | 4 +- .../litentry/primitives/Cargo.toml | 15 - bitacross-worker/service/Cargo.toml | 14 - bitacross-worker/service/src/cli.yml | 11 - bitacross-worker/service/src/config.rs | 15 - .../service/src/initialized_service.rs | 79 +- bitacross-worker/service/src/main.rs | 2 - bitacross-worker/service/src/main_impl.rs | 138 +- .../service/src/ocall_bridge/bridge_api.rs | 28 - .../src/ocall_bridge/component_factory.rs | 69 +- .../ffi/fetch_sidechain_blocks_from_peer.rs | 193 -- .../service/src/ocall_bridge/ffi/get_peers.rs | 37 - .../service/src/ocall_bridge/ffi/mod.rs | 4 - .../ffi/propose_sidechain_blocks.rs | 50 - .../ffi/store_sidechain_blocks.rs | 50 - .../service/src/ocall_bridge/mod.rs | 4 - .../src/ocall_bridge/sidechain_ocall.rs | 282 --- .../src/ocall_bridge/test/mocks/mod.rs | 19 - .../test/mocks/sidechain_bridge_mock.rs | 54 - .../service/src/ocall_bridge/test/mod.rs | 19 - .../service/src/prometheus_metrics.rs | 10 - bitacross-worker/service/src/setup.rs | 13 +- .../service/src/sidechain_setup.rs | 129 -- .../service/src/sync_block_broadcaster.rs | 57 - bitacross-worker/service/src/sync_state.rs | 30 +- bitacross-worker/service/src/tests/commons.rs | 2 - .../src/tests/mocks/broadcast_blocks_mock.rs | 28 - .../src/tests/mocks/direct_request_mock.rs | 26 - .../src/tests/mocks/enclave_api_mock.rs | 10 +- .../mocks/initialization_handler_mock.rs | 2 - .../service/src/tests/mocks/mod.rs | 2 - bitacross-worker/service/src/worker.rs | 141 -- .../sidechain/block-composer/Cargo.toml | 64 - .../block-composer/src/block_composer.rs | 185 -- .../sidechain/block-composer/src/error.rs | 59 - .../sidechain/block-composer/src/lib.rs | 36 - .../sidechain/block-verification/Cargo.toml | 52 - .../sidechain/block-verification/src/error.rs | 46 - .../sidechain/block-verification/src/lib.rs | 492 ------ .../sidechain/block-verification/src/slot.rs | 45 - .../sidechain/consensus/aura/Cargo.toml | 106 -- .../consensus/aura/src/block_importer.rs | 367 ---- .../sidechain/consensus/aura/src/lib.rs | 776 --------- .../consensus/aura/src/proposer_factory.rs | 131 -- .../consensus/aura/src/slot_proposer.rs | 206 --- .../aura/src/test/block_importer_tests.rs | 318 ---- .../consensus/aura/src/test/fixtures/mod.rs | 27 - .../consensus/aura/src/test/fixtures/types.rs | 48 - .../aura/src/test/mocks/environment_mock.rs | 58 - .../consensus/aura/src/test/mocks/mod.rs | 20 - .../aura/src/test/mocks/peer_updater_mock.rs | 7 - .../aura/src/test/mocks/proposer_mock.rs | 73 - .../sidechain/consensus/aura/src/test/mod.rs | 20 - .../sidechain/consensus/aura/src/verifier.rs | 90 - .../sidechain/consensus/common/Cargo.toml | 85 - .../consensus/common/src/block_import.rs | 201 --- .../src/block_import_confirmation_handler.rs | 130 -- .../common/src/block_import_queue_worker.rs | 120 -- .../common/src/block_production_suspension.rs | 112 -- .../sidechain/consensus/common/src/error.rs | 99 -- .../consensus/common/src/header_db.rs | 44 - .../common/src/is_descendant_of_builder.rs | 133 -- .../sidechain/consensus/common/src/lib.rs | 114 -- .../consensus/common/src/peer_block_sync.rs | 320 ---- .../mocks/block_import_queue_worker_mock.rs | 263 --- .../src/test/mocks/block_importer_mock.rs | 168 -- .../test/mocks/confirm_block_import_mock.rs | 29 - .../consensus/common/src/test/mocks/mod.rs | 21 - .../common/src/test/mocks/verifier_mock.rs | 62 - .../consensus/common/src/test/mod.rs | 18 - .../sidechain/consensus/slots/Cargo.toml | 78 - .../sidechain/consensus/slots/src/lib.rs | 575 ------ .../sidechain/consensus/slots/src/mocks.rs | 149 -- .../slots/src/per_shard_slot_worker_tests.rs | 100 -- .../consensus/slots/src/slot_stream.rs | 116 -- .../sidechain/consensus/slots/src/slots.rs | 421 ----- .../sidechain/fork-tree/Cargo.toml | 27 - .../sidechain/fork-tree/src/lib.rs | 1552 ----------------- .../sidechain/peer-fetch/Cargo.toml | 37 - .../peer-fetch/src/block_fetch_client.rs | 141 -- .../peer-fetch/src/block_fetch_server.rs | 103 -- .../sidechain/peer-fetch/src/error.rs | 44 - .../sidechain/peer-fetch/src/lib.rs | 49 - .../src/mocks/fetch_blocks_from_peer_mock.rs | 61 - .../sidechain/peer-fetch/src/mocks/mod.rs | 19 - .../src/mocks/untrusted_peer_fetch_mock.rs | 35 - .../peer-fetch/src/untrusted_peer_fetch.rs | 61 - .../sidechain/primitives/Cargo.toml | 36 - .../sidechain/primitives/src/lib.rs | 21 - .../sidechain/primitives/src/traits/mod.rs | 176 -- .../sidechain/primitives/src/types/block.rs | 159 -- .../primitives/src/types/block_data.rs | 82 - .../sidechain/primitives/src/types/header.rs | 91 - .../sidechain/primitives/src/types/mod.rs | 22 - .../sidechain/rpc-handler/Cargo.toml | 62 - .../sidechain/rpc-handler/src/constants.rs | 24 - .../rpc-handler/src/direct_top_pool_api.rs | 359 ---- .../rpc-handler/src/import_block_api.rs | 126 -- .../sidechain/rpc-handler/src/lib.rs | 38 - .../sidechain/sidechain-crate/Cargo.toml | 36 - .../sidechain/sidechain-crate/src/lib.rs | 39 - bitacross-worker/sidechain/state/Cargo.toml | 56 - bitacross-worker/sidechain/state/src/error.rs | 31 - bitacross-worker/sidechain/state/src/impls.rs | 184 -- bitacross-worker/sidechain/state/src/lib.rs | 208 --- bitacross-worker/sidechain/storage/Cargo.toml | 34 - bitacross-worker/sidechain/storage/src/db.rs | 67 - .../sidechain/storage/src/error.rs | 34 - .../storage/src/fetch_blocks_mock.rs | 71 - .../sidechain/storage/src/interface.rs | 154 -- bitacross-worker/sidechain/storage/src/lib.rs | 70 - .../sidechain/storage/src/storage.rs | 1176 ------------- .../src/storage_tests_get_blocks_after.rs | 124 -- .../src/storage_tests_get_blocks_in_range.rs | 104 -- .../sidechain/storage/src/test_utils.rs | 96 - bitacross-worker/sidechain/test/Cargo.toml | 32 - bitacross-worker/sidechain/test/src/lib.rs | 29 - .../test/src/sidechain_block_builder.rs | 105 -- .../test/src/sidechain_block_data_builder.rs | 102 -- .../test/src/sidechain_header_builder.rs | 92 - .../sidechain/validateer-fetch/Cargo.toml | 35 - .../sidechain/validateer-fetch/src/error.rs | 27 - .../sidechain/validateer-fetch/src/lib.rs | 24 - .../validateer-fetch/src/validateer.rs | 80 - scripts/pre-commit.sh | 1 - 208 files changed, 384 insertions(+), 18261 deletions(-) delete mode 100644 bitacross-worker/core-primitives/enclave-api/src/direct_request.rs delete mode 100644 bitacross-worker/core-primitives/settings/src/worker_mode.rs delete mode 100644 bitacross-worker/core/rpc-server/Cargo.toml delete mode 100644 bitacross-worker/core/rpc-server/src/lib.rs delete mode 100644 bitacross-worker/core/rpc-server/src/mock.rs delete mode 100644 bitacross-worker/core/rpc-server/src/tests.rs delete mode 100644 bitacross-worker/enclave-runtime/src/ocall/sidechain_ocall.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/mocks/enclave_rpc_ocall_mock.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/mocks/propose_to_import_call_mock.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/sidechain_aura_tests.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/sidechain_event_tests.rs delete mode 100644 bitacross-worker/enclave-runtime/src/top_pool_execution.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/ffi/fetch_sidechain_blocks_from_peer.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/ffi/get_peers.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/ffi/propose_sidechain_blocks.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/ffi/store_sidechain_blocks.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/sidechain_ocall.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/test/mocks/mod.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/test/mocks/sidechain_bridge_mock.rs delete mode 100644 bitacross-worker/service/src/ocall_bridge/test/mod.rs delete mode 100644 bitacross-worker/service/src/sidechain_setup.rs delete mode 100644 bitacross-worker/service/src/sync_block_broadcaster.rs delete mode 100644 bitacross-worker/service/src/tests/mocks/broadcast_blocks_mock.rs delete mode 100644 bitacross-worker/service/src/tests/mocks/direct_request_mock.rs delete mode 100644 bitacross-worker/sidechain/block-composer/Cargo.toml delete mode 100644 bitacross-worker/sidechain/block-composer/src/block_composer.rs delete mode 100644 bitacross-worker/sidechain/block-composer/src/error.rs delete mode 100644 bitacross-worker/sidechain/block-composer/src/lib.rs delete mode 100644 bitacross-worker/sidechain/block-verification/Cargo.toml delete mode 100644 bitacross-worker/sidechain/block-verification/src/error.rs delete mode 100644 bitacross-worker/sidechain/block-verification/src/lib.rs delete mode 100644 bitacross-worker/sidechain/block-verification/src/slot.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/Cargo.toml delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/block_importer.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/lib.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/proposer_factory.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/slot_proposer.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/fixtures/types.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/mocks/environment_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/mocks/mod.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/mocks/peer_updater_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/mocks/proposer_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/test/mod.rs delete mode 100644 bitacross-worker/sidechain/consensus/aura/src/verifier.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/Cargo.toml delete mode 100644 bitacross-worker/sidechain/consensus/common/src/block_import.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/block_import_queue_worker.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/block_production_suspension.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/error.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/header_db.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/is_descendant_of_builder.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/lib.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/peer_block_sync.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mocks/block_import_queue_worker_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mocks/block_importer_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mocks/confirm_block_import_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mocks/mod.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mocks/verifier_mock.rs delete mode 100644 bitacross-worker/sidechain/consensus/common/src/test/mod.rs delete mode 100644 bitacross-worker/sidechain/consensus/slots/Cargo.toml delete mode 100644 bitacross-worker/sidechain/consensus/slots/src/lib.rs delete mode 100644 bitacross-worker/sidechain/consensus/slots/src/mocks.rs delete mode 100644 bitacross-worker/sidechain/consensus/slots/src/per_shard_slot_worker_tests.rs delete mode 100644 bitacross-worker/sidechain/consensus/slots/src/slot_stream.rs delete mode 100644 bitacross-worker/sidechain/consensus/slots/src/slots.rs delete mode 100644 bitacross-worker/sidechain/fork-tree/Cargo.toml delete mode 100644 bitacross-worker/sidechain/fork-tree/src/lib.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/Cargo.toml delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/block_fetch_client.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/block_fetch_server.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/error.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/lib.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/mocks/fetch_blocks_from_peer_mock.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/mocks/mod.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/mocks/untrusted_peer_fetch_mock.rs delete mode 100644 bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs delete mode 100644 bitacross-worker/sidechain/primitives/Cargo.toml delete mode 100644 bitacross-worker/sidechain/primitives/src/lib.rs delete mode 100644 bitacross-worker/sidechain/primitives/src/traits/mod.rs delete mode 100644 bitacross-worker/sidechain/primitives/src/types/block.rs delete mode 100644 bitacross-worker/sidechain/primitives/src/types/block_data.rs delete mode 100644 bitacross-worker/sidechain/primitives/src/types/header.rs delete mode 100644 bitacross-worker/sidechain/primitives/src/types/mod.rs delete mode 100644 bitacross-worker/sidechain/rpc-handler/Cargo.toml delete mode 100644 bitacross-worker/sidechain/rpc-handler/src/constants.rs delete mode 100644 bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs delete mode 100644 bitacross-worker/sidechain/rpc-handler/src/import_block_api.rs delete mode 100644 bitacross-worker/sidechain/rpc-handler/src/lib.rs delete mode 100644 bitacross-worker/sidechain/sidechain-crate/Cargo.toml delete mode 100644 bitacross-worker/sidechain/sidechain-crate/src/lib.rs delete mode 100644 bitacross-worker/sidechain/state/Cargo.toml delete mode 100644 bitacross-worker/sidechain/state/src/error.rs delete mode 100644 bitacross-worker/sidechain/state/src/impls.rs delete mode 100644 bitacross-worker/sidechain/state/src/lib.rs delete mode 100644 bitacross-worker/sidechain/storage/Cargo.toml delete mode 100644 bitacross-worker/sidechain/storage/src/db.rs delete mode 100644 bitacross-worker/sidechain/storage/src/error.rs delete mode 100644 bitacross-worker/sidechain/storage/src/fetch_blocks_mock.rs delete mode 100644 bitacross-worker/sidechain/storage/src/interface.rs delete mode 100644 bitacross-worker/sidechain/storage/src/lib.rs delete mode 100644 bitacross-worker/sidechain/storage/src/storage.rs delete mode 100644 bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_after.rs delete mode 100644 bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_in_range.rs delete mode 100644 bitacross-worker/sidechain/storage/src/test_utils.rs delete mode 100644 bitacross-worker/sidechain/test/Cargo.toml delete mode 100644 bitacross-worker/sidechain/test/src/lib.rs delete mode 100644 bitacross-worker/sidechain/test/src/sidechain_block_builder.rs delete mode 100644 bitacross-worker/sidechain/test/src/sidechain_block_data_builder.rs delete mode 100644 bitacross-worker/sidechain/test/src/sidechain_header_builder.rs delete mode 100644 bitacross-worker/sidechain/validateer-fetch/Cargo.toml delete mode 100644 bitacross-worker/sidechain/validateer-fetch/src/error.rs delete mode 100644 bitacross-worker/sidechain/validateer-fetch/src/lib.rs delete mode 100644 bitacross-worker/sidechain/validateer-fetch/src/validateer.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0453c4e7fb..5fa48e1ae8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -327,9 +327,6 @@ jobs: echo "::group::cargo clippy all" cargo clippy --release -- -D warnings echo "::endgroup::" - echo "::group::cargo clippy sidechain" - cargo clippy --release --features sidechain -- -D warnings - echo "::endgroup::" echo "::group::cargo clippy offchain-worker" cargo clean --profile release cargo clippy --release --features offchain-worker -- -D warnings @@ -352,9 +349,6 @@ jobs: echo "::group::cargo clippy all" cargo clippy --release -- -D warnings echo "::endgroup::" - echo "::group::cargo clippy sidechain" - cargo clippy --release --features sidechain -- -D warnings - echo "::endgroup::" echo "::group::cargo clippy offchain-worker" cargo clean --profile release cargo clippy --release --features offchain-worker -- -D warnings diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 6fb011ab84..4c7f382cc3 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -35,8 +35,8 @@ dependencies = [ "hex 0.4.3", "log 0.4.20", "parity-scale-codec", - "scale-bits 0.4.0", - "scale-decode 0.8.0", + "scale-bits", + "scale-decode", "scale-encode", "scale-info", "serde 1.0.193", @@ -63,7 +63,7 @@ dependencies = [ "serde_json 1.0.103", "sp-application-crypto", "sp-core", - "sp-core-hashing 5.0.0", + "sp-core-hashing", "sp-runtime", "sp-runtime-interface", "sp-staking", @@ -594,34 +594,15 @@ checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" name = "bc-relayer-registry" version = "0.1.0" dependencies = [ - "base64 0.13.0 (git+https://github.com/mesalock-linux/rust-base64-sgx?rev=sgx_1.1.3)", "base64 0.13.1", - "bitcoin", - "core-primitives", - "hex 0.4.3", "itp-settings", - "itp-sgx-crypto", "itp-sgx-io", - "itp-utils", "lazy_static", - "litentry-hex-utils", "litentry-primitives", "log 0.4.20", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "parity-scale-codec", - "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", - "rand 0.7.3 (git+https://github.com/mesalock-linux/rand-sgx?tag=sgx_1.1.3)", - "ring 0.16.20", - "scale-info", - "secp256k1 0.28.0", - "serde 1.0.193", "sgx_tstd", - "sp-core", - "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", - "sp-std 5.0.0", - "strum 0.26.1", - "strum_macros 0.26.1", + "sp-std", "thiserror 1.0.44", "thiserror 1.0.9", ] @@ -633,29 +614,19 @@ dependencies = [ "bc-relayer-registry", "bc-task-sender", "frame-support", - "futures 0.3.28", "futures 0.3.8", "hex 0.4.0", - "ita-sgx-runtime", "ita-stf", - "itp-enclave-metrics", - "itp-extrinsics-factory", - "itp-node-api", "itp-ocall-api", "itp-sgx-crypto", "itp-sgx-externalities", "itp-stf-executor", "itp-stf-state-handler", - "itp-storage", - "itp-top-pool-author", - "itp-types", - "itp-utils", "lc-direct-call", "litentry-primitives", "log 0.4.20", "parity-scale-codec", "sgx_tstd", - "sp-core", "thiserror 1.0.44", "thiserror 1.0.9", "threadpool 1.8.0", @@ -670,7 +641,6 @@ dependencies = [ "futures 0.3.8", "lazy_static", "litentry-primitives", - "log 0.4.20", "parity-scale-codec", "sgx_tstd", ] @@ -743,17 +713,14 @@ dependencies = [ "chrono 0.4.26", "clap 4.1.0", "env_logger 0.9.3", - "frame-metadata", "hdrhistogram", "hex 0.4.3", - "ita-sgx-runtime", "ita-stf", "itc-rpc-client", "itp-node-api", "itp-rpc", "itp-sgx-crypto", "itp-stf-primitives", - "itp-time-utils", "itp-types", "itp-utils", "lc-direct-call", @@ -761,19 +728,18 @@ dependencies = [ "log 0.4.20", "pallet-balances", "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", + "pallet-teerex", "parity-scale-codec", "rand 0.8.5", "rayon", "regex 1.9.5", "reqwest", "rococo-parachain-runtime", - "scale-value", "serde 1.0.193", "serde_json 1.0.103", "sgx_crypto_helper", "sp-application-crypto", "sp-core", - "sp-core-hashing 6.0.0", "sp-keyring", "sp-keystore", "sp-runtime", @@ -798,12 +764,10 @@ dependencies = [ "futures 0.3.28", "hex 0.4.3", "ipfs-api", - "ita-stf", "itc-parentchain", "itc-parentchain-test", "itc-rest-client", "itc-rpc-client", - "itc-rpc-server", "itp-api-client-types", "itp-enclave-api", "itp-enclave-metrics", @@ -812,24 +776,15 @@ dependencies = [ "itp-storage", "itp-types", "itp-utils", - "its-consensus-slots", - "its-peer-fetch", - "its-primitives", - "its-rpc-handler", - "its-storage", - "its-test", "jsonrpsee 0.2.0", "lazy_static", - "litentry-hex-utils", "litentry-macros", "litentry-primitives", "log 0.4.20", "mockall", - "pallet-balances", "parity-scale-codec", "parking_lot 0.12.1", "parse_duration", - "primitive-types", "prometheus", "regex 1.9.5", "rococo-parachain-runtime", @@ -1509,7 +1464,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "strum 0.26.1", "strum_macros 0.26.1", ] @@ -1827,7 +1782,7 @@ dependencies = [ "sp-application-crypto", "sp-consensus-aura", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -1843,7 +1798,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", ] @@ -1870,7 +1825,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-trie", "sp-version", "xcm", @@ -1899,7 +1854,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", ] @@ -1918,7 +1873,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -1935,7 +1890,7 @@ dependencies = [ "scale-info", "sp-api", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-trie", "xcm", ] @@ -1957,7 +1912,7 @@ dependencies = [ "sp-inherents", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-storage", "sp-trie", "tracing", @@ -1972,7 +1927,7 @@ dependencies = [ "futures 0.3.28", "parity-scale-codec", "sp-inherents", - "sp-std 5.0.0", + "sp-std", "sp-timestamp", ] @@ -1988,7 +1943,7 @@ dependencies = [ "polkadot-runtime-common", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-builder", "xcm-executor", @@ -2022,7 +1977,7 @@ dependencies = [ "polkadot-primitives", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3194,14 +3149,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" -[[package]] -name = "fork-tree" -version = "3.0.0" -dependencies = [ - "parity-scale-codec", - "sgx_tstd", -] - [[package]] name = "fork-tree" version = "3.0.0" @@ -3234,7 +3181,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3253,7 +3200,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-runtime-interface", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3265,7 +3212,7 @@ dependencies = [ "parity-scale-codec", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3279,7 +3226,7 @@ dependencies = [ "frame-support", "num_enum 0.6.1", "parity-scale-codec", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3294,7 +3241,7 @@ dependencies = [ "serde 1.0.193", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3309,7 +3256,7 @@ dependencies = [ "serde 1.0.193", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3326,7 +3273,7 @@ dependencies = [ "sp-core", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3376,7 +3323,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-runtime-interface", - "sp-std 5.0.0", + "sp-std", "sp-storage", "static_assertions", ] @@ -3406,7 +3353,7 @@ dependencies = [ "sp-core", "sp-npos-elections", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3421,7 +3368,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-tracing", ] @@ -3464,7 +3411,7 @@ dependencies = [ "sp-runtime", "sp-staking", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-tracing", "sp-weights", "tt-call", @@ -3521,7 +3468,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-version", "sp-weights", ] @@ -3538,7 +3485,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -3559,7 +3506,7 @@ dependencies = [ "parity-scale-codec", "sp-api", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -4698,7 +4645,6 @@ dependencies = [ "itp-test", "itp-top-pool-author", "itp-types", - "itp-utils", "lc-scheduled-enclave", "litentry-hex-utils", "litentry-primitives", @@ -4707,7 +4653,6 @@ dependencies = [ "sgx_tstd", "sp-core", "sp-runtime", - "sp-std 5.0.0", ] [[package]] @@ -4729,7 +4674,7 @@ dependencies = [ "sp-api", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-version", ] @@ -4739,13 +4684,11 @@ version = "0.9.0" dependencies = [ "frame-support", "frame-system", - "hex 0.4.3", "hex-literal", "ita-sgx-runtime", "itp-hashing", "itp-node-api", "itp-node-api-metadata", - "itp-node-api-metadata-provider", "itp-sgx-externalities", "itp-stf-interface", "itp-stf-primitives", @@ -4766,7 +4709,7 @@ dependencies = [ "sp-io 7.0.0", "sp-keyring", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -4776,7 +4719,6 @@ dependencies = [ "itp-rpc", "itp-types", "itp-utils", - "litentry-primitives", "log 0.4.20", "parity-scale-codec", "rustls 0.19.0 (git+https://github.com/mesalock-linux/rustls?tag=sgx_1.1.3)", @@ -4886,7 +4828,6 @@ version = "0.9.0" dependencies = [ "binary-merkle-tree", "bs58", - "core-primitives", "env_logger 0.9.3", "futures 0.3.28", "futures 0.3.8", @@ -4900,7 +4841,6 @@ dependencies = [ "itp-test", "itp-top-pool-author", "itp-types", - "itp-utils", "lc-scheduled-enclave", "litentry-primitives", "log 0.4.20", @@ -4909,7 +4849,6 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "sp-std 5.0.0", "thiserror 1.0.44", "thiserror 1.0.9", ] @@ -4993,7 +4932,6 @@ dependencies = [ "itp-stf-primitives", "itp-types", "itp-utils", - "litentry-primitives", "log 0.4.20", "openssl", "parity-scale-codec", @@ -5001,33 +4939,11 @@ dependencies = [ "rustls 0.19.1", "serde_json 1.0.103", "sgx_crypto_helper", - "sp-core", "thiserror 1.0.44", "url 2.4.0", "ws", ] -[[package]] -name = "itc-rpc-server" -version = "0.9.0" -dependencies = [ - "anyhow", - "env_logger 0.10.0", - "itp-enclave-api", - "itp-rpc", - "itp-utils", - "its-peer-fetch", - "its-primitives", - "its-rpc-handler", - "its-storage", - "its-test", - "jsonrpsee 0.2.0", - "log 0.4.20", - "parity-scale-codec", - "sp-core", - "tokio", -] - [[package]] name = "itc-tls-websocket-server" version = "0.9.0" @@ -5194,7 +5110,6 @@ version = "0.9.0" dependencies = [ "parity-scale-codec", "sgx_tstd", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed?tag=v0.5.9)", ] [[package]] @@ -5301,7 +5216,7 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -5328,9 +5243,6 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" -dependencies = [ - "litentry-primitives", -] [[package]] name = "itp-sgx-crypto" @@ -5401,7 +5313,6 @@ version = "0.9.0" dependencies = [ "hex 0.4.3", "itc-parentchain-test", - "itp-enclave-metrics", "itp-node-api", "itp-ocall-api", "itp-sgx-crypto", @@ -5415,7 +5326,6 @@ dependencies = [ "itp-top-pool", "itp-top-pool-author", "itp-types", - "litentry-primitives", "log 0.4.20", "parity-scale-codec", "sgx_tstd", @@ -5447,7 +5357,7 @@ dependencies = [ "parity-scale-codec", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -5500,7 +5410,7 @@ dependencies = [ "sp-core", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-trie", "thiserror 1.0.44", "thiserror 1.0.9", @@ -5520,7 +5430,6 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-time-utils", "itp-types", "jsonrpc-core 18.0.0 (git+https://github.com/scs/jsonrpc?branch=no_std_v18)", "lc-teebag-storage", @@ -5533,7 +5442,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -5555,7 +5464,6 @@ dependencies = [ "itp-stf-primitives", "itp-test", "itp-types", - "its-primitives", "jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)", "jsonrpc-core 18.0.0 (git+https://github.com/scs/jsonrpc?branch=no_std_v18)", "linked-hash-map 0.5.2", @@ -5613,7 +5521,7 @@ dependencies = [ "parity-scale-codec", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "substrate-api-client", ] @@ -5626,292 +5534,6 @@ dependencies = [ "parity-scale-codec", ] -[[package]] -name = "its-block-composer" -version = "0.9.0" -dependencies = [ - "itp-node-api", - "itp-settings", - "itp-sgx-crypto", - "itp-sgx-externalities", - "itp-stf-executor", - "itp-stf-primitives", - "itp-time-utils", - "itp-top-pool-author", - "itp-types", - "its-primitives", - "its-state", - "log 0.4.20", - "parity-scale-codec", - "sgx_tstd", - "sgx_types", - "sp-core", - "sp-runtime", - "thiserror 1.0.44", - "thiserror 1.0.9", -] - -[[package]] -name = "its-block-verification" -version = "0.9.0" -dependencies = [ - "frame-support", - "itc-parentchain-test", - "itp-types", - "itp-utils", - "its-primitives", - "its-test", - "log 0.4.20", - "sgx_tstd", - "sp-consensus-slots", - "sp-core", - "sp-keyring", - "sp-runtime", - "thiserror 1.0.44", - "thiserror 1.0.9", -] - -[[package]] -name = "its-consensus-aura" -version = "0.9.0" -dependencies = [ - "env_logger 0.9.3", - "finality-grandpa", - "ita-stf", - "itc-parentchain-block-import-dispatcher", - "itc-parentchain-test", - "itc-peer-top-broadcaster", - "itp-enclave-metrics", - "itp-ocall-api", - "itp-settings", - "itp-sgx-crypto", - "itp-sgx-externalities", - "itp-stf-executor", - "itp-stf-primitives", - "itp-stf-state-handler", - "itp-storage", - "itp-test", - "itp-time-utils", - "itp-top-pool-author", - "itp-types", - "itp-utils", - "its-block-composer", - "its-block-verification", - "its-consensus-common", - "its-consensus-slots", - "its-primitives", - "its-state", - "its-test", - "its-validateer-fetch", - "lc-scheduled-enclave", - "litentry-hex-utils", - "log 0.4.20", - "parity-scale-codec", - "sgx_tstd", - "sp-core", - "sp-keyring", - "sp-runtime", -] - -[[package]] -name = "its-consensus-common" -version = "0.9.0" -dependencies = [ - "fork-tree 3.0.0", - "itc-parentchain-light-client", - "itc-parentchain-test", - "itp-enclave-metrics", - "itp-extrinsics-factory", - "itp-import-queue", - "itp-node-api-metadata", - "itp-node-api-metadata-provider", - "itp-ocall-api", - "itp-settings", - "itp-sgx-crypto", - "itp-sgx-externalities", - "itp-test", - "itp-types", - "its-block-verification", - "its-primitives", - "its-state", - "its-test", - "log 0.4.20", - "parity-scale-codec", - "sgx_tstd", - "sgx_types", - "sp-core", - "sp-runtime", - "thiserror 1.0.44", - "thiserror 1.0.9", -] - -[[package]] -name = "its-consensus-slots" -version = "0.9.0" -dependencies = [ - "derive_more", - "futures-timer", - "hex 0.4.3", - "itc-parentchain-test", - "itp-settings", - "itp-sgx-externalities", - "itp-stf-state-handler", - "itp-test", - "itp-time-utils", - "itp-types", - "its-block-verification", - "its-consensus-common", - "its-primitives", - "its-state", - "its-test", - "lazy_static", - "lc-scheduled-enclave", - "log 0.4.20", - "parity-scale-codec", - "sgx_tstd", - "sp-consensus-slots", - "sp-keyring", - "sp-runtime", - "tokio", -] - -[[package]] -name = "its-peer-fetch" -version = "0.9.0" -dependencies = [ - "anyhow", - "async-trait", - "itc-rpc-client", - "itp-node-api", - "itp-test", - "itp-types", - "its-primitives", - "its-rpc-handler", - "its-storage", - "its-test", - "jsonrpsee 0.2.0", - "log 0.4.20", - "serde 1.0.193", - "serde_json 1.0.103", - "thiserror 1.0.44", - "tokio", -] - -[[package]] -name = "its-primitives" -version = "0.1.0" -dependencies = [ - "itp-types", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-runtime", - "sp-std 5.0.0", -] - -[[package]] -name = "its-rpc-handler" -version = "0.9.0" -dependencies = [ - "bc-task-sender", - "futures 0.3.28", - "futures 0.3.8", - "itp-rpc", - "itp-stf-primitives", - "itp-top-pool-author", - "itp-types", - "itp-utils", - "its-primitives", - "jsonrpc-core 18.0.0 (registry+https://github.com/rust-lang/crates.io-index)", - "jsonrpc-core 18.0.0 (git+https://github.com/scs/jsonrpc?branch=no_std_v18)", - "litentry-primitives", - "log 0.4.20", - "parity-scale-codec", - "rust-base58 0.0.4 (registry+https://github.com/rust-lang/crates.io-index)", - "rust-base58 0.0.4 (git+https://github.com/mesalock-linux/rust-base58-sgx?rev=sgx_1.1.3)", - "sgx_tstd", - "sp-core", -] - -[[package]] -name = "its-sidechain" -version = "0.9.0" -dependencies = [ - "its-block-composer", - "its-consensus-aura", - "its-consensus-common", - "its-consensus-slots", - "its-primitives", - "its-rpc-handler", - "its-state", - "its-validateer-fetch", -] - -[[package]] -name = "its-state" -version = "0.9.0" -dependencies = [ - "frame-support", - "itp-sgx-externalities", - "itp-storage", - "its-primitives", - "log 0.4.20", - "parity-scale-codec", - "sgx_tstd", - "sp-core", - "sp-io 7.0.0", - "sp-runtime", - "thiserror 1.0.44", - "thiserror 1.0.9", -] - -[[package]] -name = "its-storage" -version = "0.9.0" -dependencies = [ - "itp-settings", - "itp-time-utils", - "itp-types", - "its-primitives", - "its-test", - "log 0.4.20", - "mockall", - "parity-scale-codec", - "parking_lot 0.12.1", - "rocksdb", - "serde 1.0.193", - "sp-core", - "temp-dir", - "thiserror 1.0.44", -] - -[[package]] -name = "its-test" -version = "0.9.0" -dependencies = [ - "itp-types", - "its-primitives", - "sgx_tstd", - "sp-core", -] - -[[package]] -name = "its-validateer-fetch" -version = "0.9.0" -dependencies = [ - "derive_more", - "itc-parentchain-test", - "itp-ocall-api", - "itp-test", - "itp-types", - "lc-teebag-storage", - "parity-scale-codec", - "sp-core", - "sp-runtime", - "sp-std 5.0.0", -] - [[package]] name = "jobserver" version = "0.1.26" @@ -5975,10 +5597,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "316a89048d2ea5530ab5502aa31e1128f6429b524a37e4c0bc54903bcdf3d342" dependencies = [ "jsonrpsee-http-client", - "jsonrpsee-http-server", "jsonrpsee-proc-macros 0.2.0", "jsonrpsee-types 0.2.0", - "jsonrpsee-utils", "jsonrpsee-ws-client", "jsonrpsee-ws-server", ] @@ -6041,28 +5661,6 @@ dependencies = [ "url 2.4.0", ] -[[package]] -name = "jsonrpsee-http-server" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22372378f63f7d16de453e786afc740fca5ee80bd260be024a616b6ac2cefe5" -dependencies = [ - "futures-channel 0.3.28", - "futures-util 0.3.28", - "globset", - "hyper", - "jsonrpsee-types 0.2.0", - "jsonrpsee-utils", - "lazy_static", - "log 0.4.20", - "serde 1.0.193", - "serde_json 1.0.103", - "socket2 0.4.9", - "thiserror 1.0.44", - "tokio", - "unicase 2.6.0 (registry+https://github.com/rust-lang/crates.io-index)", -] - [[package]] name = "jsonrpsee-proc-macros" version = "0.2.0" @@ -6303,7 +5901,6 @@ dependencies = [ "sgx_tstd", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", ] [[package]] @@ -6317,7 +5914,7 @@ dependencies = [ "log 0.4.20", "parity-scale-codec", "sgx_tstd", - "sp-std 5.0.0", + "sp-std", "thiserror 1.0.44", "thiserror 1.0.9", ] @@ -6328,7 +5925,7 @@ version = "0.1.0" dependencies = [ "itp-storage", "itp-types", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -6960,18 +6557,13 @@ version = "0.9.12" name = "litentry-primitives" version = "0.1.0" dependencies = [ - "base64 0.13.0 (git+https://github.com/mesalock-linux/rust-base64-sgx?rev=sgx_1.1.3)", "base64 0.13.1", "bitcoin", "core-primitives", "hex 0.4.3", "itp-sgx-crypto", - "itp-sgx-io", - "itp-utils", - "lazy_static", "litentry-hex-utils", "log 0.4.20", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "pallet-teebag", "parity-scale-codec", "rand 0.7.3 (registry+https://github.com/rust-lang/crates.io-index)", @@ -6984,11 +6576,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", - "strum 0.26.1", - "strum_macros 0.26.1", - "thiserror 1.0.44", - "thiserror 1.0.9", + "sp-std", ] [[package]] @@ -7394,7 +6982,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -7407,7 +6995,7 @@ dependencies = [ "parity-scale-codec", "sp-externalities", "sp-runtime-interface", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -7426,7 +7014,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -7440,7 +7028,7 @@ dependencies = [ "sp-api", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8235,7 +7823,7 @@ dependencies = [ "serde 1.0.193", "sp-arithmetic", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8253,7 +7841,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", ] @@ -8268,7 +7856,7 @@ dependencies = [ "serde 1.0.193", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8280,7 +7868,7 @@ dependencies = [ "orml-traits", "parity-scale-codec", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -8301,7 +7889,7 @@ dependencies = [ "serde 1.0.193", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -8356,7 +7944,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8371,7 +7959,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", ] @@ -8387,7 +7975,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8403,7 +7991,7 @@ dependencies = [ "sp-application-crypto", "sp-consensus-aura", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8419,7 +8007,7 @@ dependencies = [ "sp-application-crypto", "sp-authority-discovery", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8433,7 +8021,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8457,7 +8045,7 @@ dependencies = [ "sp-runtime", "sp-session", "sp-staking", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8472,7 +8060,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8487,7 +8075,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8505,7 +8093,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8522,7 +8110,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8540,7 +8128,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8557,7 +8145,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8575,7 +8163,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8589,7 +8177,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8611,7 +8199,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-npos-elections", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "strum 0.24.1", ] @@ -8648,7 +8236,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8672,7 +8260,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8697,7 +8285,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8736,7 +8324,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8784,7 +8372,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8816,7 +8404,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8834,7 +8422,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-staking", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8850,7 +8438,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8866,7 +8454,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8882,7 +8470,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8899,7 +8487,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8915,7 +8503,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8935,8 +8523,8 @@ dependencies = [ "serde 1.0.193", "sp-runtime", "sp-staking", - "sp-std 5.0.0", - "substrate-fixed 0.5.9 (git+https://github.com/encointer/substrate-fixed)", + "sp-std", + "substrate-fixed", ] [[package]] @@ -8966,7 +8554,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8981,7 +8569,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -8997,7 +8585,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-weights", ] @@ -9018,7 +8606,7 @@ dependencies = [ "sp-runtime", "sp-session", "sp-staking", - "sp-std 5.0.0", + "sp-std", "sp-trie", ] @@ -9038,7 +8626,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "teerex-primitives", ] @@ -9061,7 +8649,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-staking", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9084,7 +8672,7 @@ dependencies = [ "scale-info", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9109,7 +8697,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "x509-cert", ] @@ -9128,7 +8716,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "teerex-primitives", ] @@ -9146,7 +8734,7 @@ dependencies = [ "sp-inherents", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-timestamp", ] @@ -9166,7 +8754,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9182,7 +8770,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9211,7 +8799,7 @@ dependencies = [ "scale-info", "serde 1.0.193", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9227,7 +8815,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9243,7 +8831,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9258,7 +8846,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9277,7 +8865,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -9669,7 +9257,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9813,7 +9401,7 @@ dependencies = [ "serde 1.0.193", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9839,7 +9427,7 @@ dependencies = [ "sp-keystore", "sp-runtime", "sp-staking", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -9881,7 +9469,7 @@ dependencies = [ "sp-runtime", "sp-session", "sp-staking", - "sp-std 5.0.0", + "sp-std", "static_assertions", "xcm", ] @@ -9894,7 +9482,7 @@ dependencies = [ "bs58", "parity-scale-codec", "polkadot-primitives", - "sp-std 5.0.0", + "sp-std", "sp-tracing", ] @@ -9934,7 +9522,7 @@ dependencies = [ "sp-runtime", "sp-session", "sp-staking", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -10046,7 +9634,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", ] @@ -10992,7 +10580,7 @@ dependencies = [ "sp-offchain", "sp-runtime", "sp-session", - "sp-std 5.0.0", + "sp-std", "sp-transaction-pool", "sp-version", "substrate-wasm-builder", @@ -11111,7 +10699,7 @@ dependencies = [ "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "teerex-primitives", "xcm", "xcm-builder", @@ -11805,7 +11393,7 @@ source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.4 dependencies = [ "array-bytes 4.2.0", "async-trait", - "fork-tree 3.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", + "fork-tree", "futures 0.3.28", "futures-timer", "libp2p", @@ -12095,7 +11683,7 @@ dependencies = [ "serde_json 1.0.103", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -12215,17 +11803,6 @@ dependencies = [ "sp-arithmetic", ] -[[package]] -name = "scale-bits" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd7aca73785181cc41f0bbe017263e682b585ca660540ba569133901d013ecf" -dependencies = [ - "parity-scale-codec", - "scale-info", - "serde 1.0.193", -] - [[package]] name = "scale-bits" version = "0.4.0" @@ -12237,18 +11814,6 @@ dependencies = [ "serde 1.0.193", ] -[[package]] -name = "scale-decode" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d823d4be477fc33321f93d08fb6c2698273d044f01362dc27573a750deb7c233" -dependencies = [ - "parity-scale-codec", - "scale-bits 0.3.0", - "scale-info", - "thiserror 1.0.44", -] - [[package]] name = "scale-decode" version = "0.8.0" @@ -12257,7 +11822,7 @@ checksum = "ea509715113edab351e1f4d51fba6b186653259049a1155b52e2e994dd2f0e6d" dependencies = [ "parity-scale-codec", "primitive-types", - "scale-bits 0.4.0", + "scale-bits", "scale-decode-derive", "scale-info", "smallvec 1.11.0", @@ -12284,7 +11849,7 @@ checksum = "3f6f51bc8cd927dab2f4567b1a8a8e9d7fd5d0866f2dbc7c84fc97cfa9383a26" dependencies = [ "parity-scale-codec", "primitive-types", - "scale-bits 0.4.0", + "scale-bits", "scale-encode-derive", "scale-info", "smallvec 1.11.0", @@ -12329,23 +11894,6 @@ dependencies = [ "syn 1.0.109", ] -[[package]] -name = "scale-value" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16a5e7810815bd295da73e4216d1dfbced3c7c7c7054d70fa5f6e4c58123fff4" -dependencies = [ - "either", - "frame-metadata", - "parity-scale-codec", - "scale-bits 0.3.0", - "scale-decode 0.4.0", - "scale-info", - "serde 1.0.193", - "thiserror 1.0.44", - "yap", -] - [[package]] name = "schannel" version = "0.1.22" @@ -12695,7 +12243,7 @@ dependencies = [ "serde 1.0.193", "serde_json 1.0.103", "sp-core", - "sp-std 5.0.0", + "sp-std", "teerex-primitives", "x509-cert", ] @@ -13040,7 +12588,7 @@ dependencies = [ "parity-scale-codec", "paste", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13146,7 +12694,7 @@ dependencies = [ "sp-metadata-ir", "sp-runtime", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-trie", "sp-version", "thiserror 1.0.44", @@ -13176,7 +12724,7 @@ dependencies = [ "serde 1.0.193", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13189,7 +12737,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde 1.0.193", - "sp-std 5.0.0", + "sp-std", "static_assertions", ] @@ -13203,7 +12751,7 @@ dependencies = [ "sp-api", "sp-application-crypto", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13215,7 +12763,7 @@ dependencies = [ "sp-api", "sp-inherents", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13265,7 +12813,7 @@ dependencies = [ "sp-consensus-slots", "sp-inherents", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-timestamp", ] @@ -13286,7 +12834,7 @@ dependencies = [ "sp-inherents", "sp-keystore", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-timestamp", ] @@ -13305,7 +12853,7 @@ dependencies = [ "sp-core", "sp-keystore", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13316,7 +12864,7 @@ dependencies = [ "parity-scale-codec", "scale-info", "serde 1.0.193", - "sp-std 5.0.0", + "sp-std", "sp-timestamp", ] @@ -13351,11 +12899,11 @@ dependencies = [ "secp256k1 0.24.3", "secrecy", "serde 1.0.193", - "sp-core-hashing 5.0.0", + "sp-core-hashing", "sp-debug-derive", "sp-externalities", "sp-runtime-interface", - "sp-std 5.0.0", + "sp-std", "sp-storage", "ss58-registry", "substrate-bip39", @@ -13374,22 +12922,7 @@ dependencies = [ "digest 0.10.7", "sha2 0.10.7", "sha3", - "sp-std 5.0.0", - "twox-hash", -] - -[[package]] -name = "sp-core-hashing" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc2d1947252b7a4e403b0a260f596920443742791765ec111daa2bbf98eff25" -dependencies = [ - "blake2", - "byteorder 1.4.3", - "digest 0.10.7", - "sha2 0.10.7", - "sha3", - "sp-std 6.0.0", + "sp-std", "twox-hash", ] @@ -13400,7 +12933,7 @@ source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.4 dependencies = [ "proc-macro2", "quote", - "sp-core-hashing 5.0.0", + "sp-core-hashing", "syn 2.0.32", ] @@ -13430,7 +12963,7 @@ source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.4 dependencies = [ "environmental 1.1.4", "parity-scale-codec", - "sp-std 5.0.0", + "sp-std", "sp-storage", ] @@ -13445,7 +12978,7 @@ dependencies = [ "scale-info", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "thiserror 1.0.44", ] @@ -13480,7 +13013,7 @@ dependencies = [ "sp-keystore", "sp-runtime-interface", "sp-state-machine", - "sp-std 5.0.0", + "sp-std", "sp-tracing", "sp-trie", "tracing", @@ -13529,7 +13062,7 @@ dependencies = [ "frame-metadata", "parity-scale-codec", "scale-info", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13543,7 +13076,7 @@ dependencies = [ "sp-arithmetic", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13594,7 +13127,7 @@ dependencies = [ "sp-arithmetic", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-std 5.0.0", + "sp-std", "sp-weights", ] @@ -13609,7 +13142,7 @@ dependencies = [ "primitive-types", "sp-externalities", "sp-runtime-interface-proc-macro", - "sp-std 5.0.0", + "sp-std", "sp-storage", "sp-tracing", "sp-wasm-interface", @@ -13639,7 +13172,7 @@ dependencies = [ "sp-core", "sp-runtime", "sp-staking", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13652,7 +13185,7 @@ dependencies = [ "serde 1.0.193", "sp-core", "sp-runtime", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13669,7 +13202,7 @@ dependencies = [ "sp-core", "sp-externalities", "sp-panic-handler", - "sp-std 5.0.0", + "sp-std", "sp-trie", "thiserror 1.0.44", "tracing", @@ -13680,12 +13213,6 @@ name = "sp-std" version = "5.0.0" source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" -[[package]] -name = "sp-std" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af0ee286f98455272f64ac5bb1384ff21ac029fbb669afbaf48477faff12760e" - [[package]] name = "sp-storage" version = "7.0.0" @@ -13696,7 +13223,7 @@ dependencies = [ "ref-cast", "serde 1.0.193", "sp-debug-derive", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -13710,7 +13237,7 @@ dependencies = [ "parity-scale-codec", "sp-inherents", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "thiserror 1.0.44", ] @@ -13720,7 +13247,7 @@ version = "6.0.0" source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" dependencies = [ "parity-scale-codec", - "sp-std 5.0.0", + "sp-std", "tracing", "tracing-core", "tracing-subscriber", @@ -13747,7 +13274,7 @@ dependencies = [ "sp-core", "sp-inherents", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-trie", ] @@ -13767,7 +13294,7 @@ dependencies = [ "scale-info", "schnellru", "sp-core", - "sp-std 5.0.0", + "sp-std", "thiserror 1.0.44", "tracing", "trie-db", @@ -13786,7 +13313,7 @@ dependencies = [ "serde 1.0.193", "sp-core-hashing-proc-macro", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-version-proc-macro", "thiserror 1.0.44", ] @@ -13811,7 +13338,7 @@ dependencies = [ "impl-trait-for-tuples", "log 0.4.20", "parity-scale-codec", - "sp-std 5.0.0", + "sp-std", "wasmi", "wasmtime", ] @@ -13828,7 +13355,7 @@ dependencies = [ "sp-arithmetic", "sp-core", "sp-debug-derive", - "sp-std 5.0.0", + "sp-std", ] [[package]] @@ -14057,17 +13584,6 @@ dependencies = [ "sp-keystore", ] -[[package]] -name = "substrate-fixed" -version = "0.5.9" -source = "git+https://github.com/encointer/substrate-fixed?tag=v0.5.9#a4fb461aae6205ffc55bed51254a40c52be04e5d" -dependencies = [ - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "typenum 1.16.0 (git+https://github.com/encointer/typenum?tag=v1.16.0)", -] - [[package]] name = "substrate-fixed" version = "0.5.9" @@ -14196,15 +13712,9 @@ dependencies = [ "parity-scale-codec", "scale-info", "sp-core", - "sp-std 5.0.0", + "sp-std", ] -[[package]] -name = "temp-dir" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af547b166dd1ea4b472165569fc456cfb6818116f854690b0ff205e636523dab" - [[package]] name = "tempfile" version = "3.7.0" @@ -16349,7 +15859,7 @@ dependencies = [ "sp-arithmetic", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "xcm", "xcm-executor", ] @@ -16368,7 +15878,7 @@ dependencies = [ "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", - "sp-std 5.0.0", + "sp-std", "sp-weights", "xcm", ] @@ -16461,12 +15971,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "yap" -version = "0.7.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc77f52dc9e9b10d55d3f4462c3b7fc393c4f17975d641542833ab2d3bc26ef" - [[package]] name = "yasna" version = "0.3.1" diff --git a/bitacross-worker/Cargo.toml b/bitacross-worker/Cargo.toml index 1b734ef250..0bf7d05941 100644 --- a/bitacross-worker/Cargo.toml +++ b/bitacross-worker/Cargo.toml @@ -16,7 +16,6 @@ members = [ "core/parentchain/parentchain-crate", "core/rest-client", "core/rpc-client", - "core/rpc-server", "core/tls-websocket-server", "core-primitives/attestation-handler", "core-primitives/import-queue", @@ -57,18 +56,6 @@ members = [ "core-primitives/types", "core-primitives/utils", "service", - "sidechain/block-composer", - "sidechain/block-verification", - "sidechain/consensus/aura", - "sidechain/consensus/common", - "sidechain/consensus/slots", - "sidechain/fork-tree", - "sidechain/peer-fetch", - "sidechain/primitives", - "sidechain/rpc-handler", - "sidechain/sidechain-crate", - "sidechain/state", - "sidechain/validateer-fetch", "litentry/primitives", "litentry/core/direct-call", "bitacross/core/bc-task-receiver", diff --git a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml index a31f3ee956..9e21fe26c8 100644 --- a/bitacross-worker/app-libs/parentchain-interface/Cargo.toml +++ b/bitacross-worker/app-libs/parentchain-interface/Cargo.toml @@ -16,7 +16,6 @@ itp-api-client-types = { path = "../../core-primitives/node-api/api-client-types itp-node-api = { path = "../../core-primitives/node-api", default-features = false } itp-stf-primitives = { path = "../../core-primitives/stf-primitives", default-features = false } itp-types = { path = "../../core-primitives/types", default-features = false } -itp-utils = { path = "../../core-primitives/utils", default-features = false } # no-std compatible libraries bs58 = { version = "0.4.0", default-features = false, features = ["alloc"] } @@ -32,7 +31,6 @@ bc-relayer-registry = { path = "../../bitacross/core/bc-relayer-registry", defau lc-scheduled-enclave = { path = "../../litentry/core/scheduled-enclave", default-features = false, optional = true } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } [dev-dependencies] @@ -60,14 +58,12 @@ std = [ "itp-stf-primitives/std", "itp-top-pool-author/std", "itp-types/std", - "itp-utils/std", "log/std", #substrate "sp-core/std", "sp-runtime/std", "litentry-primitives/std", "lc-scheduled-enclave/std", - "sp-std/std", "bc-relayer-registry/std", ] sgx = [ diff --git a/bitacross-worker/app-libs/stf/Cargo.toml b/bitacross-worker/app-libs/stf/Cargo.toml index 759b6d7bee..c08164736c 100644 --- a/bitacross-worker/app-libs/stf/Cargo.toml +++ b/bitacross-worker/app-libs/stf/Cargo.toml @@ -7,7 +7,6 @@ edition = "2021" [dependencies] # crates.io codec = { version = "3.0.0", default-features = false, features = ["derive"], package = "parity-scale-codec" } -hex = { version = "0.4", default-features = false } hex-literal = { version = "0.4" } log = { version = "0.4", default-features = false } rlp = { version = "0.5", default-features = false } @@ -39,7 +38,6 @@ sp-runtime = { default-features = false, git = "https://github.com/paritytech/su sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } # litentry -itp-node-api-metadata-provider = { path = "../../core-primitives/node-api/metadata-provider", default-features = false } litentry-macros = { path = "../../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../../litentry/primitives", default-features = false } pallet-parentchain = { path = "../../../pallets/parentchain", default-features = false } @@ -58,7 +56,6 @@ sgx = [ "itp-node-api/sgx", # litentry "litentry-primitives/sgx", - "itp-node-api-metadata-provider/sgx", ] std = [ # crates.io @@ -85,7 +82,6 @@ std = [ "sp-io/std", # litentry "litentry-primitives/std", - "itp-node-api-metadata-provider/std", ] test = [] production = [ diff --git a/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml index dc34df7d55..c9fa935eb7 100644 --- a/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-relayer-registry/Cargo.toml @@ -6,41 +6,22 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -bitcoin = { version = "0.31.0", default-features = false, features = ["secp-recovery", "no-std"] } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -hex = { version = "0.4.3", default-features = false } lazy_static = { version = "1.1.0", features = ["spin_no_std"] } log = { version = "0.4", default-features = false } -pallet-evm = { default-features = false, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } -rand = { version = "0.7", optional = true } -rand-sgx = { package = "rand", git = "https://github.com/mesalock-linux/rand-sgx", tag = "sgx_1.1.3", features = ["sgx_tstd"], optional = true } -ring = { version = "0.16.20", default-features = false } -scale-info = { version = "2.4.0", default-features = false, features = ["derive"] } -secp256k1 = { version = "0.28.0", default-features = false } -serde = { version = "1.0", default-features = false, features = ["alloc", "derive"] } -sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -strum = { version = "0.26", default-features = false } -strum_macros = { version = "0.26", default-features = false } thiserror = { version = "1.0.26", optional = true } # sgx dependencies -base64_sgx = { package = "base64", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base64-sgx", optional = true } sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true, features = ["net", "thread"] } thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } # internal dependencies itp-settings = { path = "../../../core-primitives/settings", default-features = false } -itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } itp-sgx-io = { path = "../../../core-primitives/sgx/io", default-features = false } -itp-utils = { path = "../../../core-primitives/utils", default-features = false } -litentry-hex-utils = { path = "../../../../primitives/hex", default-features = false } -parentchain-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } -# litentry primities +# litentry primities litentry-primitives = { path = "../../../litentry/primitives", default-features = false } [dev-dependencies] @@ -49,32 +30,16 @@ base64 = { version = "0.13", features = ["alloc"] } [features] default = ["std"] production = [ - "parentchain-primitives/production", ] sgx = [ "sgx_tstd", - "rand-sgx", - "itp-sgx-crypto/sgx", "thiserror-sgx", "itp-sgx-io/sgx", "litentry-primitives/sgx", ] std = [ - "strum/std", - "hex/std", - "serde/std", - "itp-sgx-crypto/std", - "itp-utils/std", - "sp-core/std", "sp-std/std", - "sp-io/std", - "sp-runtime/std", - "ring/std", - "parentchain-primitives/std", - "rand", "log/std", - "bitcoin/std", - "secp256k1/std", "thiserror", "itp-sgx-io/std", "litentry-primitives/std", diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml index e332e15e8f..d386a1a6d0 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-task-receiver/Cargo.toml @@ -12,7 +12,6 @@ threadpool = { version = "1.8.0", optional = true } thiserror_sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } -futures = { version = "0.3.8", optional = true } futures_sgx = { package = "futures", git = "https://github.com/mesalock-linux/futures-rs-sgx", optional = true } # sgx dependencies @@ -23,24 +22,16 @@ threadpool_sgx = { git = "https://github.com/mesalock-linux/rust-threadpool-sgx" # no_std dependencies codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } log = { version = "0.4", default-features = false } -sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } # internal dependencies frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -ita-sgx-runtime = { path = "../../../app-libs/sgx-runtime", default-features = false } + ita-stf = { path = "../../../app-libs/stf", default-features = false } -itp-enclave-metrics = { path = "../../../core-primitives/enclave-metrics", default-features = false } -itp-extrinsics-factory = { path = "../../../core-primitives/extrinsics-factory", default-features = false } -itp-node-api = { path = "../../../core-primitives/node-api", default-features = false } itp-ocall-api = { path = "../../../core-primitives/ocall-api", default-features = false } itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } itp-sgx-externalities = { path = "../../../core-primitives/substrate-sgx/externalities", default-features = false } itp-stf-executor = { path = "../../../core-primitives/stf-executor", default-features = false } itp-stf-state-handler = { path = "../../../core-primitives/stf-state-handler", default-features = false } -itp-storage = { path = "../../../core-primitives/storage", default-features = false } -itp-top-pool-author = { path = "../../../core-primitives/top-pool-author", default-features = false } -itp-types = { path = "../../../core-primitives/types", default-features = false } -itp-utils = { path = "../../../core-primitives/utils", default-features = false } # litentry primities bc-relayer-registry = { path = "../bc-relayer-registry", default-features = false } @@ -60,15 +51,10 @@ sgx = [ "lc-direct-call/sgx", "litentry-primitives/sgx", "ita-stf/sgx", - "itp-enclave-metrics/sgx", - "itp-extrinsics-factory/sgx", - "itp-node-api/sgx", "itp-sgx-crypto/sgx", "itp-sgx-externalities/sgx", "itp-stf-executor/sgx", "itp-stf-state-handler/sgx", - "itp-storage/sgx", - "itp-top-pool-author/sgx", "thiserror_sgx", "futures_sgx", ] @@ -79,21 +65,12 @@ std = [ "bc-relayer-registry/std", "lc-direct-call/std", "litentry-primitives/std", - "ita-sgx-runtime/std", "ita-stf/std", - "itp-enclave-metrics/std", - "itp-extrinsics-factory/std", - "itp-node-api/std", "itp-ocall-api/std", "itp-sgx-crypto/std", "itp-sgx-externalities/std", "itp-stf-executor/std", "itp-stf-state-handler/std", - "itp-storage/std", - "itp-top-pool-author/std", - "itp-types/std", - "itp-utils/std", - "futures", "thiserror", ] production = [ diff --git a/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml b/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml index a1a4c5024d..fd738ebe7b 100644 --- a/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml +++ b/bitacross-worker/bitacross/core/bc-task-sender/Cargo.toml @@ -16,7 +16,6 @@ sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sd # no_std dependencies codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } lazy_static = { version = "1.1.0", features = ["spin_no_std"] } -log = { version = "0.4", default-features = false } # litentry primities litentry-primitives = { path = "../../../litentry/primitives", default-features = false } @@ -31,7 +30,6 @@ sgx = [ ] std = [ "futures", - "log/std", "futures", "litentry-primitives/std", ] diff --git a/bitacross-worker/cli/Cargo.toml b/bitacross-worker/cli/Cargo.toml index d29ddd70de..eb02dc1117 100644 --- a/bitacross-worker/cli/Cargo.toml +++ b/bitacross-worker/cli/Cargo.toml @@ -45,23 +45,18 @@ itp-node-api = { path = "../core-primitives/node-api" } itp-rpc = { path = "../core-primitives/rpc" } itp-sgx-crypto = { path = "../core-primitives/sgx/crypto" } itp-stf-primitives = { path = "../core-primitives/stf-primitives" } -itp-time-utils = { path = "../core-primitives/time-utils" } itp-types = { path = "../core-primitives/types" } itp-utils = { path = "../core-primitives/utils" } # litentry -frame-metadata = "15.0.0" -ita-sgx-runtime = { path = "../app-libs/sgx-runtime" } lc-direct-call = { path = "../litentry/core/direct-call" } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } -scale-value = "0.6.0" -sp-core-hashing = "6.0.0" +pallet-teerex = { path = "../../pallets/teerex", default-features = false } [features] default = [] evm = ["ita-stf/evm_std", "pallet-evm"] -sidechain = [] offchain-worker = [] production = [] # dcap feature flag is not used in this crate, but for easier build purposes only it present here as well diff --git a/bitacross-worker/cli/src/lib.rs b/bitacross-worker/cli/src/lib.rs index 8acf785bb3..e07909c196 100644 --- a/bitacross-worker/cli/src/lib.rs +++ b/bitacross-worker/cli/src/lib.rs @@ -59,8 +59,6 @@ pub(crate) const ED25519_KEY_TYPE: KeyTypeId = KeyTypeId(*b"ed25"); #[clap(version = VERSION)] #[clap(author = "Trust Computing GmbH ")] #[clap(about = "cli tool to interact with litentry-parachain and workers", long_about = None)] -#[cfg_attr(feature = "sidechain", clap(about = "interact with litentry-parachain and sidechain", long_about = None))] -#[cfg_attr(feature = "offchain-worker", clap(about = "interact with litentry-parachain and offchain-worker", long_about = None))] #[clap(after_help = "stf subcommands depend on the stf crate this has been built against")] pub struct Cli { /// node url diff --git a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs index f39b1b7da3..dc4469f198 100644 --- a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs @@ -33,10 +33,6 @@ extern "C" { pub fn init_enclave_sidechain_components( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, - fail_mode: *const u8, - fail_mode_size: u32, - fail_at: *const u8, - fail_at_size: u32, ) -> sgx_status_t; pub fn init_direct_invocation_server( @@ -71,8 +67,6 @@ extern "C" { parentchain_id_size: u32, ) -> sgx_status_t; - pub fn execute_trusted_calls(eid: sgx_enclave_id_t, retval: *mut sgx_status_t) -> sgx_status_t; - pub fn sync_parentchain( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, @@ -206,15 +200,6 @@ extern "C" { pub fn test_main_entrance(eid: sgx_enclave_id_t, retval: *mut sgx_status_t) -> sgx_status_t; - pub fn call_rpc_methods( - eid: sgx_enclave_id_t, - retval: *mut sgx_status_t, - request: *const u8, - request_len: u32, - response: *mut u8, - response_len: u32, - ) -> sgx_status_t; - pub fn run_state_provisioning_server( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, diff --git a/bitacross-worker/core-primitives/enclave-api/src/direct_request.rs b/bitacross-worker/core-primitives/enclave-api/src/direct_request.rs deleted file mode 100644 index f3fff3388a..0000000000 --- a/bitacross-worker/core-primitives/enclave-api/src/direct_request.rs +++ /dev/null @@ -1,58 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::EnclaveResult; - -pub trait DirectRequest: Send + Sync + 'static { - // Todo: Vec shall be replaced by D: Decode, E: Encode but this is currently - // not compatible with the direct_api_server... - fn rpc(&self, request: Vec) -> EnclaveResult>; -} - -#[cfg(feature = "implement-ffi")] -mod impl_ffi { - use super::DirectRequest; - use crate::{error::Error, Enclave, EnclaveResult}; - use frame_support::ensure; - use itp_enclave_api_ffi as ffi; - use sgx_types::sgx_status_t; - - impl DirectRequest for Enclave { - fn rpc(&self, request: Vec) -> EnclaveResult> { - let mut retval = sgx_status_t::SGX_SUCCESS; - let response_len = 8192; - let mut response: Vec = vec![0u8; response_len as usize]; - - let res = unsafe { - ffi::call_rpc_methods( - self.eid, - &mut retval, - request.as_ptr(), - request.len() as u32, - response.as_mut_ptr(), - response_len, - ) - }; - - ensure!(res == sgx_status_t::SGX_SUCCESS, Error::Sgx(res)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - - Ok(response) - } - } -} diff --git a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs index 71f632ecfe..8cc9d6c42a 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs @@ -36,11 +36,7 @@ pub trait EnclaveBase: Send + Sync + 'static { ) -> EnclaveResult<()>; /// Initialize the enclave sidechain components. - fn init_enclave_sidechain_components( - &self, - fail_mode: Option, - fail_at: u64, - ) -> EnclaveResult<()>; + fn init_enclave_sidechain_components(&self) -> EnclaveResult<()>; /// Initialize the direct invocation RPC server. fn init_direct_invocation_server(&self, rpc_server_addr: String) -> EnclaveResult<()>; @@ -137,25 +133,10 @@ mod impl_ffi { Ok(()) } - fn init_enclave_sidechain_components( - &self, - fail_mode: Option, - fail_at: u64, - ) -> EnclaveResult<()> { + fn init_enclave_sidechain_components(&self) -> EnclaveResult<()> { let mut retval = sgx_status_t::SGX_SUCCESS; - let encoded_fail_mode = fail_mode.encode(); - let encoded_fail_at = fail_at.encode(); - let result = unsafe { - ffi::init_enclave_sidechain_components( - self.eid, - &mut retval, - encoded_fail_mode.as_ptr(), - encoded_fail_mode.len() as u32, - encoded_fail_at.as_ptr(), - encoded_fail_at.len() as u32, - ) - }; + let result = unsafe { ffi::init_enclave_sidechain_components(self.eid, &mut retval) }; ensure!(result == sgx_status_t::SGX_SUCCESS, Error::Sgx(result)); ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); diff --git a/bitacross-worker/core-primitives/enclave-api/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/src/lib.rs index 463608e111..131f4e9b7a 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/lib.rs @@ -13,7 +13,6 @@ use crate::error::Error; -pub mod direct_request; pub mod enclave_base; pub mod enclave_test; pub mod error; diff --git a/bitacross-worker/core-primitives/enclave-api/src/sidechain.rs b/bitacross-worker/core-primitives/enclave-api/src/sidechain.rs index 877460075b..9dee8e3bb4 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/sidechain.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/sidechain.rs @@ -35,8 +35,6 @@ pub trait Sidechain: Send + Sync + 'static { is_syncing: bool, ) -> EnclaveResult<()>; - fn execute_trusted_calls(&self) -> EnclaveResult<()>; - // litentry /// Ignore the parentchain block import validation until the given block number /// TODO: use the generic Header::Number trait @@ -92,17 +90,6 @@ mod impl_ffi { Ok(()) } - fn execute_trusted_calls(&self) -> EnclaveResult<()> { - let mut retval = sgx_status_t::SGX_SUCCESS; - - let result = unsafe { ffi::execute_trusted_calls(self.eid, &mut retval) }; - - ensure!(result == sgx_status_t::SGX_SUCCESS, Error::Sgx(result)); - ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); - - Ok(()) - } - fn ignore_parentchain_block_import_validation_until( &self, until: u32, diff --git a/bitacross-worker/core-primitives/enclave-metrics/Cargo.toml b/bitacross-worker/core-primitives/enclave-metrics/Cargo.toml index b6f3ae3e29..27504af273 100644 --- a/bitacross-worker/core-primitives/enclave-metrics/Cargo.toml +++ b/bitacross-worker/core-primitives/enclave-metrics/Cargo.toml @@ -12,12 +12,10 @@ sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sd # no-std dependencies codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full"] } -substrate-fixed = { default-features = false, git = "https://github.com/encointer/substrate-fixed", tag = "v0.5.9" } [features] default = ["std"] std = [ - "substrate-fixed/std", "codec/std", ] sgx = [ diff --git a/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs b/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs index 9d080b1243..0bec4a51da 100644 --- a/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-metrics/src/lib.rs @@ -37,9 +37,4 @@ pub enum EnclaveMetric { SuccessfulTrustedOperationIncrement(String), FailedTrustedOperationIncrement(String), ParentchainBlockImportTime(Duration), - SidechainBlockImportTime(Duration), - SidechainSlotPrepareTime(Duration), - SidechainSlotStfExecutionTime(Duration), - SidechainSlotBlockCompositionTime(Duration), - SidechainBlockBroadcastingTime(Duration), } diff --git a/bitacross-worker/core-primitives/ocall-api/src/lib.rs b/bitacross-worker/core-primitives/ocall-api/src/lib.rs index d4a0a9b944..2366dd00f1 100644 --- a/bitacross-worker/core-primitives/ocall-api/src/lib.rs +++ b/bitacross-worker/core-primitives/ocall-api/src/lib.rs @@ -19,14 +19,14 @@ pub extern crate alloc; -use alloc::{string::String, vec::Vec}; +use alloc::vec::Vec; use codec::{Decode, Encode}; use core::result::Result as StdResult; use derive_more::{Display, From}; use itp_storage::Error as StorageError; use itp_types::{ - parentchain::ParentchainId, storage::StorageEntryVerified, BlockHash, ShardIdentifier, - TrustedOperationStatus, WorkerRequest, WorkerResponse, + parentchain::ParentchainId, storage::StorageEntryVerified, TrustedOperationStatus, + WorkerRequest, WorkerResponse, }; use sgx_types::*; use sp_core::H256; @@ -126,27 +126,6 @@ pub trait EnclaveMetricsOCallApi: Clone + Send + Sync { fn update_metric(&self, metric: Metric) -> SgxResult<()>; } -pub trait EnclaveSidechainOCallApi: Clone + Send + Sync { - fn propose_sidechain_blocks( - &self, - signed_blocks: Vec, - ) -> SgxResult<()>; - - fn store_sidechain_blocks( - &self, - signed_blocks: Vec, - ) -> SgxResult<()>; - - fn fetch_sidechain_blocks_from_peer( - &self, - last_imported_block_hash: BlockHash, - maybe_until_block_hash: Option, - shard_identifier: ShardIdentifier, - ) -> SgxResult>; - - fn get_trusted_peers_urls(&self) -> SgxResult>; -} - /// Newtype for IPFS CID pub struct IpfsCid(pub [u8; 46]); diff --git a/bitacross-worker/core-primitives/settings/Cargo.toml b/bitacross-worker/core-primitives/settings/Cargo.toml index 3aadf1e5e5..5248c07308 100644 --- a/bitacross-worker/core-primitives/settings/Cargo.toml +++ b/bitacross-worker/core-primitives/settings/Cargo.toml @@ -5,11 +5,8 @@ authors = ['Trust Computing GmbH ', 'Integritee AG WorkerMode; -} - -#[derive(Default, Copy, Clone)] -pub struct WorkerModeProvider; - -#[cfg(feature = "offchain-worker")] -impl ProvideWorkerMode for WorkerModeProvider { - fn worker_mode() -> WorkerMode { - WorkerMode::OffChainWorker - } -} - -#[cfg(feature = "sidechain")] -impl ProvideWorkerMode for WorkerModeProvider { - fn worker_mode() -> WorkerMode { - WorkerMode::Sidechain - } -} - -// Default to `Sidechain` worker mode when no cargo features are set. -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] -impl ProvideWorkerMode for WorkerModeProvider { - fn worker_mode() -> WorkerMode { - WorkerMode::Sidechain - } -} diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index 3c81afe672..709c1ad97a 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -60,10 +60,7 @@ pub mod sgx { std::string::ToString, }; use itp_sgx_io::{seal, unseal, SealedIO}; - use k256::{ - ecdsa::{SigningKey, VerifyingKey}, - PublicKey, - }; + use k256::ecdsa::SigningKey; use log::*; use sgx_rand::{Rng, StdRng}; use std::{path::PathBuf, string::String}; @@ -134,7 +131,6 @@ pub mod sgx { #[cfg(feature = "test")] pub mod sgx_tests { - use super::sgx::*; use crate::{ create_ecdsa_repository, key_repository::AccessKey, std::string::ToString, Pair, Seal, }; diff --git a/bitacross-worker/core-primitives/stf-executor/Cargo.toml b/bitacross-worker/core-primitives/stf-executor/Cargo.toml index 0b34106bc4..3a2b6d5e9b 100644 --- a/bitacross-worker/core-primitives/stf-executor/Cargo.toml +++ b/bitacross-worker/core-primitives/stf-executor/Cargo.toml @@ -12,7 +12,6 @@ sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sd sgx_types = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git" } # local dependencies -itp-enclave-metrics = { path = "../enclave-metrics", default-features = false } itp-node-api = { path = "../node-api", default-features = false } itp-ocall-api = { path = "../ocall-api", default-features = false } itp-sgx-crypto = { path = "../sgx/crypto", default-features = false } @@ -43,9 +42,6 @@ sp-runtime = { default-features = false, git = "https://github.com/paritytech/su itc-parentchain-test = { path = "../../core/parentchain/test", optional = true, default-features = false } itp-test = { path = "../test", default-features = false, optional = true } -# litentry -litentry-primitives = { path = "../../litentry/primitives", default-features = false } - [dev-dependencies] itp-stf-state-observer = { path = "../stf-state-observer", features = ["mocks"] } itp-stf-interface = { path = "../stf-interface", features = ["mocks"] } @@ -73,8 +69,6 @@ std = [ "sp-core/std", "sp-runtime/std", "thiserror", - # litentry - "litentry-primitives/std", ] sgx = [ "sgx_tstd", @@ -86,8 +80,6 @@ sgx = [ "itp-top-pool-author/sgx", "itp-time-utils/sgx", "thiserror_sgx", - # litentry - "litentry-primitives/sgx", ] test = [ "itc-parentchain-test", diff --git a/bitacross-worker/core-primitives/test/Cargo.toml b/bitacross-worker/core-primitives/test/Cargo.toml index 92466e8615..eff1d893b5 100644 --- a/bitacross-worker/core-primitives/test/Cargo.toml +++ b/bitacross-worker/core-primitives/test/Cargo.toml @@ -29,7 +29,6 @@ itp-stf-interface = { path = "../stf-interface", default-features = false } itp-stf-primitives = { path = "../stf-primitives", default-features = false } itp-stf-state-handler = { path = "../stf-state-handler", default-features = false } itp-storage = { path = "../storage", default-features = false } -itp-time-utils = { path = "../time-utils", default-features = false } itp-types = { path = "../types", default-features = false, features = ["test"] } # litentry @@ -50,7 +49,6 @@ std = [ "itp-stf-primitives/std", "itp-stf-state-handler/std", "itp-storage/std", - "itp-time-utils/std", "itp-types/std", "log/std", "sp-core/std", @@ -66,7 +64,6 @@ sgx = [ "itp-sgx-crypto/sgx", "itp-sgx-externalities/sgx", "itp-stf-state-handler/sgx", - "itp-time-utils/sgx", "jsonrpc-core_sgx", "sgx_tstd", "litentry-primitives/sgx", diff --git a/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs b/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs index 86c0d12a35..5871396aee 100644 --- a/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs +++ b/bitacross-worker/core-primitives/test/src/mock/onchain_mock.rs @@ -18,21 +18,18 @@ use codec::{Decode, Encode}; use core::fmt::Debug; -use itp_ocall_api::{ - EnclaveAttestationOCallApi, EnclaveMetricsOCallApi, EnclaveOnChainOCallApi, - EnclaveSidechainOCallApi, -}; +use itp_ocall_api::{EnclaveAttestationOCallApi, EnclaveMetricsOCallApi, EnclaveOnChainOCallApi}; use itp_storage::Error::StorageValueUnavailable; use itp_types::{ - parentchain::ParentchainId, storage::StorageEntryVerified, AccountId, BlockHash, - ShardIdentifier, WorkerRequest, WorkerResponse, WorkerType, + parentchain::ParentchainId, storage::StorageEntryVerified, AccountId, WorkerRequest, + WorkerResponse, WorkerType, }; use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; use sgx_types::*; use sp_core::H256; use sp_runtime::{traits::Header as HeaderTrait, OpaqueExtrinsic}; use sp_std::prelude::*; -use std::{collections::HashMap, string::String}; +use std::collections::HashMap; #[derive(Default, Clone, Debug)] pub struct OnchainMock { @@ -139,35 +136,6 @@ impl EnclaveAttestationOCallApi for OnchainMock { } } -impl EnclaveSidechainOCallApi for OnchainMock { - fn propose_sidechain_blocks( - &self, - _signed_blocks: Vec, - ) -> SgxResult<()> { - Ok(()) - } - - fn store_sidechain_blocks( - &self, - _signed_blocks: Vec, - ) -> SgxResult<()> { - Ok(()) - } - - fn fetch_sidechain_blocks_from_peer( - &self, - _last_imported_block_hash: BlockHash, - _maybe_until_block_hash: Option, - _shard_identifier: ShardIdentifier, - ) -> SgxResult> { - Ok(Vec::new()) - } - - fn get_trusted_peers_urls(&self) -> SgxResult> { - Ok(Vec::default()) - } -} - impl EnclaveMetricsOCallApi for OnchainMock { fn update_metric(&self, _metric: Metric) -> SgxResult<()> { Ok(()) diff --git a/bitacross-worker/core-primitives/test/src/mock/sidechain_ocall_api_mock.rs b/bitacross-worker/core-primitives/test/src/mock/sidechain_ocall_api_mock.rs index 0210e3bd85..640a6aa73f 100644 --- a/bitacross-worker/core-primitives/test/src/mock/sidechain_ocall_api_mock.rs +++ b/bitacross-worker/core-primitives/test/src/mock/sidechain_ocall_api_mock.rs @@ -23,10 +23,9 @@ use std::sync::RwLock; use codec::{Decode, Encode}; use core::marker::PhantomData; -use itp_ocall_api::{EnclaveMetricsOCallApi, EnclaveSidechainOCallApi}; -use itp_types::{BlockHash, ShardIdentifier}; -use sgx_types::{sgx_status_t, SgxResult}; -use std::{string::String, vec::Vec}; +use itp_ocall_api::EnclaveMetricsOCallApi; +use sgx_types::SgxResult; +use std::vec::Vec; pub struct SidechainOCallApiMock { fetch_from_peer_blocks: Option>, @@ -80,45 +79,3 @@ where Ok(()) } } - -impl EnclaveSidechainOCallApi - for SidechainOCallApiMock -where - SignedSidechainBlockType: Clone + Encode + Decode + Send + Sync, -{ - fn propose_sidechain_blocks( - &self, - _signed_blocks: Vec, - ) -> SgxResult<()> { - Ok(()) - } - - fn store_sidechain_blocks( - &self, - _signed_blocks: Vec, - ) -> SgxResult<()> { - Ok(()) - } - - fn fetch_sidechain_blocks_from_peer( - &self, - _last_imported_block_hash: BlockHash, - _maybe_until_block_hash: Option, - _shard_identifier: ShardIdentifier, - ) -> SgxResult> { - let mut number_of_fetch_calls_lock = self.number_of_fetch_calls.write().unwrap(); - *number_of_fetch_calls_lock += 1; - - match &self.fetch_from_peer_blocks { - Some(blocks) => Ok(blocks - .iter() - .map(|b| SignedSidechainBlock::decode(&mut b.encode().as_slice()).unwrap()) - .collect()), - None => Err(sgx_status_t::SGX_ERROR_UNEXPECTED), - } - } - - fn get_trusted_peers_urls(&self) -> SgxResult> { - Ok(Vec::default()) - } -} diff --git a/bitacross-worker/core-primitives/top-pool-author/Cargo.toml b/bitacross-worker/core-primitives/top-pool-author/Cargo.toml index b5f8d264e4..6bafe2e32c 100644 --- a/bitacross-worker/core-primitives/top-pool-author/Cargo.toml +++ b/bitacross-worker/core-primitives/top-pool-author/Cargo.toml @@ -73,5 +73,4 @@ sgx = [ ] test = ["itp-test/sgx", "itp-top-pool/mocks"] mocks = ["lazy_static"] -sidechain = [] offchain-worker = [] diff --git a/bitacross-worker/core-primitives/top-pool-author/src/api.rs b/bitacross-worker/core-primitives/top-pool-author/src/api.rs index 7214e184e3..7a133dca40 100644 --- a/bitacross-worker/core-primitives/top-pool-author/src/api.rs +++ b/bitacross-worker/core-primitives/top-pool-author/src/api.rs @@ -134,9 +134,6 @@ mod tests { type TestChainApi = SidechainApi; - type Seed = [u8; 32]; - const TEST_SEED: Seed = *b"12345678901234567890123456789012"; - pub fn endowed_account() -> ed25519::Pair { ed25519::Pair::from_seed(&[42u8; 32].into()) } diff --git a/bitacross-worker/core-primitives/top-pool-author/src/author.rs b/bitacross-worker/core-primitives/top-pool-author/src/author.rs index a123d72491..49d015198f 100644 --- a/bitacross-worker/core-primitives/top-pool-author/src/author.rs +++ b/bitacross-worker/core-primitives/top-pool-author/src/author.rs @@ -58,20 +58,7 @@ use std::{ }; /// Define type of TOP filter that is used in the Author -#[cfg(feature = "sidechain")] -pub type AuthorTopFilter = crate::top_filter::CallsOnlyFilter; -#[cfg(feature = "sidechain")] -pub type BroadcastedTopFilter = crate::top_filter::DirectCallsOnlyFilter; - -#[cfg(feature = "offchain-worker")] pub type AuthorTopFilter = crate::top_filter::IndirectCallsOnlyFilter; -#[cfg(feature = "offchain-worker")] -pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; - -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] -pub type AuthorTopFilter = crate::top_filter::CallsOnlyFilter; - -#[cfg(not(any(feature = "sidechain", feature = "offchain-worker")))] pub type BroadcastedTopFilter = crate::top_filter::DenyAllFilter; /// Currently we treat all RPC operations as externals. @@ -571,7 +558,5 @@ impl< { type Hash = TxHash; - fn on_block_imported(&self, hashes: &[Self::Hash], block_hash: SidechainBlockHash) { - self.top_pool.on_block_imported(hashes, block_hash) - } + fn on_block_imported(&self, _hashes: &[Self::Hash], _block_hash: SidechainBlockHash) {} } diff --git a/bitacross-worker/core-primitives/top-pool-author/src/author_tests.rs b/bitacross-worker/core-primitives/top-pool-author/src/author_tests.rs index 3fb0370970..9a2db01035 100644 --- a/bitacross-worker/core-primitives/top-pool-author/src/author_tests.rs +++ b/bitacross-worker/core-primitives/top-pool-author/src/author_tests.rs @@ -39,7 +39,6 @@ use itp_top_pool::mocks::trusted_operation_pool_mock::TrustedOperationPoolMock; use itp_utils::ToHexPrefixed; use litentry_primitives::BroadcastedRequest; use sgx_crypto_helper::{rsa3072::Rsa3072KeyPair, RsaKeyPair}; -use sp_core::H256; use std::sync::Arc; type TestAuthor = Author< diff --git a/bitacross-worker/core-primitives/top-pool-author/src/test_fixtures.rs b/bitacross-worker/core-primitives/top-pool-author/src/test_fixtures.rs index b46f1d3e7c..d5c83341d5 100644 --- a/bitacross-worker/core-primitives/top-pool-author/src/test_fixtures.rs +++ b/bitacross-worker/core-primitives/top-pool-author/src/test_fixtures.rs @@ -18,25 +18,9 @@ use codec::Encode; use itp_stf_primitives::types::ShardIdentifier; -use sp_core::{ed25519, Pair}; use sp_runtime::traits::{BlakeTwo256, Hash}; use std::vec; -type Seed = [u8; 32]; -const TEST_SEED: Seed = *b"12345678901234567890123456789012"; - -pub(crate) fn mr_enclave() -> [u8; 32] { - [1u8; 32] -} - pub(crate) fn shard_id() -> ShardIdentifier { BlakeTwo256::hash(vec![1u8, 2u8, 3u8].as_slice().encode().as_slice()) } - -fn alice_pair() -> ed25519::Pair { - ed25519::Pair::from_seed(b"22222678901234567890123456789012") -} - -fn bob_pair() -> ed25519::Pair { - ed25519::Pair::from_seed(b"33333378901234567890123456789012") -} diff --git a/bitacross-worker/core-primitives/top-pool/Cargo.toml b/bitacross-worker/core-primitives/top-pool/Cargo.toml index 21f6f89ea7..a7407db3a7 100644 --- a/bitacross-worker/core-primitives/top-pool/Cargo.toml +++ b/bitacross-worker/core-primitives/top-pool/Cargo.toml @@ -12,7 +12,6 @@ sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sd itc-direct-rpc-server = { path = "../../core/direct-rpc-server", default-features = false } itp-stf-primitives = { path = "../stf-primitives", default-features = false } itp-types = { path = "../types", default-features = false } -its-primitives = { path = "../../sidechain/primitives", default-features = false } # sgx enabled external libraries jsonrpc-core_sgx = { package = "jsonrpc-core", git = "https://github.com/scs/jsonrpc", branch = "no_std_v18", default-features = false, optional = true } @@ -52,7 +51,6 @@ sgx = [ std = [ "itc-direct-rpc-server/std", "itp-types/std", - "its-primitives/std", "jsonrpc-core", "linked-hash-map", "log/std", diff --git a/bitacross-worker/core-primitives/top-pool/src/basic_pool.rs b/bitacross-worker/core-primitives/top-pool/src/basic_pool.rs index 577898f4f0..d046fa72b6 100644 --- a/bitacross-worker/core-primitives/top-pool/src/basic_pool.rs +++ b/bitacross-worker/core-primitives/top-pool/src/basic_pool.rs @@ -40,7 +40,6 @@ use codec::Encode; use core::{marker::PhantomData, pin::Pin}; use itc_direct_rpc_server::SendRpcResponse; use itp_stf_primitives::{traits::PoolTransactionValidation, types::ShardIdentifier}; -use its_primitives::types::BlockHash as SidechainBlockHash; use jsonrpc_core::futures::{ channel::oneshot, future::{ready, Future, FutureExt}, @@ -244,10 +243,6 @@ where self.pool.validated_pool().ready_by_hash(hash, shard) } - fn on_block_imported(&self, hashes: &[TxHash], block_hash: SidechainBlockHash) { - self.pool.validated_pool().on_block_imported(hashes, block_hash); - } - fn update_connection_state(&self, updates: Vec<(TxHash, (Vec, bool))>) { self.pool.validated_pool().update_connection_state(updates); } diff --git a/bitacross-worker/core-primitives/top-pool/src/mocks/trusted_operation_pool_mock.rs b/bitacross-worker/core-primitives/top-pool/src/mocks/trusted_operation_pool_mock.rs index 72f5514da6..1e515d612d 100644 --- a/bitacross-worker/core-primitives/top-pool/src/mocks/trusted_operation_pool_mock.rs +++ b/bitacross-worker/core-primitives/top-pool/src/mocks/trusted_operation_pool_mock.rs @@ -36,7 +36,7 @@ use crate::{ use codec::Encode; use core::{future::Future, pin::Pin}; -use itp_types::{Block, BlockHash as SidechainBlockHash, ShardIdentifier, H256}; +use itp_types::{Block, ShardIdentifier, H256}; use jsonrpc_core::futures::future::ready; use sp_runtime::{ generic::BlockId, @@ -211,8 +211,6 @@ where unimplemented!() } - fn on_block_imported(&self, _hashes: &[TxHash], _block_hash: SidechainBlockHash) {} - fn update_connection_state(&self, _updates: Vec<(TxHash, (Vec, bool))>) {} fn swap_rpc_connection_hash(&self, _old_hash: TxHash, _new_hash: TxHash) {} diff --git a/bitacross-worker/core-primitives/top-pool/src/pool.rs b/bitacross-worker/core-primitives/top-pool/src/pool.rs index 17a8fcbd5b..42f43c645b 100644 --- a/bitacross-worker/core-primitives/top-pool/src/pool.rs +++ b/bitacross-worker/core-primitives/top-pool/src/pool.rs @@ -478,7 +478,6 @@ pub mod tests { primitives::from_low_u64_to_be_h256, }; use codec::{Decode, Encode}; - use itp_stf_primitives::types::Nonce; use itp_test::mock::stf_mock::{ mock_top_direct_trusted_call_signed, mock_trusted_call_signed, TrustedOperationMock, }; @@ -492,7 +491,6 @@ pub mod tests { use sp_application_crypto::ed25519; use sp_core::hash::H256; use sp_runtime::traits::{BlakeTwo256, Extrinsic as ExtrinsicT, Hash, Verify}; - use std::{collections::HashSet, sync::Mutex}; #[derive(Clone, PartialEq, Eq, Encode, Decode, core::fmt::Debug, Serialize, MallocSizeOf)] pub enum Extrinsic { @@ -541,16 +539,10 @@ pub mod tests { /// Test RPC responder pub type TestRpcResponder = RpcResponderMock; - const INVALID_NONCE: Nonce = 254; const SOURCE: TrustedOperationSource = TrustedOperationSource::External; #[derive(Clone, Debug, Default)] - struct TestApi { - delay: Arc>>>, - invalidate: Arc>>, - clear_requirements: Arc>>, - add_requirements: Arc>>, - } + struct TestApi {} impl ChainApi for TestApi { type Block = tests::Block; diff --git a/bitacross-worker/core-primitives/top-pool/src/primitives.rs b/bitacross-worker/core-primitives/top-pool/src/primitives.rs index d40fbabd93..acd24bb317 100644 --- a/bitacross-worker/core-primitives/top-pool/src/primitives.rs +++ b/bitacross-worker/core-primitives/top-pool/src/primitives.rs @@ -10,7 +10,6 @@ use byteorder::{BigEndian, ByteOrder}; use codec::{Decode, Encode}; use core::pin::Pin; use itp_stf_primitives::types::ShardIdentifier; -use itp_types::BlockHash as SidechainBlockHash; use jsonrpc_core::futures::{channel::mpsc::Receiver, Future, Stream}; use sp_core::H256; use sp_runtime::{ @@ -250,9 +249,6 @@ pub trait TrustedOperationPool: Send + Sync { shard: ShardIdentifier, ) -> Option>; - /// Notify the listener of top inclusion in sidechain block - fn on_block_imported(&self, hashes: &[TxHash], block_hash: SidechainBlockHash); - /// Litentry: set the rpc response value #[allow(clippy::type_complexity)] fn update_connection_state(&self, updates: Vec<(TxHash, (Vec, bool))>); diff --git a/bitacross-worker/core/direct-rpc-client/Cargo.toml b/bitacross-worker/core/direct-rpc-client/Cargo.toml index 69631b5e40..18e30ea6a3 100644 --- a/bitacross-worker/core/direct-rpc-client/Cargo.toml +++ b/bitacross-worker/core/direct-rpc-client/Cargo.toml @@ -27,7 +27,6 @@ webpki = { version = "0.21", optional = true } itp-rpc = { path = "../../core-primitives/rpc", default-features = false } itp-types = { path = "../../core-primitives/types", default-features = false } itp-utils = { path = "../../core-primitives/utils", default-features = false } -litentry-primitives = { path = "../../litentry/primitives", default-features = false } [features] default = ["std"] @@ -38,7 +37,6 @@ sgx = [ "rustls_sgx", "sgx_tstd", "itp-rpc/sgx", - "litentry-primitives/sgx", ] std = [ "rustls", @@ -49,5 +47,4 @@ std = [ "itp-types/std", "itp-utils/std", "log/std", - "litentry-primitives/std", ] diff --git a/bitacross-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs b/bitacross-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs index 141ff21b54..461142f97c 100644 --- a/bitacross-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs +++ b/bitacross-worker/core/direct-rpc-server/src/rpc_watch_extractor.rs @@ -78,7 +78,6 @@ pub mod tests { use crate::builders::{ rpc_response_builder::RpcResponseBuilder, rpc_return_value_builder::RpcReturnValueBuilder, }; - use codec::Encode; use itp_rpc::Id; use itp_types::{TrustedOperationStatus, H256}; diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/Cargo.toml b/bitacross-worker/core/parentchain/indirect-calls-executor/Cargo.toml index e8e018d334..b006457b3a 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/Cargo.toml +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/Cargo.toml @@ -39,11 +39,8 @@ sp-core = { default-features = false, features = ["full_crypto"], git = "https:/ sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } # litentry -itp-utils = { path = "../../../core-primitives/utils", default-features = false } lc-scheduled-enclave = { path = "../../../litentry/core/scheduled-enclave", default-features = false, optional = true } litentry-primitives = { path = "../../../litentry/primitives", default-features = false } -parachain-core-primitives = { package = "core-primitives", path = "../../../../primitives/core", default-features = false } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } [dev-dependencies] env_logger = "0.9.0" @@ -76,7 +73,6 @@ std = [ "thiserror", # litentry "litentry-primitives/std", - "itp-utils/std", "lc-scheduled-enclave/std", ] sgx = [ diff --git a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs index aae0dcc240..7ab712aa71 100644 --- a/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs +++ b/bitacross-worker/core/parentchain/indirect-calls-executor/src/executor.rs @@ -300,7 +300,7 @@ pub fn hash_of(xt: &T) -> H256 { mod test { use super::*; use crate::mock::*; - use codec::{Decode, Encode}; + use codec::Encode; use itc_parentchain_test::ParentchainBlockBuilder; use itp_node_api::{ api_client::{ @@ -311,10 +311,6 @@ mod test { }; use itp_sgx_crypto::mocks::KeyRepositoryMock; use itp_stf_executor::mocks::StfEnclaveSignerMock; - use itp_stf_primitives::{ - traits::TrustedCallVerification, - types::{AccountId, TrustedOperation}, - }; use itp_test::mock::{ shielding_crypto_mock::ShieldingCryptoMock, stf_mock::{GetterMock, TrustedCallSignedMock}, diff --git a/bitacross-worker/core/rpc-client/Cargo.toml b/bitacross-worker/core/rpc-client/Cargo.toml index 1f2b3ff104..ebbba96ba7 100644 --- a/bitacross-worker/core/rpc-client/Cargo.toml +++ b/bitacross-worker/core/rpc-client/Cargo.toml @@ -19,7 +19,6 @@ ws = { version = "0.9.1", features = ["ssl"] } # parity frame-metadata = { version = "15.1.0", features = ["v14"] } -sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42", default-features = false } # local itp-api-client-types = { path = "../../core-primitives/node-api/api-client-types" } @@ -30,7 +29,6 @@ itp-utils = { path = "../../core-primitives/utils" } # litentry ita-stf = { path = "../../app-libs/stf" } itp-stf-primitives = { path = "../../core-primitives/stf-primitives" } -litentry-primitives = { path = "../../litentry/primitives", default-features = false } [dev-dependencies] env_logger = "0.9.0" diff --git a/bitacross-worker/core/rpc-client/src/mock.rs b/bitacross-worker/core/rpc-client/src/mock.rs index cbbda5a7a4..401c9496d2 100644 --- a/bitacross-worker/core/rpc-client/src/mock.rs +++ b/bitacross-worker/core/rpc-client/src/mock.rs @@ -20,11 +20,9 @@ use crate::{direct_client::DirectApi, error::Result}; use codec::Decode; use frame_metadata::RuntimeMetadataPrefixed; -use ita_stf::H256; use itp_api_client_types::Metadata; use itp_stf_primitives::types::{AccountId, ShardIdentifier}; use itp_types::MrEnclave; -use litentry_primitives::Identity; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use std::{sync::mpsc::Sender as MpscSender, thread::JoinHandle}; diff --git a/bitacross-worker/core/rpc-server/Cargo.toml b/bitacross-worker/core/rpc-server/Cargo.toml deleted file mode 100644 index d7f22c184e..0000000000 --- a/bitacross-worker/core/rpc-server/Cargo.toml +++ /dev/null @@ -1,30 +0,0 @@ -[package] -name = "itc-rpc-server" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -anyhow = "1.0.40" -jsonrpsee = { version = "0.2.0-alpha.7", features = ["full"] } -log = "0.4" -tokio = { version = "1.6.1", features = ["full"] } - -# local -itp-enclave-api = { path = "../../core-primitives/enclave-api" } -itp-rpc = { path = "../../core-primitives/rpc" } -itp-utils = { path = "../../core-primitives/utils" } -its-peer-fetch = { path = "../../sidechain/peer-fetch" } -its-primitives = { path = "../../sidechain/primitives" } -its-rpc-handler = { path = "../../sidechain/rpc-handler" } -its-storage = { path = "../../sidechain/storage" } - -[features] -default = ["std"] -std = [] - -[dev-dependencies] -env_logger = { version = "*" } -sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -its-test = { path = "../../sidechain/test" } -parity-scale-codec = "3.0.0" diff --git a/bitacross-worker/core/rpc-server/src/lib.rs b/bitacross-worker/core/rpc-server/src/lib.rs deleted file mode 100644 index 1386f0de4d..0000000000 --- a/bitacross-worker/core/rpc-server/src/lib.rs +++ /dev/null @@ -1,81 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use itp_enclave_api::direct_request::DirectRequest; -use itp_rpc::{Id, RpcRequest}; -use itp_utils::ToHexPrefixed; -use its_peer_fetch::block_fetch_server::BlockFetchServerModuleBuilder; -use its_primitives::types::block::SignedBlock; -use its_rpc_handler::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; -use its_storage::interface::FetchBlocks; -use jsonrpsee::{ - types::error::CallError, - ws_server::{RpcModule, WsServerBuilder}, -}; -use log::debug; -use std::{net::SocketAddr, sync::Arc}; -use tokio::net::ToSocketAddrs; - -#[cfg(test)] -mod mock; -#[cfg(test)] -mod tests; - -pub async fn run_server( - addr: impl ToSocketAddrs, - enclave: Arc, - sidechain_block_fetcher: Arc, -) -> anyhow::Result -where - Enclave: DirectRequest, - FetchSidechainBlocks: FetchBlocks + Send + Sync + 'static, -{ - let mut server = WsServerBuilder::default().build(addr).await?; - - // FIXME: import block should be moved to trusted side. - let mut import_sidechain_block_module = RpcModule::new(enclave); - import_sidechain_block_module.register_method( - RPC_METHOD_NAME_IMPORT_BLOCKS, - |params, enclave| { - debug!("{} params: {:?}", RPC_METHOD_NAME_IMPORT_BLOCKS, params); - - let enclave_req = RpcRequest::compose_jsonrpc_call( - Id::Text("1".to_string()), - RPC_METHOD_NAME_IMPORT_BLOCKS.into(), - vec![params.one::>()?.to_hex()], - ) - .unwrap(); - - enclave - .rpc(enclave_req.as_bytes().to_vec()) - .map_err(|e| CallError::Failed(e.into())) - }, - )?; - server.register_module(import_sidechain_block_module).unwrap(); - - let fetch_sidechain_blocks_module = BlockFetchServerModuleBuilder::new(sidechain_block_fetcher) - .build() - .map_err(|e| CallError::Failed(e.to_string().into()))?; // `to_string` necessary due to no all errors implementing Send + Sync. - server.register_module(fetch_sidechain_blocks_module).unwrap(); - - let socket_addr = server.local_addr()?; - tokio::spawn(async move { server.start().await }); - - println!("[+] Untrusted RPC server is spawned on: {}", socket_addr); - - Ok(socket_addr) -} diff --git a/bitacross-worker/core/rpc-server/src/mock.rs b/bitacross-worker/core/rpc-server/src/mock.rs deleted file mode 100644 index 172c1a7528..0000000000 --- a/bitacross-worker/core/rpc-server/src/mock.rs +++ /dev/null @@ -1,75 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use itp_enclave_api::{direct_request::DirectRequest, EnclaveResult}; -use itp_rpc::{Id, RpcResponse}; -use itp_utils::ToHexPrefixed; -use its_primitives::{ - traits::ShardIdentifierFor, - types::{BlockHash, BlockNumber, SignedBlock, SignedBlock as SignedSidechainBlock}, -}; -use its_storage::{interface::FetchBlocks, LastSidechainBlock}; -use parity_scale_codec::Encode; - -pub struct TestEnclave; - -impl DirectRequest for TestEnclave { - fn rpc(&self, _request: Vec) -> EnclaveResult> { - Ok(RpcResponse { - jsonrpc: "mock_response".into(), - result: "null".to_hex(), - id: Id::Number(1), - } - .encode()) - } -} - -pub struct MockSidechainBlockFetcher; - -impl FetchBlocks for MockSidechainBlockFetcher { - fn fetch_all_blocks_after( - &self, - _block_hash: &BlockHash, - _shard_identifier: &ShardIdentifierFor, - ) -> its_storage::Result> { - Ok(Vec::new()) - } - - fn fetch_blocks_in_range( - &self, - _block_hash_from: &BlockHash, - _block_hash_until: &BlockHash, - _shard_identifier: &ShardIdentifierFor, - ) -> its_storage::Result> { - Ok(Vec::new()) - } - - fn latest_block( - &self, - _shard_identifier: &ShardIdentifierFor, - ) -> Option { - Some(LastSidechainBlock::default()) - } - - fn block_hash( - &self, - _block_number: BlockNumber, - _shard_identifier: &ShardIdentifierFor, - ) -> Option { - Some(LastSidechainBlock::default()) - } -} diff --git a/bitacross-worker/core/rpc-server/src/tests.rs b/bitacross-worker/core/rpc-server/src/tests.rs deleted file mode 100644 index 4c99081804..0000000000 --- a/bitacross-worker/core/rpc-server/src/tests.rs +++ /dev/null @@ -1,56 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use super::*; -use crate::mock::MockSidechainBlockFetcher; -use itp_rpc::RpcResponse; -use its_rpc_handler::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; -use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; -use jsonrpsee::{ - types::{to_json_value, traits::Client}, - ws_client::WsClientBuilder, -}; -use log::info; -use mock::TestEnclave; -use parity_scale_codec::Decode; - -fn init() { - let _ = env_logger::builder().is_test(true).try_init(); -} - -#[tokio::test] -async fn test_client_calls() { - init(); - let addr = - run_server("127.0.0.1:0", Arc::new(TestEnclave), Arc::new(MockSidechainBlockFetcher)) - .await - .unwrap(); - info!("ServerAddress: {:?}", addr); - - let url = format!("ws://{}", addr); - let client = WsClientBuilder::default().build(&url).await.unwrap(); - let response: Vec = client - .request( - RPC_METHOD_NAME_IMPORT_BLOCKS, - vec![to_json_value(vec![SidechainBlockBuilder::default().build_signed()]).unwrap()] - .into(), - ) - .await - .unwrap(); - - assert!(RpcResponse::decode(&mut response.as_slice()).is_ok()); -} diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 285bb971e4..7b78fec982 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -252,31 +252,14 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" name = "bc-relayer-registry" version = "0.1.0" dependencies = [ - "bitcoin", - "core-primitives", - "hex 0.4.3", "itp-settings", - "itp-sgx-crypto", "itp-sgx-io", - "itp-utils", "lazy_static", - "litentry-hex-utils", "litentry-primitives", "log", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "parity-scale-codec", - "rand 0.7.3", - "ring 0.16.20", - "scale-info", - "secp256k1 0.28.0", - "serde 1.0.193", "sgx_tstd", - "sp-core", - "sp-io", - "sp-runtime", "sp-std", - "strum", - "strum_macros", "thiserror", ] @@ -289,26 +272,17 @@ dependencies = [ "frame-support", "futures 0.3.8", "hex 0.4.0", - "ita-sgx-runtime", "ita-stf", - "itp-enclave-metrics", - "itp-extrinsics-factory", - "itp-node-api", "itp-ocall-api", "itp-sgx-crypto", "itp-sgx-externalities", "itp-stf-executor", "itp-stf-state-handler", - "itp-storage", - "itp-top-pool-author", - "itp-types", - "itp-utils", "lc-direct-call", "litentry-primitives", "log", "parity-scale-codec", "sgx_tstd", - "sp-core", "thiserror", "threadpool", ] @@ -320,7 +294,6 @@ dependencies = [ "futures 0.3.8", "lazy_static", "litentry-primitives", - "log", "parity-scale-codec", "sgx_tstd", ] @@ -713,7 +686,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.7", - "typenum 1.17.0", + "typenum", ] [[package]] @@ -928,11 +901,12 @@ dependencies = [ "array-bytes 6.1.0", "bc-relayer-registry", "bc-task-receiver", + "bc-task-sender", "cid", "derive_more", "env_logger", "frame-support", - "frame-system", + "futures 0.3.8", "hex 0.4.3", "ipfs-unixfs", "ita-parentchain-interface", @@ -949,7 +923,6 @@ dependencies = [ "itc-tls-websocket-server", "itp-attestation-handler", "itp-component-container", - "itp-enclave-metrics", "itp-extrinsics-factory", "itp-import-queue", "itp-node-api", @@ -970,14 +943,10 @@ dependencies = [ "itp-stf-state-observer", "itp-storage", "itp-test", - "itp-time-utils", "itp-top-pool", "itp-top-pool-author", "itp-types", "itp-utils", - "its-block-verification", - "its-primitives", - "its-sidechain", "jsonrpc-core", "lazy_static", "lc-scheduled-enclave", @@ -1611,7 +1580,7 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" dependencies = [ - "typenum 1.17.0", + "typenum", ] [[package]] @@ -1620,7 +1589,7 @@ version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ - "typenum 1.17.0", + "typenum", "version_check", "zeroize", ] @@ -1915,7 +1884,6 @@ dependencies = [ "itp-node-api", "itp-stf-primitives", "itp-types", - "itp-utils", "lc-scheduled-enclave", "litentry-hex-utils", "litentry-primitives", @@ -1924,7 +1892,6 @@ dependencies = [ "sgx_tstd", "sp-core", "sp-runtime", - "sp-std", ] [[package]] @@ -1956,13 +1923,11 @@ version = "0.9.0" dependencies = [ "frame-support", "frame-system", - "hex 0.4.3", "hex-literal", "ita-sgx-runtime", "itp-hashing", "itp-node-api", "itp-node-api-metadata", - "itp-node-api-metadata-provider", "itp-sgx-externalities", "itp-stf-interface", "itp-stf-primitives", @@ -1992,7 +1957,6 @@ dependencies = [ "itp-rpc", "itp-types", "itp-utils", - "litentry-primitives", "log", "parity-scale-codec", "rustls 0.19.0 (git+https://github.com/mesalock-linux/rustls?tag=sgx_1.1.3)", @@ -2090,7 +2054,6 @@ version = "0.9.0" dependencies = [ "binary-merkle-tree", "bs58", - "core-primitives", "futures 0.3.8", "itp-api-client-types", "itp-node-api", @@ -2101,7 +2064,6 @@ dependencies = [ "itp-test", "itp-top-pool-author", "itp-types", - "itp-utils", "lc-scheduled-enclave", "litentry-primitives", "log", @@ -2110,7 +2072,6 @@ dependencies = [ "sgx_types", "sp-core", "sp-runtime", - "sp-std", "thiserror", ] @@ -2265,7 +2226,6 @@ version = "0.9.0" dependencies = [ "parity-scale-codec", "sgx_tstd", - "substrate-fixed", ] [[package]] @@ -2376,9 +2336,6 @@ dependencies = [ [[package]] name = "itp-settings" version = "0.9.0" -dependencies = [ - "litentry-primitives", -] [[package]] name = "itp-sgx-crypto" @@ -2446,7 +2403,6 @@ version = "0.9.0" dependencies = [ "hex 0.4.3", "itc-parentchain-test", - "itp-enclave-metrics", "itp-node-api", "itp-ocall-api", "itp-sgx-crypto", @@ -2459,7 +2415,6 @@ dependencies = [ "itp-time-utils", "itp-top-pool-author", "itp-types", - "litentry-primitives", "log", "parity-scale-codec", "sgx_tstd", @@ -2558,7 +2513,6 @@ dependencies = [ "itp-stf-primitives", "itp-stf-state-handler", "itp-storage", - "itp-time-utils", "itp-types", "jsonrpc-core", "lc-teebag-storage", @@ -2591,7 +2545,6 @@ dependencies = [ "itc-direct-rpc-server", "itp-stf-primitives", "itp-types", - "its-primitives", "jsonrpc-core", "linked-hash-map", "log", @@ -2652,210 +2605,6 @@ dependencies = [ "parity-scale-codec", ] -[[package]] -name = "its-block-composer" -version = "0.9.0" -dependencies = [ - "itp-node-api", - "itp-settings", - "itp-sgx-crypto", - "itp-sgx-externalities", - "itp-stf-executor", - "itp-stf-primitives", - "itp-time-utils", - "itp-top-pool-author", - "itp-types", - "its-primitives", - "its-state", - "log", - "parity-scale-codec", - "sgx_tstd", - "sgx_types", - "sp-core", - "sp-runtime", - "thiserror", -] - -[[package]] -name = "its-block-verification" -version = "0.9.0" -dependencies = [ - "frame-support", - "itp-types", - "itp-utils", - "its-primitives", - "log", - "sgx_tstd", - "sp-consensus-slots", - "sp-core", - "sp-runtime", - "thiserror", -] - -[[package]] -name = "its-consensus-aura" -version = "0.9.0" -dependencies = [ - "finality-grandpa", - "ita-stf", - "itc-parentchain-block-import-dispatcher", - "itc-peer-top-broadcaster", - "itp-enclave-metrics", - "itp-ocall-api", - "itp-settings", - "itp-sgx-crypto", - "itp-sgx-externalities", - "itp-stf-executor", - "itp-stf-primitives", - "itp-stf-state-handler", - "itp-time-utils", - "itp-top-pool-author", - "itp-types", - "itp-utils", - "its-block-composer", - "its-block-verification", - "its-consensus-common", - "its-consensus-slots", - "its-primitives", - "its-state", - "its-validateer-fetch", - "lc-scheduled-enclave", - "litentry-hex-utils", - "log", - "parity-scale-codec", - "sgx_tstd", - "sp-core", - "sp-runtime", -] - -[[package]] -name = "its-consensus-common" -version = "0.9.0" -dependencies = [ - "itc-parentchain-light-client", - "itp-enclave-metrics", - "itp-extrinsics-factory", - "itp-import-queue", - "itp-node-api-metadata", - "itp-node-api-metadata-provider", - "itp-ocall-api", - "itp-settings", - "itp-sgx-crypto", - "itp-types", - "its-block-verification", - "its-primitives", - "its-state", - "log", - "parity-scale-codec", - "sgx_tstd", - "sgx_types", - "sp-runtime", - "thiserror", -] - -[[package]] -name = "its-consensus-slots" -version = "0.9.0" -dependencies = [ - "derive_more", - "hex 0.4.3", - "itp-settings", - "itp-sgx-externalities", - "itp-stf-state-handler", - "itp-time-utils", - "itp-types", - "its-block-verification", - "its-consensus-common", - "its-primitives", - "its-state", - "lazy_static", - "lc-scheduled-enclave", - "log", - "parity-scale-codec", - "sgx_tstd", - "sp-consensus-slots", - "sp-runtime", -] - -[[package]] -name = "its-primitives" -version = "0.1.0" -dependencies = [ - "itp-types", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-runtime", - "sp-std", -] - -[[package]] -name = "its-rpc-handler" -version = "0.9.0" -dependencies = [ - "bc-task-sender", - "futures 0.3.8", - "itp-rpc", - "itp-stf-primitives", - "itp-top-pool-author", - "itp-types", - "itp-utils", - "its-primitives", - "jsonrpc-core", - "litentry-primitives", - "log", - "parity-scale-codec", - "rust-base58", - "sgx_tstd", - "sp-core", -] - -[[package]] -name = "its-sidechain" -version = "0.9.0" -dependencies = [ - "its-block-composer", - "its-consensus-aura", - "its-consensus-common", - "its-consensus-slots", - "its-primitives", - "its-rpc-handler", - "its-state", - "its-validateer-fetch", -] - -[[package]] -name = "its-state" -version = "0.9.0" -dependencies = [ - "frame-support", - "itp-sgx-externalities", - "itp-storage", - "its-primitives", - "log", - "parity-scale-codec", - "sgx_tstd", - "sp-core", - "sp-io", - "sp-runtime", - "thiserror", -] - -[[package]] -name = "its-validateer-fetch" -version = "0.9.0" -dependencies = [ - "derive_more", - "itp-ocall-api", - "itp-types", - "lc-teebag-storage", - "parity-scale-codec", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "js-sys" version = "0.3.65" @@ -2927,7 +2676,6 @@ dependencies = [ "sgx_tstd", "sp-core", "sp-io", - "sp-runtime", ] [[package]] @@ -3032,12 +2780,8 @@ dependencies = [ "core-primitives", "hex 0.4.3", "itp-sgx-crypto", - "itp-sgx-io", - "itp-utils", - "lazy_static", "litentry-hex-utils", "log", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "pallet-teebag", "parity-scale-codec", "rand 0.7.3", @@ -3050,9 +2794,6 @@ dependencies = [ "sp-io", "sp-runtime", "sp-std", - "strum", - "strum_macros", - "thiserror", ] [[package]] @@ -4564,17 +4305,6 @@ dependencies = [ "sp-std", ] -[[package]] -name = "sp-consensus-slots" -version = "0.10.0-dev" -source = "git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42#ff24c60ac7d9f87727ecdd0ded9a80c56e4f4b65" -dependencies = [ - "parity-scale-codec", - "scale-info", - "sp-std", - "sp-timestamp", -] - [[package]] name = "sp-core" version = "7.0.0" @@ -4932,16 +4662,6 @@ dependencies = [ "sp-runtime-interface", ] -[[package]] -name = "substrate-fixed" -version = "0.5.9" -source = "git+https://github.com/encointer/substrate-fixed?tag=v0.5.9#a4fb461aae6205ffc55bed51254a40c52be04e5d" -dependencies = [ - "parity-scale-codec", - "scale-info", - "typenum 1.16.0", -] - [[package]] name = "subtle" version = "2.4.1" @@ -5181,15 +4901,6 @@ dependencies = [ "static_assertions", ] -[[package]] -name = "typenum" -version = "1.16.0" -source = "git+https://github.com/encointer/typenum?tag=v1.16.0#4c8dddaa8bdd13130149e43b4085ad14e960617f" -dependencies = [ - "parity-scale-codec", - "scale-info", -] - [[package]] name = "typenum" version = "1.17.0" diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 8c80fc75aa..19acc0e263 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -25,7 +25,6 @@ production = [ "litentry-macros/production", "bc-task-receiver/production", ] -sidechain = ["itp-settings/sidechain", "itp-top-pool-author/sidechain"] offchain-worker = [ "itp-settings/offchain-worker", "itp-top-pool-author/offchain-worker", @@ -45,8 +44,6 @@ test = [ "itp-test/sgx", "itp-top-pool-author/test", "itp-top-pool-author/mocks", - # substrate - "frame-system", ] dcap = [] @@ -67,11 +64,13 @@ sgx_types = { branch = "master", git = "https://github.com/apache/teaclave-sgx-s array-bytes = { version = "6.0.0" } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } derive_more = { version = "0.99.5" } +futures_sgx = { package = "futures", git = "https://github.com/mesalock-linux/futures-rs-sgx" } hex = { version = "0.4.3", default-features = false, features = ["alloc"] } ipfs-unixfs = { default-features = false, git = "https://github.com/whalelephant/rust-ipfs", branch = "w-nstd" } lazy_static = { version = "1.1.0", features = ["spin_no_std"] } primitive-types = { version = "0.12.1", default-features = false, features = ["codec", "serde_no_std"] } + # scs / integritee jsonrpc-core = { default-features = false, git = "https://github.com/scs/jsonrpc", branch = "no_std_v18" } @@ -105,7 +104,6 @@ itc-peer-top-broadcaster = { path = "../core/peer-top-broadcaster", default-feat itc-tls-websocket-server = { path = "../core/tls-websocket-server", default-features = false, features = ["sgx"] } itp-attestation-handler = { path = "../core-primitives/attestation-handler", default-features = false, features = ["sgx"] } itp-component-container = { path = "../core-primitives/component-container", default-features = false, features = ["sgx"] } -itp-enclave-metrics = { path = "../core-primitives/enclave-metrics", default-features = false, features = ["sgx"] } itp-extrinsics-factory = { path = "../core-primitives/extrinsics-factory", default-features = false, features = ["sgx"] } itp-import-queue = { path = "../core-primitives/import-queue", default-features = false, features = ["sgx"] } itp-node-api = { path = "../core-primitives/node-api", default-features = false, features = ["sgx"] } @@ -125,17 +123,14 @@ itp-stf-state-handler = { path = "../core-primitives/stf-state-handler", default itp-stf-state-observer = { path = "../core-primitives/stf-state-observer", default-features = false, features = ["sgx"] } itp-storage = { path = "../core-primitives/storage", default-features = false, features = ["sgx"] } itp-test = { path = "../core-primitives/test", default-features = false, optional = true } -itp-time-utils = { path = "../core-primitives/time-utils", default-features = false, features = ["sgx"] } itp-top-pool = { path = "../core-primitives/top-pool", default-features = false, features = ["sgx"] } itp-top-pool-author = { path = "../core-primitives/top-pool-author", default-features = false, features = ["sgx"] } itp-types = { path = "../core-primitives/types", default-features = false } itp-utils = { path = "../core-primitives/utils", default-features = false } -its-block-verification = { path = "../sidechain/block-verification", default-features = false } -its-primitives = { path = "../sidechain/primitives", default-features = false } -its-sidechain = { path = "../sidechain/sidechain-crate", default-features = false, features = ["sgx"] } # litentry bc-relayer-registry = { path = "../bitacross/core/bc-relayer-registry", default-features = false, features = ["sgx"] } +bc-task-sender = { path = "../bitacross/core/bc-task-sender", default-features = false, features = ["sgx"] } lc-scheduled-enclave = { path = "../litentry/core/scheduled-enclave", default-features = false, features = ["sgx"] } litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-macros = { path = "../../primitives/core/macros", default-features = false } @@ -147,7 +142,6 @@ bc-task-receiver = { path = "../bitacross/core/bc-task-receiver", default-featur # substrate deps frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -frame-system = { optional = true, default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } diff --git a/bitacross-worker/enclave-runtime/Enclave.edl b/bitacross-worker/enclave-runtime/Enclave.edl index d6b3073fb2..ecc350dd13 100644 --- a/bitacross-worker/enclave-runtime/Enclave.edl +++ b/bitacross-worker/enclave-runtime/Enclave.edl @@ -45,10 +45,7 @@ enclave { public sgx_status_t publish_wallets(); - public sgx_status_t init_enclave_sidechain_components( - [in, size=fail_mode_size] uint8_t* fail_mode, uint32_t fail_mode_size, - [in, size=fail_at_size] uint8_t* fail_at, uint32_t fail_at_size - ); + public sgx_status_t init_enclave_sidechain_components(); public sgx_status_t init_direct_invocation_server( [in, size=server_addr_size] uint8_t* server_addr, uint32_t server_addr_size @@ -68,8 +65,6 @@ enclave { [in, size=parentchain_id_size] uint8_t* parentchain_id, uint32_t parentchain_id_size ); - public sgx_status_t execute_trusted_calls(); - public sgx_status_t sync_parentchain( [in, size=blocks_size] uint8_t* blocks, size_t blocks_size, [in, size=events_size] uint8_t* events, size_t events_size, @@ -164,11 +159,6 @@ enclave { int skip_ra ); - public sgx_status_t call_rpc_methods( - [in, size=request_len] uint8_t* request, uint32_t request_len, - [out, size=response_len] uint8_t* response, uint32_t response_len - ); - public size_t test_main_entrance(); public sgx_status_t migrate_shard( @@ -176,7 +166,7 @@ enclave { [in, size=shard_size] uint8_t* new_shard, uint32_t shard_size ); - + public sgx_status_t ignore_parentchain_block_import_validation_until( [in] uint32_t* until ); @@ -239,23 +229,6 @@ enclave { [in, size = metric_size] uint8_t * metric, uint32_t metric_size ); - sgx_status_t ocall_propose_sidechain_blocks( - [in, size = signed_blocks_size] uint8_t * signed_blocks, uint32_t signed_blocks_size - ); - - sgx_status_t ocall_store_sidechain_blocks( - [in, size = signed_blocks_size] uint8_t * signed_blocks, uint32_t signed_blocks_size - ); - - sgx_status_t ocall_fetch_sidechain_blocks_from_peer( - [in, size = last_imported_block_hash_size] uint8_t * last_imported_block_hash, uint32_t last_imported_block_hash_size, - [in, size = maybe_until_block_hash_size] uint8_t * maybe_until_block_hash, uint32_t maybe_until_block_hash_size, - [in, size = shard_identifier_size] uint8_t * shard_identifier, uint32_t shard_identifier_size, - [out, size = sidechain_blocks_size] uint8_t * sidechain_blocks, uint32_t sidechain_blocks_size - ); - - sgx_status_t ocall_get_trusted_peers_urls([out, size = peers_size] uint8_t * peers, uint32_t peers_size); - sgx_status_t ocall_send_to_parentchain( [in, size = extrinsics_size] uint8_t * extrinsics, uint32_t extrinsics_size, [in, size=parentchain_id_size] uint8_t* parentchain_id, uint32_t parentchain_id_size, diff --git a/bitacross-worker/enclave-runtime/src/attestation.rs b/bitacross-worker/enclave-runtime/src/attestation.rs index 5faa2b6893..08348de70f 100644 --- a/bitacross-worker/enclave-runtime/src/attestation.rs +++ b/bitacross-worker/enclave-runtime/src/attestation.rs @@ -47,15 +47,13 @@ use itp_node_api::metadata::{ Error as MetadataError, }; use itp_node_api_metadata::NodeMetadata; -use itp_settings::{ - worker::MR_ENCLAVE_SIZE, - worker_mode::{ProvideWorkerMode, WorkerModeProvider}, -}; +use itp_settings::worker::MR_ENCLAVE_SIZE; use itp_sgx_crypto::{ ed25519_derivation::DeriveEd25519, key_repository::AccessKey, Error as SgxCryptoError, }; use itp_types::{AttestationType, OpaqueCall, WorkerType}; use itp_utils::write_slice_and_whitespace_pad; +use litentry_primitives::WorkerMode; use log::*; use sgx_types::*; use sp_core::{ed25519::Public as Ed25519Public, Pair}; @@ -335,7 +333,7 @@ pub fn generate_dcap_ra_extrinsic_from_quote_internal( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::BitAcross, - WorkerModeProvider::worker_mode(), + WorkerMode::OffChainWorker, quote, url, shielding_pubkey, @@ -364,7 +362,7 @@ pub fn generate_dcap_skip_ra_extrinsic_from_mr_enclave( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::BitAcross, - WorkerModeProvider::worker_mode(), + WorkerMode::OffChainWorker, quote, url, shielding_pubkey, @@ -404,7 +402,7 @@ pub fn generate_ias_ra_extrinsic_from_der_cert_internal( let call = OpaqueCall::from_tuple(&( call_ids, WorkerType::BitAcross, - WorkerModeProvider::worker_mode(), + WorkerMode::OffChainWorker, cert_der, url, shielding_pubkey, diff --git a/bitacross-worker/enclave-runtime/src/error.rs b/bitacross-worker/enclave-runtime/src/error.rs index da657f87de..04ff2b796d 100644 --- a/bitacross-worker/enclave-runtime/src/error.rs +++ b/bitacross-worker/enclave-runtime/src/error.rs @@ -34,7 +34,6 @@ pub enum Error { NodeMetadataProvider(itp_node_api::metadata::provider::Error), Sgx(sgx_status_t), SgxQuote(sgx_quote3_error_t), - Consensus(its_sidechain::consensus_common::Error), Stf(String), StfStateHandler(itp_stf_state_handler::error::Error), StfExecution(itp_stf_executor::error::Error), diff --git a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs index 6ebddbc911..261c7fdfd0 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/global_components.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/global_components.rs @@ -85,16 +85,6 @@ use itp_top_pool_author::{ author::{Author, AuthorTopFilter, BroadcastedTopFilter}, }; use itp_types::{Block as ParentchainBlock, SignedBlock as SignedParentchainBlock}; -use its_primitives::{ - traits::{Block as SidechainBlockTrait, SignedBlock as SignedSidechainBlockTrait}, - types::block::SignedBlock as SignedSidechainBlock, -}; -use its_sidechain::{ - aura::block_importer::BlockImporter as SidechainBlockImporter, - block_composer::BlockComposer, - consensus_common::{BlockImportConfirmationHandler, BlockImportQueueWorker, PeerBlockSync}, - slots::FailSlotOnDemand, -}; use lazy_static::lazy_static; use litentry_primitives::BroadcastedRequest; use sgx_crypto_helper::rsa3072::Rsa3072KeyPair; @@ -316,43 +306,6 @@ pub type EnclaveTopPoolAuthor = Author< EnclaveGetter, >; pub type EnclaveDirectRpcBroadcaster = DirectRpcBroadcaster; -pub type EnclaveSidechainBlockComposer = - BlockComposer; -pub type EnclaveSidechainBlockImporter = SidechainBlockImporter< - Pair, - ParentchainBlock, - SignedSidechainBlock, - EnclaveOCallApi, - EnclaveStateHandler, - EnclaveStateKeyRepository, - EnclaveTopPoolAuthor, - // For now the sidechain does only support one parentchain. - IntegriteeParentchainTriggeredBlockImportDispatcher, - EnclaveDirectRpcBroadcaster, - EnclaveTrustedCallSigned, - EnclaveGetter, ->; -pub type EnclaveSidechainBlockImportQueue = ImportQueue; -pub type EnclaveBlockImportConfirmationHandler = BlockImportConfirmationHandler< - ParentchainBlock, - <::Block as SidechainBlockTrait>::HeaderType, - EnclaveNodeMetadataRepository, - EnclaveExtrinsicsFactory, - EnclaveValidatorAccessor, ->; -pub type EnclaveSidechainBlockSyncer = PeerBlockSync< - ParentchainBlock, - SignedSidechainBlock, - EnclaveSidechainBlockImporter, - EnclaveOCallApi, - EnclaveBlockImportConfirmationHandler, ->; -pub type EnclaveSidechainBlockImportQueueWorker = BlockImportQueueWorker< - ParentchainBlock, - SignedSidechainBlock, - EnclaveSidechainBlockImportQueue, - EnclaveSidechainBlockSyncer, ->; pub type EnclaveSealHandler = SealHandler< EnclaveShieldingKeyRepository, EnclaveStateKeyRepository, @@ -491,28 +444,3 @@ pub static GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT: ComponentContainer< /// Enclave RPC WS handler. pub static GLOBAL_RPC_WS_HANDLER_COMPONENT: ComponentContainer = ComponentContainer::new("rpc_ws_handler"); - -/// Sidechain import queue. -pub static GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT: ComponentContainer< - EnclaveSidechainBlockImportQueue, -> = ComponentContainer::new("sidechain_import_queue"); - -/// Sidechain import queue worker - processes the import queue. -pub static GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT: ComponentContainer< - EnclaveSidechainBlockImportQueueWorker, -> = ComponentContainer::new("sidechain_import_queue_worker"); - -/// Sidechain block composer. -pub static GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT: ComponentContainer< - EnclaveSidechainBlockComposer, -> = ComponentContainer::new("sidechain_block_composer"); - -/// Sidechain block syncer. -pub static GLOBAL_SIDECHAIN_BLOCK_SYNCER_COMPONENT: ComponentContainer< - EnclaveSidechainBlockSyncer, -> = ComponentContainer::new("sidechain_block_syncer"); - -/// Sidechain fail slot on demand. -pub static GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT: ComponentContainer< - Option, -> = ComponentContainer::new("sidechain_fail_slot_on_demand"); diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index 12180e7cc0..fbbd7ab234 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -21,11 +21,11 @@ pub mod global_components; pub mod parentchain; use crate::{ error::{Error, Result as EnclaveResult}, + get_node_metadata_repository_from_integritee_solo_or_parachain, + get_validator_accessor_from_integritee_solo_or_parachain, initialization::global_components::{ - EnclaveBlockImportConfirmationHandler, EnclaveGetterExecutor, EnclaveLightClientSeal, - EnclaveOCallApi, EnclaveRpcResponder, EnclaveShieldingKeyRepository, EnclaveSidechainApi, - EnclaveSidechainBlockImportQueue, EnclaveSidechainBlockImportQueueWorker, - EnclaveSidechainBlockImporter, EnclaveSidechainBlockSyncer, EnclaveStateFileIo, + EnclaveGetterExecutor, EnclaveLightClientSeal, EnclaveOCallApi, EnclaveRpcResponder, + EnclaveShieldingKeyRepository, EnclaveSidechainApi, EnclaveStateFileIo, EnclaveStateHandler, EnclaveStateInitializer, EnclaveStateObserver, EnclaveStateSnapshotRepository, EnclaveStfEnclaveSigner, EnclaveTopPool, EnclaveTopPoolAuthor, DIRECT_RPC_REQUEST_SINK_COMPONENT, @@ -33,29 +33,21 @@ use crate::{ GLOBAL_DIRECT_RPC_BROADCASTER_COMPONENT, GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT, GLOBAL_INTEGRITEE_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_OCALL_API_COMPONENT, GLOBAL_RPC_WS_HANDLER_COMPONENT, GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT, - GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT, GLOBAL_SIDECHAIN_BLOCK_SYNCER_COMPONENT, - GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT, GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT, - GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT, GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, - GLOBAL_STATE_HANDLER_COMPONENT, GLOBAL_STATE_KEY_REPOSITORY_COMPONENT, - GLOBAL_STATE_OBSERVER_COMPONENT, GLOBAL_TARGET_A_PARENTCHAIN_LIGHT_CLIENT_SEAL, + GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, GLOBAL_STATE_HANDLER_COMPONENT, + GLOBAL_STATE_KEY_REPOSITORY_COMPONENT, GLOBAL_STATE_OBSERVER_COMPONENT, + GLOBAL_TARGET_A_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TARGET_B_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TOP_POOL_AUTHOR_COMPONENT, GLOBAL_WEB_SOCKET_SERVER_COMPONENT, }, ocall::OcallApi, rpc::{rpc_response_channel::RpcResponseChannel, worker_api_direct::public_api_rpc_handler}, - utils::{ - get_extrinsic_factory_from_integritee_solo_or_parachain, - get_node_metadata_repository_from_integritee_solo_or_parachain, - get_triggered_dispatcher_from_integritee_solo_or_parachain, - get_validator_accessor_from_integritee_solo_or_parachain, - }, + utils::get_extrinsic_factory_from_integritee_solo_or_parachain, Hash, }; use base58::ToBase58; use bc_relayer_registry::{RelayerRegistryUpdater, GLOBAL_RELAYER_REGISTRY}; use bc_task_receiver::{run_bit_across_handler_runner, BitAcrossTaskContext}; use codec::Encode; -use core::str::FromStr; use ita_stf::{Getter, TrustedCallSigned}; use itc_direct_rpc_server::{ create_determine_watch, rpc_connection_registry::ConnectionRegistry, @@ -89,14 +81,9 @@ use itp_stf_state_handler::{ use itp_top_pool::pool::Options as PoolOptions; use itp_top_pool_author::author::{AuthorTopFilter, BroadcastedTopFilter}; use itp_types::{parentchain::ParentchainId, OpaqueCall, ShardIdentifier}; -use its_sidechain::{ - block_composer::BlockComposer, - slots::{FailSlotMode, FailSlotOnDemand}, -}; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; use litentry_primitives::BroadcastedRequest; use log::*; -use sgx_types::sgx_status_t; use sp_core::crypto::Pair; use std::{collections::HashMap, path::PathBuf, string::String, sync::Arc}; @@ -207,7 +194,7 @@ pub(crate) fn init_enclave( let top_pool_author = create_top_pool_author( rpc_responder, - state_handler.clone(), + state_handler, ocall_api.clone(), shielding_key_repository.clone(), request_sink_cloned, @@ -218,18 +205,11 @@ pub(crate) fn init_enclave( DIRECT_RPC_REQUEST_SINK_COMPONENT.initialize(request_sink); let getter_executor = Arc::new(EnclaveGetterExecutor::new(state_observer)); - let io_handler = public_api_rpc_handler( - top_pool_author, - getter_executor, - shielding_key_repository, - Some(state_handler), - ); + let io_handler = + public_api_rpc_handler(top_pool_author, getter_executor, shielding_key_repository); let rpc_handler = Arc::new(RpcWsHandler::new(io_handler, watch_extractor, connection_registry)); GLOBAL_RPC_WS_HANDLER_COMPONENT.initialize(rpc_handler); - let sidechain_block_import_queue = Arc::new(EnclaveSidechainBlockImportQueue::default()); - GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT.initialize(sidechain_block_import_queue); - let attestation_handler = Arc::new(IntelAttestationHandler::new(ocall_api, signing_key_repository)); GLOBAL_ATTESTATION_HANDLER_COMPONENT.initialize(attestation_handler); @@ -324,73 +304,11 @@ fn run_bit_across_handler() -> Result<(), Error> { Ok(()) } -pub(crate) fn init_enclave_sidechain_components( - fail_mode: Option, - fail_at: u64, -) -> EnclaveResult<()> { - let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - let direct_rpc_broadcaster = GLOBAL_DIRECT_RPC_BROADCASTER_COMPONENT.get()?; - - let top_pool_author = GLOBAL_TOP_POOL_AUTHOR_COMPONENT.get()?; - let state_key_repository = GLOBAL_STATE_KEY_REPOSITORY_COMPONENT.get()?; - +pub(crate) fn init_enclave_sidechain_components() -> EnclaveResult<()> { // GLOBAL_SCHEDULED_ENCLAVE must be initialized after attestation_handler and enclave let attestation_handler = GLOBAL_ATTESTATION_HANDLER_COMPONENT.get()?; let mrenclave = attestation_handler.get_mrenclave()?; GLOBAL_SCHEDULED_ENCLAVE.init(mrenclave).map_err(|e| Error::Other(e.into()))?; - - let parentchain_block_import_dispatcher = - get_triggered_dispatcher_from_integritee_solo_or_parachain()?; - - let signer = GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT.get()?.retrieve_key()?; - - let sidechain_block_importer = Arc::new(EnclaveSidechainBlockImporter::new( - state_handler, - state_key_repository.clone(), - top_pool_author, - parentchain_block_import_dispatcher, - ocall_api.clone(), - direct_rpc_broadcaster, - )); - - let sidechain_block_import_queue = GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT.get()?; - let metadata_repository = get_node_metadata_repository_from_integritee_solo_or_parachain()?; - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let validator_accessor = get_validator_accessor_from_integritee_solo_or_parachain()?; - - let sidechain_block_import_confirmation_handler = - Arc::new(EnclaveBlockImportConfirmationHandler::new( - metadata_repository, - extrinsics_factory, - validator_accessor, - )); - - let sidechain_block_syncer = Arc::new(EnclaveSidechainBlockSyncer::new( - sidechain_block_importer, - ocall_api, - sidechain_block_import_confirmation_handler, - )); - GLOBAL_SIDECHAIN_BLOCK_SYNCER_COMPONENT.initialize(sidechain_block_syncer.clone()); - - let sidechain_block_import_queue_worker = - Arc::new(EnclaveSidechainBlockImportQueueWorker::new( - sidechain_block_import_queue, - sidechain_block_syncer, - )); - GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT.initialize(sidechain_block_import_queue_worker); - - let block_composer = Arc::new(BlockComposer::new(signer, state_key_repository)); - GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT.initialize(block_composer); - if let Some(fail_mode) = fail_mode { - let fail_mode = FailSlotMode::from_str(&fail_mode) - .map_err(|_| Error::Sgx(sgx_status_t::SGX_ERROR_UNEXPECTED))?; - let fail_on_demand = Arc::new(Some(FailSlotOnDemand::new(fail_at, fail_mode))); - GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT.initialize(fail_on_demand); - } else { - GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT.initialize(Arc::new(None)); - } - Ok(()) } diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/common.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/common.rs index 7db3228214..dac07b9c9c 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/common.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/common.rs @@ -21,21 +21,13 @@ use crate::{ global_components::{ EnclaveExtrinsicsFactory, EnclaveNodeMetadataRepository, EnclaveOffchainWorkerExecutor, EnclaveParentchainSigner, EnclaveStfExecutor, EnclaveValidatorAccessor, - IntegriteeParentchainBlockImportDispatcher, IntegriteeParentchainBlockImportQueue, - IntegriteeParentchainBlockImporter, IntegriteeParentchainEventImportQueue, + IntegriteeParentchainBlockImportDispatcher, IntegriteeParentchainBlockImporter, IntegriteeParentchainImmediateBlockImportDispatcher, - IntegriteeParentchainIndirectCallsExecutor, - IntegriteeParentchainTriggeredBlockImportDispatcher, - TargetAParentchainBlockImportDispatcher, TargetAParentchainBlockImportQueue, - TargetAParentchainBlockImporter, TargetAParentchainEventImportQueue, - TargetAParentchainImmediateBlockImportDispatcher, - TargetAParentchainIndirectCallsExecutor, - TargetAParentchainTriggeredBlockImportDispatcher, - TargetBParentchainBlockImportDispatcher, TargetBParentchainBlockImportQueue, - TargetBParentchainBlockImporter, TargetBParentchainEventImportQueue, - TargetBParentchainImmediateBlockImportDispatcher, - TargetBParentchainIndirectCallsExecutor, - TargetBParentchainTriggeredBlockImportDispatcher, GLOBAL_OCALL_API_COMPONENT, + IntegriteeParentchainIndirectCallsExecutor, TargetAParentchainBlockImportDispatcher, + TargetAParentchainBlockImporter, TargetAParentchainImmediateBlockImportDispatcher, + TargetAParentchainIndirectCallsExecutor, TargetBParentchainBlockImportDispatcher, + TargetBParentchainBlockImporter, TargetBParentchainImmediateBlockImportDispatcher, + TargetBParentchainIndirectCallsExecutor, GLOBAL_OCALL_API_COMPONENT, GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT, GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, GLOBAL_STATE_HANDLER_COMPONENT, GLOBAL_STATE_OBSERVER_COMPONENT, GLOBAL_TOP_POOL_AUTHOR_COMPONENT, @@ -250,48 +242,3 @@ pub(crate) fn create_target_b_offchain_immediate_import_dispatcher( immediate_dispatcher, )))) } - -pub(crate) fn create_sidechain_triggered_import_dispatcher( - block_importer: IntegriteeParentchainBlockImporter, -) -> Arc { - let parentchain_block_import_queue = IntegriteeParentchainBlockImportQueue::default(); - let parentchain_event_import_queue = IntegriteeParentchainEventImportQueue::default(); - let triggered_dispatcher = IntegriteeParentchainTriggeredBlockImportDispatcher::new( - block_importer, - parentchain_block_import_queue, - parentchain_event_import_queue, - ); - Arc::new(IntegriteeParentchainBlockImportDispatcher::new_triggered_dispatcher(Arc::new( - triggered_dispatcher, - ))) -} - -pub(crate) fn create_sidechain_triggered_import_dispatcher_for_target_a( - block_importer: TargetAParentchainBlockImporter, -) -> Arc { - let parentchain_block_import_queue = TargetAParentchainBlockImportQueue::default(); - let parentchain_event_import_queue = TargetAParentchainEventImportQueue::default(); - let triggered_dispatcher = TargetAParentchainTriggeredBlockImportDispatcher::new( - block_importer, - parentchain_block_import_queue, - parentchain_event_import_queue, - ); - Arc::new(TargetAParentchainBlockImportDispatcher::new_triggered_dispatcher(Arc::new( - triggered_dispatcher, - ))) -} - -pub(crate) fn create_sidechain_triggered_import_dispatcher_for_target_b( - block_importer: TargetBParentchainBlockImporter, -) -> Arc { - let parentchain_block_import_queue = TargetBParentchainBlockImportQueue::default(); - let parentchain_event_import_queue = TargetBParentchainEventImportQueue::default(); - let triggered_dispatcher = TargetBParentchainTriggeredBlockImportDispatcher::new( - block_importer, - parentchain_block_import_queue, - parentchain_event_import_queue, - ); - Arc::new(TargetBParentchainBlockImportDispatcher::new_triggered_dispatcher(Arc::new( - triggered_dispatcher, - ))) -} diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs index aeeec12a5b..a15b8a6025 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_parachain.rs @@ -29,13 +29,11 @@ use crate::{ parentchain::common::{ create_extrinsics_factory, create_integritee_offchain_immediate_import_dispatcher, create_integritee_parentchain_block_importer, - create_sidechain_triggered_import_dispatcher, }, }, }; use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -52,10 +50,7 @@ pub struct IntegriteeParachainHandler { } impl IntegriteeParachainHandler { - pub fn init( - _base_path: PathBuf, - params: ParachainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: ParachainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let node_metadata_repository = Arc::new(EnclaveNodeMetadataRepository::default()); @@ -92,15 +87,12 @@ impl IntegriteeParachainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_integritee_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - }; + let import_dispatcher = create_integritee_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let parachain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs index 207115d47f..ee08abf4c9 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/integritee_solochain.rs @@ -29,13 +29,11 @@ use crate::{ parentchain::common::{ create_extrinsics_factory, create_integritee_offchain_immediate_import_dispatcher, create_integritee_parentchain_block_importer, - create_sidechain_triggered_import_dispatcher, }, }, }; use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -51,10 +49,7 @@ pub struct IntegriteeSolochainHandler { } impl IntegriteeSolochainHandler { - pub fn init( - _base_path: PathBuf, - params: SolochainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: SolochainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let light_client_seal = GLOBAL_INTEGRITEE_PARENTCHAIN_LIGHT_CLIENT_SEAL.get()?; @@ -91,15 +86,12 @@ impl IntegriteeSolochainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_integritee_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => create_sidechain_triggered_import_dispatcher(block_importer), - }; + let import_dispatcher = create_integritee_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let solochain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/mod.rs index b0045d6ca5..8e15dfcd80 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/mod.rs @@ -42,7 +42,6 @@ use itc_parentchain::{ primitives::{ParentchainId, ParentchainInitParams}, }; use itp_component_container::ComponentInitializer; -use itp_settings::worker_mode::ProvideWorkerMode; use std::{path::PathBuf, vec::Vec}; mod common; @@ -53,15 +52,14 @@ pub mod target_a_solochain; pub mod target_b_parachain; pub mod target_b_solochain; -pub(crate) fn init_parentchain_components( +pub(crate) fn init_parentchain_components( base_path: PathBuf, encoded_params: Vec, ) -> Result> { match ParentchainInitParams::decode(&mut encoded_params.as_slice())? { ParentchainInitParams::Parachain { id, params } => match id { ParentchainId::Litentry => { - let handler = - IntegriteeParachainHandler::init::(base_path, params)?; + let handler = IntegriteeParachainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; @@ -69,8 +67,7 @@ pub(crate) fn init_parentchain_components Ok(header.encode()) }, ParentchainId::TargetA => { - let handler = - TargetAParachainHandler::init::(base_path, params)?; + let handler = TargetAParachainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; @@ -78,8 +75,7 @@ pub(crate) fn init_parentchain_components Ok(header.encode()) }, ParentchainId::TargetB => { - let handler = - TargetBParachainHandler::init::(base_path, params)?; + let handler = TargetBParachainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; @@ -89,8 +85,7 @@ pub(crate) fn init_parentchain_components }, ParentchainInitParams::Solochain { id, params } => match id { ParentchainId::Litentry => { - let handler = - IntegriteeSolochainHandler::init::(base_path, params)?; + let handler = IntegriteeSolochainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; @@ -98,8 +93,7 @@ pub(crate) fn init_parentchain_components Ok(header.encode()) }, ParentchainId::TargetA => { - let handler = - TargetASolochainHandler::init::(base_path, params)?; + let handler = TargetASolochainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; @@ -107,8 +101,7 @@ pub(crate) fn init_parentchain_components Ok(header.encode()) }, ParentchainId::TargetB => { - let handler = - TargetBSolochainHandler::init::(base_path, params)?; + let handler = TargetBSolochainHandler::init(base_path, params)?; let header = handler .validator_accessor .execute_on_validator(|v| v.latest_finalized_header())?; diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs index e4a08cce6d..209ab9f70d 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_parachain.rs @@ -31,8 +31,7 @@ use crate::{ GLOBAL_TARGET_A_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TARGET_A_PARENTCHAIN_NONCE_CACHE, }, parentchain::common::{ - create_extrinsics_factory, create_sidechain_triggered_import_dispatcher_for_target_a, - create_target_a_offchain_immediate_import_dispatcher, + create_extrinsics_factory, create_target_a_offchain_immediate_import_dispatcher, create_target_a_parentchain_block_importer, }, }, @@ -40,7 +39,6 @@ use crate::{ use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; pub use itc_parentchain::primitives::{ParachainBlock, ParachainHeader, ParachainParams}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -55,10 +53,7 @@ pub struct TargetAParachainHandler { } impl TargetAParachainHandler { - pub fn init( - _base_path: PathBuf, - params: ParachainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: ParachainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let node_metadata_repository = Arc::new(EnclaveNodeMetadataRepository::default()); @@ -95,16 +90,12 @@ impl TargetAParachainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_target_a_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => - create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - }; + let import_dispatcher = create_target_a_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let parachain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs index e26ce6833d..5ce352bece 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_a_solochain.rs @@ -25,8 +25,7 @@ use crate::{ GLOBAL_TARGET_A_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TARGET_A_PARENTCHAIN_NONCE_CACHE, }, parentchain::common::{ - create_extrinsics_factory, create_sidechain_triggered_import_dispatcher_for_target_a, - create_target_a_offchain_immediate_import_dispatcher, + create_extrinsics_factory, create_target_a_offchain_immediate_import_dispatcher, create_target_a_parentchain_block_importer, }, }, @@ -34,7 +33,6 @@ use crate::{ use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; pub use itc_parentchain::primitives::{SolochainBlock, SolochainHeader, SolochainParams}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -48,10 +46,7 @@ pub struct TargetASolochainHandler { } impl TargetASolochainHandler { - pub fn init( - _base_path: PathBuf, - params: SolochainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: SolochainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let light_client_seal = GLOBAL_TARGET_A_PARENTCHAIN_LIGHT_CLIENT_SEAL.get()?; @@ -88,16 +83,12 @@ impl TargetASolochainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_target_a_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => - create_sidechain_triggered_import_dispatcher_for_target_a(block_importer), - }; + let import_dispatcher = create_target_a_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let solochain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs index 36d83a0e06..4bb1f2638e 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_parachain.rs @@ -31,8 +31,7 @@ use crate::{ GLOBAL_TARGET_B_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TARGET_B_PARENTCHAIN_NONCE_CACHE, }, parentchain::common::{ - create_extrinsics_factory, create_sidechain_triggered_import_dispatcher_for_target_b, - create_target_b_offchain_immediate_import_dispatcher, + create_extrinsics_factory, create_target_b_offchain_immediate_import_dispatcher, create_target_b_parentchain_block_importer, }, }, @@ -40,7 +39,6 @@ use crate::{ use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; pub use itc_parentchain::primitives::{ParachainBlock, ParachainHeader, ParachainParams}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -55,10 +53,7 @@ pub struct TargetBParachainHandler { } impl TargetBParachainHandler { - pub fn init( - _base_path: PathBuf, - params: ParachainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: ParachainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let node_metadata_repository = Arc::new(EnclaveNodeMetadataRepository::default()); @@ -95,16 +90,12 @@ impl TargetBParachainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_target_b_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => - create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - }; + let import_dispatcher = create_target_b_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let parachain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs index 015ff2cea6..40b51d76de 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/parentchain/target_b_solochain.rs @@ -25,8 +25,7 @@ use crate::{ GLOBAL_TARGET_B_PARENTCHAIN_LIGHT_CLIENT_SEAL, GLOBAL_TARGET_B_PARENTCHAIN_NONCE_CACHE, }, parentchain::common::{ - create_extrinsics_factory, create_sidechain_triggered_import_dispatcher_for_target_b, - create_target_b_offchain_immediate_import_dispatcher, + create_extrinsics_factory, create_target_b_offchain_immediate_import_dispatcher, create_target_b_parentchain_block_importer, }, }, @@ -34,7 +33,6 @@ use crate::{ use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, LightClientState}; pub use itc_parentchain::primitives::{SolochainBlock, SolochainHeader, SolochainParams}; use itp_component_container::ComponentGetter; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::parentchain::ParentchainId; use std::{path::PathBuf, sync::Arc}; @@ -48,10 +46,7 @@ pub struct TargetBSolochainHandler { } impl TargetBSolochainHandler { - pub fn init( - _base_path: PathBuf, - params: SolochainParams, - ) -> Result { + pub fn init(_base_path: PathBuf, params: SolochainParams) -> Result { let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; let light_client_seal = GLOBAL_TARGET_B_PARENTCHAIN_LIGHT_CLIENT_SEAL.get()?; @@ -88,16 +83,12 @@ impl TargetBSolochainHandler { node_metadata_repository.clone(), )?; - let import_dispatcher = match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => create_target_b_offchain_immediate_import_dispatcher( - stf_executor.clone(), - block_importer, - validator_accessor.clone(), - extrinsics_factory.clone(), - )?, - WorkerMode::Sidechain => - create_sidechain_triggered_import_dispatcher_for_target_b(block_importer), - }; + let import_dispatcher = create_target_b_offchain_immediate_import_dispatcher( + stf_executor.clone(), + block_importer, + validator_accessor.clone(), + extrinsics_factory.clone(), + )?; let solochain_handler = Self { genesis_header, diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index 3bcadef0b7..06bde8acfd 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -46,18 +46,16 @@ use crate::{ initialization::global_components::{ GLOBAL_INTEGRITEE_PARACHAIN_HANDLER_COMPONENT, GLOBAL_INTEGRITEE_PARENTCHAIN_NONCE_CACHE, GLOBAL_INTEGRITEE_SOLOCHAIN_HANDLER_COMPONENT, GLOBAL_SHIELDING_KEY_REPOSITORY_COMPONENT, - GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT, GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, - GLOBAL_STATE_HANDLER_COMPONENT, GLOBAL_TARGET_A_PARACHAIN_HANDLER_COMPONENT, - GLOBAL_TARGET_A_PARENTCHAIN_NONCE_CACHE, GLOBAL_TARGET_A_SOLOCHAIN_HANDLER_COMPONENT, - GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_B_PARENTCHAIN_NONCE_CACHE, - GLOBAL_TARGET_B_SOLOCHAIN_HANDLER_COMPONENT, + GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, GLOBAL_STATE_HANDLER_COMPONENT, + GLOBAL_TARGET_A_PARACHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_A_PARENTCHAIN_NONCE_CACHE, + GLOBAL_TARGET_A_SOLOCHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT, + GLOBAL_TARGET_B_PARENTCHAIN_NONCE_CACHE, GLOBAL_TARGET_B_SOLOCHAIN_HANDLER_COMPONENT, }, - rpc::worker_api_direct::sidechain_io_handler, utils::{ get_node_metadata_repository_from_integritee_solo_or_parachain, get_node_metadata_repository_from_target_a_solo_or_parachain, get_node_metadata_repository_from_target_b_solo_or_parachain, - get_validator_accessor_from_integritee_solo_or_parachain, utf8_str_from_raw, DecodeRaw, + get_validator_accessor_from_integritee_solo_or_parachain, DecodeRaw, }, }; use codec::Decode; @@ -68,10 +66,8 @@ use itc_parentchain::{ primitives::ParentchainId, }; use itp_component_container::ComponentGetter; -use itp_import_queue::PushToQueue; use itp_node_api::metadata::NodeMetadata; use itp_nonce_cache::{MutateNonce, Nonce}; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerModeProvider}; use itp_sgx_crypto::key_repository::AccessPubkey; use itp_storage::{StorageProof, StorageProofChecker}; use itp_types::{ShardIdentifier, SignedBlock}; @@ -102,7 +98,6 @@ mod sync; #[cfg(feature = "test")] pub mod test; mod tls_ra; -pub mod top_pool_execution; pub type Hash = sp_core::H256; pub type AuthorityPair = sp_core::ed25519::Pair; @@ -321,52 +316,6 @@ pub unsafe extern "C" fn set_node_metadata( sgx_status_t::SGX_SUCCESS } -/// This is reduced to the sidechain block import RPC interface (i.e. worker-worker communication). -/// The entire rest of the RPC server is run inside the enclave and does not use this e-call function anymore. -#[no_mangle] -pub unsafe extern "C" fn call_rpc_methods( - request: *const u8, - request_len: u32, - response: *mut u8, - response_len: u32, -) -> sgx_status_t { - let request = match utf8_str_from_raw(request, request_len as usize) { - Ok(req) => req, - Err(e) => { - error!("[SidechainRpc] FFI: Invalid utf8 request: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let res = match sidechain_rpc_int(request) { - Ok(res) => res, - Err(e) => { - error!("RPC request failed: {:?}", e); - return e.into() - }, - }; - - let response_slice = slice::from_raw_parts_mut(response, response_len as usize); - if let Err(e) = write_slice_and_whitespace_pad(response_slice, res.into_bytes()) { - return Error::BufferError(e).into() - }; - - sgx_status_t::SGX_SUCCESS -} - -fn sidechain_rpc_int(request: &str) -> Result { - let sidechain_block_import_queue = GLOBAL_SIDECHAIN_IMPORT_QUEUE_COMPONENT.get()?; - - let io = sidechain_io_handler(move |signed_block| { - sidechain_block_import_queue.push_single(signed_block) - }); - - // note: errors are still returned as Option - Ok(io - .handle_request_sync(request) - .unwrap_or_else(|| format!("Empty rpc response for request: {}", request))) -} - #[no_mangle] pub unsafe extern "C" fn publish_wallets() -> sgx_status_t { if let Err(e) = initialization::publish_wallets() { @@ -383,27 +332,8 @@ pub unsafe extern "C" fn publish_wallets() -> sgx_status_t { /// (parentchain components) have been initialized (because we need the parentchain /// block import dispatcher). #[no_mangle] -pub unsafe extern "C" fn init_enclave_sidechain_components( - fail_mode: *const u8, - fail_mode_size: u32, - fail_at: *const u8, - fail_at_size: u32, -) -> sgx_status_t { - let fail_mode = match Option::::decode_raw(fail_mode, fail_mode_size as usize) { - Ok(s) => s, - Err(e) => { - error!("failed to decode fail mode {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - let fail_at = match u64::decode_raw(fail_at, fail_at_size as usize) { - Ok(v) => v, - Err(e) => { - error!("failed to decode fail at {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - if let Err(e) = initialization::init_enclave_sidechain_components(fail_mode, fail_at) { +pub unsafe extern "C" fn init_enclave_sidechain_components() -> sgx_status_t { + if let Err(e) = initialization::init_enclave_sidechain_components() { error!("Failed to initialize sidechain components: {:?}", e); return sgx_status_t::SGX_ERROR_UNEXPECTED } @@ -461,8 +391,7 @@ pub unsafe extern "C" fn init_parentchain_components( fn init_parentchain_params_internal(params: Vec, latest_header: &mut [u8]) -> Result<()> { use initialization::parentchain::init_parentchain_components; - let encoded_latest_header = - init_parentchain_components::(get_base_path()?, params)?; + let encoded_latest_header = init_parentchain_components(get_base_path()?, params)?; write_slice_and_whitespace_pad(latest_header, encoded_latest_header)?; @@ -557,7 +486,7 @@ unsafe fn sync_parentchain_internal( let events_to_sync = Vec::>::decode_raw(events_to_sync, events_to_sync_size)?; - dispatch_parentchain_blocks_for_import::( + dispatch_parentchain_blocks_for_import( blocks_to_sync, events_to_sync, &parentchain_id, @@ -590,7 +519,7 @@ pub unsafe extern "C" fn ignore_parentchain_block_import_validation_until( /// * The sidechain uses a triggered dispatcher, where the import of a parentchain block is /// synchronized and triggered by the sidechain block production cycle. /// -fn dispatch_parentchain_blocks_for_import( +fn dispatch_parentchain_blocks_for_import( blocks_to_sync: Vec, events_to_sync: Vec>, id: &ParentchainId, diff --git a/bitacross-worker/enclave-runtime/src/ocall/ffi.rs b/bitacross-worker/enclave-runtime/src/ocall/ffi.rs index 388cc0c54a..1b3b1adec2 100644 --- a/bitacross-worker/enclave-runtime/src/ocall/ffi.rs +++ b/bitacross-worker/enclave-runtime/src/ocall/ffi.rs @@ -83,36 +83,6 @@ extern "C" { metric_size: u32, ) -> sgx_status_t; - pub fn ocall_propose_sidechain_blocks( - ret_val: *mut sgx_status_t, - signed_blocks: *const u8, - signed_blocks_size: u32, - ) -> sgx_status_t; - - pub fn ocall_store_sidechain_blocks( - ret_val: *mut sgx_status_t, - signed_blocks: *const u8, - signed_blocks_size: u32, - ) -> sgx_status_t; - - pub fn ocall_fetch_sidechain_blocks_from_peer( - ret_val: *mut sgx_status_t, - last_imported_block_hash: *const u8, - last_imported_block_hash_size: u32, - maybe_until_block_hash: *const u8, - maybe_until_block_hash_encoded_size: u32, - shard_identifier: *const u8, - shard_identifier_size: u32, - sidechain_blocks: *mut u8, - sidechain_blocks_size: u32, - ) -> sgx_status_t; - - pub fn ocall_get_trusted_peers_urls( - ret_val: *mut sgx_status_t, - peers: *mut u8, - peers_size: u32, - ) -> sgx_status_t; - pub fn ocall_send_to_parentchain( ret_val: *mut sgx_status_t, extrinsics: *const u8, diff --git a/bitacross-worker/enclave-runtime/src/ocall/mod.rs b/bitacross-worker/enclave-runtime/src/ocall/mod.rs index 7374b63fde..f85d82d8b3 100644 --- a/bitacross-worker/enclave-runtime/src/ocall/mod.rs +++ b/bitacross-worker/enclave-runtime/src/ocall/mod.rs @@ -20,7 +20,6 @@ mod ffi; mod ipfs_ocall; mod metrics_ocall; mod on_chain_ocall; -mod sidechain_ocall; #[derive(Clone, Debug, Default)] pub struct OcallApi; diff --git a/bitacross-worker/enclave-runtime/src/ocall/sidechain_ocall.rs b/bitacross-worker/enclave-runtime/src/ocall/sidechain_ocall.rs deleted file mode 100644 index b961e93752..0000000000 --- a/bitacross-worker/enclave-runtime/src/ocall/sidechain_ocall.rs +++ /dev/null @@ -1,139 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::ocall::{ffi, OcallApi}; -use codec::{Decode, Encode}; -use frame_support::ensure; -use itp_ocall_api::EnclaveSidechainOCallApi; -use itp_types::{BlockHash, ShardIdentifier}; -use log::*; -use sgx_types::{sgx_status_t, SgxResult}; -use std::{string::String, vec::Vec}; - -impl EnclaveSidechainOCallApi for OcallApi { - fn propose_sidechain_blocks( - &self, - signed_blocks: Vec, - ) -> SgxResult<()> { - let mut rt: sgx_status_t = sgx_status_t::SGX_ERROR_UNEXPECTED; - let signed_blocks_encoded = signed_blocks.encode(); - - let res = unsafe { - ffi::ocall_propose_sidechain_blocks( - &mut rt as *mut sgx_status_t, - signed_blocks_encoded.as_ptr(), - signed_blocks_encoded.len() as u32, - ) - }; - - ensure!(rt == sgx_status_t::SGX_SUCCESS, rt); - ensure!(res == sgx_status_t::SGX_SUCCESS, res); - - Ok(()) - } - - fn store_sidechain_blocks( - &self, - signed_blocks: Vec, - ) -> SgxResult<()> { - let mut rt: sgx_status_t = sgx_status_t::SGX_ERROR_UNEXPECTED; - let signed_blocks_encoded = signed_blocks.encode(); - - let res = unsafe { - ffi::ocall_store_sidechain_blocks( - &mut rt as *mut sgx_status_t, - signed_blocks_encoded.as_ptr(), - signed_blocks_encoded.len() as u32, - ) - }; - - ensure!(rt == sgx_status_t::SGX_SUCCESS, rt); - ensure!(res == sgx_status_t::SGX_SUCCESS, res); - - Ok(()) - } - - fn fetch_sidechain_blocks_from_peer( - &self, - last_imported_block_hash: BlockHash, - maybe_until_block_hash: Option, - shard_identifier: ShardIdentifier, - ) -> SgxResult> { - const BLOCK_BUFFER_SIZE: usize = 262144; // Buffer size for sidechain blocks in bytes (256KB). - - let mut rt: sgx_status_t = sgx_status_t::SGX_ERROR_UNEXPECTED; - let last_imported_block_hash_encoded = last_imported_block_hash.encode(); - let maybe_until_block_hash_encoded = maybe_until_block_hash.encode(); - let shard_identifier_encoded = shard_identifier.encode(); - - // We have to pre-allocate the vector and hope it's large enough (see GitHub issue #621). - let mut signed_blocks_encoded: Vec = vec![0; BLOCK_BUFFER_SIZE]; - - let res = unsafe { - ffi::ocall_fetch_sidechain_blocks_from_peer( - &mut rt as *mut sgx_status_t, - last_imported_block_hash_encoded.as_ptr(), - last_imported_block_hash_encoded.len() as u32, - maybe_until_block_hash_encoded.as_ptr(), - maybe_until_block_hash_encoded.len() as u32, - shard_identifier_encoded.as_ptr(), - shard_identifier_encoded.len() as u32, - signed_blocks_encoded.as_mut_ptr(), - signed_blocks_encoded.len() as u32, - ) - }; - - ensure!(rt == sgx_status_t::SGX_SUCCESS, rt); - ensure!(res == sgx_status_t::SGX_SUCCESS, res); - - let decoded_signed_blocks: Vec = - Decode::decode(&mut signed_blocks_encoded.as_slice()).map_err(|e| { - error!("Failed to decode WorkerResponse: {}", e); - sgx_status_t::SGX_ERROR_UNEXPECTED - })?; - - Ok(decoded_signed_blocks) - } - - fn get_trusted_peers_urls(&self) -> SgxResult> { - let mut rt: sgx_status_t = sgx_status_t::SGX_ERROR_UNEXPECTED; - const BLOCK_BUFFER_SIZE: usize = 262144; // Buffer size for sidechain blocks in bytes (256KB). - - // We have to pre-allocate the vector and hope it's large enough (see GitHub issue #621). - let mut peers_encoded: Vec = vec![0; BLOCK_BUFFER_SIZE]; - - let res = unsafe { - ffi::ocall_get_trusted_peers_urls( - &mut rt as *mut sgx_status_t, - peers_encoded.as_mut_ptr(), - peers_encoded.len() as u32, - ) - }; - - ensure!(rt == sgx_status_t::SGX_SUCCESS, rt); - ensure!(res == sgx_status_t::SGX_SUCCESS, res); - - let decoded_peers: Vec = - Decode::decode(&mut peers_encoded.as_slice()).map_err(|e| { - error!("Failed to decode peers list: {}", e); - sgx_status_t::SGX_ERROR_UNEXPECTED - })?; - - Ok(decoded_peers) - } -} diff --git a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs index e02f775f0d..009a19786d 100644 --- a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs +++ b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs @@ -20,14 +20,17 @@ use crate::{ generate_dcap_ra_extrinsic_from_quote_internal, generate_ias_ra_extrinsic_from_der_cert_internal, }, + std::string::ToString, utils::{ get_stf_enclave_signer_from_solo_or_parachain, get_validator_accessor_from_integritee_solo_or_parachain, }, }; +use bc_task_sender::{BitAcrossRequest, BitAcrossRequestSender}; use codec::Encode; use core::result::Result; -use ita_sgx_runtime::{Runtime, System}; +use futures_sgx::channel::oneshot; +use ita_sgx_runtime::Runtime; use ita_stf::{Getter, TrustedCallSigned}; use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, ExtrinsicSender}; use itp_primitives_cache::{GetPrimitives, GLOBAL_PRIMITIVES_CACHE}; @@ -37,21 +40,14 @@ use itp_sgx_crypto::{ key_repository::{AccessKey, AccessPubkey}, ShieldingCryptoDecrypt, ShieldingCryptoEncrypt, }; -use itp_sgx_externalities::SgxExternalitiesTrait; use itp_stf_executor::{getter_executor::ExecuteGetter, traits::StfShardVaultQuery}; -use itp_stf_primitives::types::AccountId; -use itp_stf_state_handler::handle_state::HandleState; use itp_top_pool_author::traits::AuthorApi; -use itp_types::{DirectRequestStatus, Index, RsaRequest, ShardIdentifier, H256}; +use itp_types::{DirectRequestStatus, RsaRequest, ShardIdentifier, H256}; use itp_utils::{FromHexPrefixed, ToHexPrefixed}; -use its_primitives::types::block::SignedBlock; -use its_sidechain::rpc_handler::{ - direct_top_pool_api, direct_top_pool_api::decode_shard_from_base58, import_block_api, -}; use jsonrpc_core::{serde_json::json, IoHandler, Params, Value}; use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; use litentry_macros::if_not_production; -use litentry_primitives::DecryptableRequest; +use litentry_primitives::{AesRequest, DecryptableRequest}; use log::debug; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; use sp_core::Pair; @@ -72,11 +68,10 @@ fn get_all_rpc_methods_string(io_handler: &IoHandler) -> String { format!("methods: [{}]", method_string) } -pub fn public_api_rpc_handler( +pub fn public_api_rpc_handler( top_pool_author: Arc, getter_executor: Arc, shielding_key: Arc, - state: Option>, ) -> IoHandler where Author: AuthorApi + Send + Sync + 'static, @@ -84,13 +79,8 @@ where AccessShieldingKey: AccessPubkey + AccessKey + Send + Sync + 'static, ::KeyType: ShieldingCryptoDecrypt + ShieldingCryptoEncrypt + DeriveEd25519 + Send + Sync + 'static, - State: HandleState + Send + Sync + 'static, - State::StateT: SgxExternalitiesTrait, { - let mut io = direct_top_pool_api::add_top_pool_direct_rpc_methods( - top_pool_author.clone(), - IoHandler::new(), - ); + let mut io = IoHandler::new(); let shielding_key_cloned = shielding_key.clone(); io.add_sync_method("author_getShieldingKey", move |_: Params| { @@ -140,64 +130,15 @@ where Ok(json!(json_value.to_hex())) }); - let local_top_pool_author = top_pool_author.clone(); - let local_state = state.clone(); - io.add_sync_method("author_getNextNonce", move |params: Params| { - let local_state = match local_state.clone() { - Some(s) => s, - None => - return Ok(json!(compute_hex_encoded_return_error( - "author_getNextNonce is not avaiable" - ))), - }; - - match params.parse::<(String, String)>() { - Ok((shard_base58, account_hex)) => { - let shard = match decode_shard_from_base58(shard_base58.as_str()) { - Ok(id) => id, - Err(msg) => { - let error_msg: String = - format!("Could not retrieve author_getNextNonce calls due to: {}", msg); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - let account = match AccountId::from_hex(account_hex.as_str()) { - Ok(acc) => acc, - Err(msg) => { - let error_msg: String = format!( - "Could not retrieve author_getNextNonce calls due to: {:?}", - msg - ); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - - match local_state.load_cloned(&shard) { - Ok((mut state, _hash)) => { - let trusted_calls = - local_top_pool_author.get_pending_trusted_calls_for(shard, &account); - let pending_tx_count = trusted_calls.len(); - #[allow(clippy::unwrap_used)] - let pending_tx_count = Index::try_from(pending_tx_count).unwrap(); - let nonce = state.execute_with(|| System::account_nonce(&account)); - let json_value = RpcReturnValue { - do_watch: false, - value: (nonce.saturating_add(pending_tx_count)).encode(), - status: DirectRequestStatus::Ok, - }; - Ok(json!(json_value.to_hex())) - }, - Err(e) => { - let error_msg = format!("load shard failure due to: {:?}", e); - Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - } - }, - Err(e) => { - let error_msg: String = - format!("Could not retrieve author_getNextNonce calls due to: {}", e); - Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, + // Submit BitAcross Request + io.add_method("bitacross_submitRequest", move |params: Params| { + debug!("worker_api_direct rpc was called: bitacross_submitRequest"); + async move { + let json_value = match request_bit_across_inner(params).await { + Ok(value) => value.to_hex(), + Err(error) => compute_hex_encoded_return_error(&error), + }; + Ok(json!(json_value)) } }); @@ -375,56 +316,6 @@ where Err(_) => Ok(json!(compute_hex_encoded_return_error("parse error"))), } }); - - // state_getStorage - io.add_sync_method("state_getStorage", move |params: Params| { - let local_state = match state.clone() { - Some(s) => s, - None => - return Ok(json!(compute_hex_encoded_return_error( - "state_getStorage is not avaiable" - ))), - }; - match params.parse::<(String, String)>() { - Ok((shard_str, key_hash)) => { - let key_hash = if key_hash.starts_with("0x") { - #[allow(clippy::unwrap_used)] - key_hash.strip_prefix("0x").unwrap() - } else { - key_hash.as_str() - }; - let key_hash = match hex::decode(key_hash) { - Ok(key_hash) => key_hash, - Err(_) => - return Ok(json!(compute_hex_encoded_return_error("docode key error"))), - }; - - let shard: ShardIdentifier = match decode_shard_from_base58(shard_str.as_str()) - { - Ok(id) => id, - Err(msg) => { - let error_msg = format!("decode shard failure due to: {}", msg); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - match local_state.load_cloned(&shard) { - Ok((state, _)) => { - // Get storage by key hash - let value = state.get(key_hash.as_slice()).cloned().unwrap_or_default(); - debug!("query storage value:{:?}", &value); - let json_value = - RpcReturnValue::new(value, false, DirectRequestStatus::Ok); - Ok(json!(json_value.to_hex())) - }, - Err(e) => { - let error_msg = format!("load shard failure due to: {:?}", e); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - } - }, - Err(_err) => Ok(json!(compute_hex_encoded_return_error("parse error"))), - } - }); }); // system_health @@ -541,13 +432,46 @@ fn attesteer_forward_ias_attestation_report_inner( Ok(ext) } -pub fn sidechain_io_handler(import_fn: ImportFn) -> IoHandler +async fn request_bit_across_inner(params: Params) -> Result { + let payload = get_request_payload(params)?; + let request = AesRequest::from_hex(&payload) + .map_err(|e| format!("AesRequest construction error: {:?}", e))?; + + let bit_across_request_sender = BitAcrossRequestSender::new(); + let (sender, receiver) = oneshot::channel::, String>>(); + + bit_across_request_sender.send(BitAcrossRequest { sender, request })?; + + // we only expect one response, hence no loop + match receiver.await { + Ok(Ok(response)) => + Ok(RpcReturnValue { do_watch: false, value: response, status: DirectRequestStatus::Ok }), + Ok(Err(e)) => { + log::error!("Received error in jsonresponse: {:?} ", e); + Err(compute_hex_encoded_return_error(&e)) + }, + Err(_) => { + // This case will only happen if the sender has been dropped + Err(compute_hex_encoded_return_error("The sender has been dropped")) + }, + } +} + +// we expect our `params` to be "by-position array" +// see https://www.jsonrpc.org/specification#parameter_structures +fn get_request_payload(params: Params) -> Result { + let s_vec = params.parse::>().map_err(|e| format!("{}", e))?; + + let s = s_vec.get(0).ok_or_else(|| "Empty params".to_string())?; + debug!("Request payload: {}", s); + Ok(s.to_owned()) +} + +pub fn sidechain_io_handler() -> IoHandler where - ImportFn: Fn(SignedBlock) -> Result<(), Error> + Sync + Send + 'static, Error: std::fmt::Debug, { - let io = IoHandler::new(); - import_block_api::add_import_block_rpc_method(import_fn, io) + IoHandler::new() } #[cfg(feature = "test")] diff --git a/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs b/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs index bcb7ba5d45..6a88de3a7b 100644 --- a/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs @@ -29,7 +29,6 @@ use itp_sgx_crypto::get_rsa3072_repository; use itp_sgx_temp_dir::TempDir; use itp_stf_executor::{getter_executor::GetterExecutor, mocks::GetStateMock}; use itp_stf_state_observer::mock::ObserveStateMock; -use itp_test::mock::handle_state_mock::HandleStateMock; use itp_top_pool_author::mocks::AuthorApiMock; use itp_types::{DirectRequestStatus, RsaRequest, ShardIdentifier}; use itp_utils::{FromHexPrefixed, ToHexPrefixed}; @@ -51,12 +50,8 @@ pub fn get_state_request_works() { Arc::new(GetterExecutor::<_, GetStateMock, Getter>::new(state_observer)); let top_pool_author = Arc::new(AuthorApiMock::default()); - let io_handler = public_api_rpc_handler( - top_pool_author, - getter_executor, - Arc::new(rsa_repository), - None::>, - ); + let io_handler = + public_api_rpc_handler(top_pool_author, getter_executor, Arc::new(rsa_repository)); let rpc_handler = Arc::new(RpcWsHandler::new(io_handler, watch_extractor, connection_registry)); let getter = diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/enclave_rpc_ocall_mock.rs b/bitacross-worker/enclave-runtime/src/test/mocks/enclave_rpc_ocall_mock.rs deleted file mode 100644 index 23003989e8..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/mocks/enclave_rpc_ocall_mock.rs +++ /dev/null @@ -1,40 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use codec::Encode; -use itp_ocall_api::EnclaveRpcOCallApi; -use itp_types::TrustedOperationStatus; -use sgx_types::SgxResult; -use std::vec::Vec; - -#[derive(Clone, Debug, Default)] -pub struct EnclaveRpcOCallMock; - -impl EnclaveRpcOCallApi for EnclaveRpcOCallMock { - fn update_status_event( - &self, - _hash: H, - _status_update: TrustedOperationStatus, - ) -> SgxResult<()> { - Ok(()) - } - - fn send_state(&self, _hash: H, _value_opt: Option>) -> SgxResult<()> { - Ok(()) - } -} diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/mod.rs b/bitacross-worker/enclave-runtime/src/test/mocks/mod.rs index 26551844d6..a6079c29eb 100644 --- a/bitacross-worker/enclave-runtime/src/test/mocks/mod.rs +++ b/bitacross-worker/enclave-runtime/src/test/mocks/mod.rs @@ -17,8 +17,5 @@ */ pub mod attestation_ocall_mock; -pub mod enclave_rpc_ocall_mock; -pub mod peer_updater_mock; -pub mod propose_to_import_call_mock; pub mod rpc_responder_mock; pub mod types; diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs b/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs deleted file mode 100644 index 2fe4526c43..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/mocks/peer_updater_mock.rs +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use itc_peer_top_broadcaster::PeerUpdater; -use sgx_tstd::{string::String, vec::Vec}; - -pub struct PeerUpdaterMock {} - -impl PeerUpdater for PeerUpdaterMock { - fn update(&self, _peers: Vec) {} -} diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/propose_to_import_call_mock.rs b/bitacross-worker/enclave-runtime/src/test/mocks/propose_to_import_call_mock.rs deleted file mode 100644 index fa47ae9539..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/mocks/propose_to_import_call_mock.rs +++ /dev/null @@ -1,137 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::test::mocks::types::TestBlockImporter; -use codec::{Decode, Encode}; -use itc_parentchain::primitives::ParentchainId; -use itp_ocall_api::{ - EnclaveMetricsOCallApi, EnclaveOnChainOCallApi, EnclaveSidechainOCallApi, Result, -}; -use itp_types::{ - storage::StorageEntryVerified, BlockHash, Header as ParentchainHeader, ShardIdentifier, - WorkerRequest, WorkerResponse, H256, -}; -use its_primitives::types::block::SignedBlock as SignedSidechainBlockType; -use its_sidechain::consensus_common::BlockImport; -use sgx_types::SgxResult; -use sp_runtime::{traits::Header as ParentchainHeaderTrait, OpaqueExtrinsic}; -use std::{string::String, sync::Arc, vec::Vec}; - -/// OCallApi mock that routes the proposed sidechain blocks directly to the importer, -/// short circuiting all the RPC calls. -#[derive(Clone)] -pub struct ProposeToImportOCallApi { - parentchain_header: ParentchainHeader, - block_importer: Arc, -} - -impl ProposeToImportOCallApi { - pub fn new( - parentchain_header: ParentchainHeader, - block_importer: Arc, - ) -> Self { - ProposeToImportOCallApi { parentchain_header, block_importer } - } -} - -impl EnclaveOnChainOCallApi for ProposeToImportOCallApi { - fn send_to_parentchain( - &self, - _extrinsics: Vec, - _: &ParentchainId, - _: bool, - ) -> SgxResult<()> { - Ok(()) - } - - fn worker_request( - &self, - _req: Vec, - _: &ParentchainId, - ) -> SgxResult>> { - todo!() - } - - fn get_storage_verified, V: Decode>( - &self, - _storage_hash: Vec, - _header: &H, - _: &ParentchainId, - ) -> Result> { - todo!() - } - - fn get_multiple_storages_verified, V: Decode>( - &self, - _storage_hashes: Vec>, - _header: &H, - _: &ParentchainId, - ) -> Result>> { - todo!() - } - - fn get_storage_keys(&self, _key_prefix: Vec) -> Result>> { - todo!() - } -} - -impl EnclaveSidechainOCallApi for ProposeToImportOCallApi { - fn propose_sidechain_blocks( - &self, - signed_blocks: Vec, - ) -> SgxResult<()> { - let decoded_signed_blocks: Vec = signed_blocks - .iter() - .map(|sb| sb.encode()) - .map(|e| SignedSidechainBlockType::decode(&mut e.as_slice()).unwrap()) - .collect(); - - for signed_block in decoded_signed_blocks { - self.block_importer - .import_block(signed_block, &self.parentchain_header) - .unwrap(); - } - Ok(()) - } - - fn store_sidechain_blocks( - &self, - _signed_blocks: Vec, - ) -> SgxResult<()> { - Ok(()) - } - - fn fetch_sidechain_blocks_from_peer( - &self, - _last_imported_block_hash: BlockHash, - _maybe_until_block_hash: Option, - _shard_identifier: ShardIdentifier, - ) -> SgxResult> { - Ok(Vec::new()) - } - - fn get_trusted_peers_urls(&self) -> SgxResult> { - Ok(vec![]) - } -} - -impl EnclaveMetricsOCallApi for ProposeToImportOCallApi { - fn update_metric(&self, _metric: Metric) -> SgxResult<()> { - Ok(()) - } -} diff --git a/bitacross-worker/enclave-runtime/src/test/mocks/types.rs b/bitacross-worker/enclave-runtime/src/test/mocks/types.rs index ae939c53e4..4c9500eb5a 100644 --- a/bitacross-worker/enclave-runtime/src/test/mocks/types.rs +++ b/bitacross-worker/enclave-runtime/src/test/mocks/types.rs @@ -18,9 +18,7 @@ //! Type definitions for testing. Includes various mocks. -use crate::test::mocks::{ - peer_updater_mock::PeerUpdaterMock, rpc_responder_mock::RpcResponderMock, -}; +use crate::test::mocks::rpc_responder_mock::RpcResponderMock; use ita_sgx_runtime::Runtime; use ita_stf::{Getter, Stf, TrustedCallSigned}; use itc_parentchain::block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; @@ -40,8 +38,6 @@ use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, }; use itp_types::{Block as ParentchainBlock, SignedBlock as SignedParentchainBlock}; -use its_primitives::types::SignedBlock as SignedSidechainBlock; -use its_sidechain::{aura::block_importer::BlockImporter, block_composer::BlockComposer}; use primitive_types::H256; use sgx_crypto_helper::rsa3072::Rsa3072KeyPair; use sp_core::ed25519 as spEd25519; @@ -95,20 +91,3 @@ pub type TestTopPoolAuthor = Author< TrustedCallSigned, Getter, >; - -pub type TestBlockComposer = - BlockComposer; - -pub type TestBlockImporter = BlockImporter< - TestSigner, - ParentchainBlock, - SignedSidechainBlock, - TestOCallApi, - HandleStateMock, - TestStateKeyRepo, - TestTopPoolAuthor, - TestParentchainBlockImportTrigger, - PeerUpdaterMock, - TrustedCallSigned, - Getter, ->; diff --git a/bitacross-worker/enclave-runtime/src/test/mod.rs b/bitacross-worker/enclave-runtime/src/test/mod.rs index b3a25415a3..2e341e5064 100644 --- a/bitacross-worker/enclave-runtime/src/test/mod.rs +++ b/bitacross-worker/enclave-runtime/src/test/mod.rs @@ -24,8 +24,6 @@ pub mod evm_pallet_tests; pub mod fixtures; pub mod ipfs_tests; pub mod mocks; -pub mod sidechain_aura_tests; -pub mod sidechain_event_tests; mod state_getter_tests; pub mod tests_main; pub mod top_pool_tests; diff --git a/bitacross-worker/enclave-runtime/src/test/sidechain_aura_tests.rs b/bitacross-worker/enclave-runtime/src/test/sidechain_aura_tests.rs deleted file mode 100644 index 36ad0f69b9..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/sidechain_aura_tests.rs +++ /dev/null @@ -1,287 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::{ - test::{ - fixtures::{ - components::{ - create_ocall_api, create_top_pool, encrypt_trusted_operation, sign_trusted_call, - }, - initialize_test_state::init_state, - test_setup::{enclave_call_signer, TestStf}, - }, - mocks::{ - peer_updater_mock::PeerUpdaterMock, - propose_to_import_call_mock::ProposeToImportOCallApi, types::*, - }, - }, - top_pool_execution::{exec_aura_on_slot, send_blocks_and_extrinsics}, -}; -use codec::Decode; -use ita_stf::{ - test_genesis::{endowed_account, second_endowed_account, unendowed_account}, - Balance, Getter, TrustedCall, TrustedCallSigned, -}; -use itc_parentchain_test::ParentchainHeaderBuilder; -use itp_node_api::metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}; -use itp_ocall_api::EnclaveAttestationOCallApi; -use itp_settings::{ - sidechain::SLOT_DURATION, - worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}, -}; -use itp_sgx_crypto::{Aes, ShieldingCryptoEncrypt, StateCrypto}; -use itp_sgx_externalities::SgxExternalitiesDiffType; -use itp_stf_interface::system_pallet::{SystemPalletAccountInterface, SystemPalletEventInterface}; -use itp_stf_primitives::types::{StatePayload, TrustedOperation}; -use itp_stf_state_handler::handle_state::HandleState; -use itp_test::mock::{handle_state_mock::HandleStateMock, metrics_ocall_mock::MetricsOCallMock}; -use itp_time_utils::duration_now; -use itp_top_pool_author::{ - top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, - traits::AuthorApi, -}; -use itp_types::{AccountId, Block as ParentchainBlock, RsaRequest, ShardIdentifier}; -use its_block_verification::slot::slot_from_timestamp_and_duration; -use its_primitives::{traits::Block, types::SignedBlock as SignedSidechainBlock}; -use its_sidechain::{aura::proposer_factory::ProposerFactory, slots::SlotInfo}; -use jsonrpc_core::futures::executor; -use lc_scheduled_enclave::ScheduledEnclaveMock; -use litentry_primitives::Identity; -use log::*; -use primitive_types::H256; -use sgx_crypto_helper::RsaKeyPair; -use sp_core::{ed25519, Pair}; -use std::{sync::Arc, vec, vec::Vec}; - -/// Integration test for sidechain block production and block import. -/// (requires Sidechain mode) -/// -/// - Create trusted calls and add them to the TOP pool. -/// - Run AURA on a valid and claimed slot, which executes the trusted operations and produces a new block. -/// - Import the new sidechain block, which updates the state. -pub fn produce_sidechain_block_and_import_it() { - // Test can only be run in Sidechain mode - if WorkerModeProvider::worker_mode() != WorkerMode::Sidechain { - info!("Ignoring sidechain block production test: Not in sidechain mode"); - return - } - - let _ = env_logger::builder().is_test(true).try_init(); - info!("Setting up test."); - - let signer = TestSigner::from_seed(b"42315678901234567890123456789012"); - let shielding_key = TestShieldingKey::new().unwrap(); - let state_key = TestStateKey::new([3u8; 16], [1u8; 16]); - let shielding_key_repo = Arc::new(TestShieldingKeyRepo::new(shielding_key)); - let state_key_repo = Arc::new(TestStateKeyRepo::new(state_key)); - let parentchain_header = ParentchainHeaderBuilder::default().build(); - - let ocall_api = create_ocall_api(&parentchain_header, &signer); - - info!("Initializing state and shard.."); - let state_handler = Arc::new(TestStateHandler::default()); - let enclave_call_signer = enclave_call_signer(&shielding_key); - let (_, shard_id) = init_state(state_handler.as_ref(), enclave_call_signer.public().into()); - let shards = vec![shard_id]; - - let node_metadata_repo = Arc::new(NodeMetadataRepository::new(NodeMetadataMock::new())); - let stf_executor = Arc::new(TestStfExecutor::new( - ocall_api.clone(), - state_handler.clone(), - node_metadata_repo, - )); - let top_pool = create_top_pool(); - - let (sender, _receiver) = std::sync::mpsc::sync_channel(1000); - - let metrics_ocall_mock = Arc::new(MetricsOCallMock::default()); - - let top_pool_author = Arc::new(TestTopPoolAuthor::new( - top_pool, - AllowAllTopsFilter::::new(), - DirectCallsOnlyFilter::::new(), - state_handler.clone(), - shielding_key_repo, - metrics_ocall_mock.clone(), - Arc::new(sender), - )); - let parentchain_block_import_trigger = Arc::new(TestParentchainBlockImportTrigger::default()); - let peer_updater_mock = Arc::new(PeerUpdaterMock {}); - let block_importer = Arc::new(TestBlockImporter::new( - state_handler.clone(), - state_key_repo.clone(), - top_pool_author.clone(), - parentchain_block_import_trigger.clone(), - ocall_api.clone(), - peer_updater_mock, - )); - let block_composer = Arc::new(TestBlockComposer::new(signer, state_key_repo)); - let proposer_environment = ProposerFactory::new( - top_pool_author.clone(), - stf_executor, - block_composer, - metrics_ocall_mock, - ); - - info!("Create trusted operations.."); - let sender = endowed_account(); - let sender_with_low_balance = second_endowed_account(); - let receiver = unendowed_account(); - let transfered_amount: Balance = 1000; - let trusted_operation = encrypted_trusted_operation_transfer_balance( - ocall_api.as_ref(), - &shard_id, - &shielding_key, - sender, - receiver.public().into(), - transfered_amount, - ); - let invalid_trusted_operation = encrypted_trusted_operation_transfer_balance( - ocall_api.as_ref(), - &shard_id, - &shielding_key, - sender_with_low_balance, - receiver.public().into(), - ita_stf::test_genesis::SECOND_ENDOWED_ACC_FUNDS + 1, - ); - info!("Add trusted operations to TOP pool.."); - executor::block_on(top_pool_author.submit_top(RsaRequest::new(shard_id, trusted_operation))) - .unwrap(); - executor::block_on( - top_pool_author.submit_top(RsaRequest::new(shard_id, invalid_trusted_operation)), - ) - .unwrap(); - - // Ensure we have exactly two trusted calls in our TOP pool, and no getters. - assert_eq!(2, top_pool_author.get_pending_trusted_calls(shard_id).len()); - assert!(top_pool_author.get_pending_getters(shard_id).is_empty()); - - info!("Setup AURA SlotInfo"); - let timestamp = duration_now(); - let slot = slot_from_timestamp_and_duration(duration_now(), SLOT_DURATION); - let ends_at = timestamp + SLOT_DURATION; - let slot_info = SlotInfo::new( - slot, - timestamp, - SLOT_DURATION, - ends_at, - parentchain_header.clone(), - None, - None, - ); - - info!("Test setup is done."); - - let state_hash_before_block_production = get_state_hash(state_handler.as_ref(), &shard_id); - let scheduled_enclave = Arc::new(ScheduledEnclaveMock::default()); - - info!("Executing AURA on slot.."); - let (blocks, opaque_calls) = - exec_aura_on_slot::<_, ParentchainBlock, SignedSidechainBlock, _, _, _, _, _, _, _>( - slot_info, - signer, - ocall_api, - parentchain_block_import_trigger.clone(), - None::>, - None::>, - proposer_environment, - shards, - scheduled_enclave, - state_handler.clone(), - ) - .unwrap(); - - assert_eq!(1, blocks.len()); - assert_eq!( - state_hash_before_block_production, - get_state_hash(state_handler.as_ref(), &shard_id) - ); - - let (apriori_state_hash_in_block, aposteriori_state_hash_in_block) = - get_state_hashes_from_block(blocks.first().unwrap(), &state_key); - assert_ne!(state_hash_before_block_production, aposteriori_state_hash_in_block); - assert_eq!(state_hash_before_block_production, apriori_state_hash_in_block); - - // Ensure we have triggered the parentchain block import, because we claimed the slot. - assert!(parentchain_block_import_trigger.has_import_been_called()); - - // Ensure that invalid calls are removed from pool. Valid calls should only be removed upon block import. - assert_eq!(1, top_pool_author.get_pending_trusted_calls(shard_id).len()); - - info!("Executed AURA successfully. Sending blocks and extrinsics.."); - let propose_to_block_import_ocall_api = - Arc::new(ProposeToImportOCallApi::new(parentchain_header, block_importer)); - - send_blocks_and_extrinsics::( - blocks, - opaque_calls, - propose_to_block_import_ocall_api, - ) - .unwrap(); - - // After importing the sidechain block, the trusted operation should be removed. - assert!(top_pool_author.get_pending_trusted_calls(shard_id).is_empty()); - - // After importing the block, the state hash must be changed. - // We don't have a way to directly compare state hashes, because calculating the state hash - // would also involve applying set_last_block action, which updates the state upon import. - assert_ne!( - state_hash_before_block_production, - get_state_hash(state_handler.as_ref(), &shard_id) - ); - - let (mut state, _) = state_handler.load_cloned(&shard_id).unwrap(); - let free_balance = TestStf::get_account_data(&mut state, &receiver.public().into()).free; - assert_eq!(free_balance, transfered_amount); - assert!(TestStf::get_event_count(&mut state) > 0); - assert!(!TestStf::get_events(&mut state).is_empty()); -} - -fn encrypted_trusted_operation_transfer_balance< - AttestationApi: EnclaveAttestationOCallApi, - ShieldingKey: ShieldingCryptoEncrypt, ->( - attestation_api: &AttestationApi, - shard_id: &ShardIdentifier, - shielding_key: &ShieldingKey, - from: ed25519::Pair, - to: AccountId, - amount: Balance, -) -> Vec { - let call = TrustedCall::balance_transfer(Identity::Substrate(from.public().into()), to, amount); - let call_signed = sign_trusted_call(&call, attestation_api, shard_id, from); - let trusted_operation = TrustedOperation::::direct_call(call_signed); - encrypt_trusted_operation(shielding_key, &trusted_operation) -} - -fn get_state_hashes_from_block( - signed_block: &SignedSidechainBlock, - state_key: &Aes, -) -> (H256, H256) { - let mut encrypted_state_diff = signed_block.block.block_data().encrypted_state_diff.clone(); - state_key.decrypt(&mut encrypted_state_diff).unwrap(); - let decoded_state = - StatePayload::::decode(&mut encrypted_state_diff.as_slice()) - .unwrap(); - (decoded_state.state_hash_apriori(), decoded_state.state_hash_aposteriori()) -} - -fn get_state_hash(state_handler: &HandleStateMock, shard_id: &ShardIdentifier) -> H256 { - let (_, state_hash) = state_handler.load_cloned(shard_id).unwrap(); - state_hash -} diff --git a/bitacross-worker/enclave-runtime/src/test/sidechain_event_tests.rs b/bitacross-worker/enclave-runtime/src/test/sidechain_event_tests.rs deleted file mode 100644 index 64294f0121..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/sidechain_event_tests.rs +++ /dev/null @@ -1,190 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::{ - test::{ - fixtures::{ - components::{create_ocall_api, create_top_pool}, - initialize_test_state::init_state, - test_setup::{enclave_call_signer, TestStf}, - }, - mocks::{ - peer_updater_mock::PeerUpdaterMock, - propose_to_import_call_mock::ProposeToImportOCallApi, types::*, - }, - }, - top_pool_execution::{exec_aura_on_slot, send_blocks_and_extrinsics}, -}; -use ita_sgx_runtime::Runtime; -use ita_stf::{helpers::set_block_number, Getter, TrustedCallSigned}; -use itc_parentchain_test::ParentchainHeaderBuilder; -use itp_node_api::metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}; -use itp_settings::{ - sidechain::SLOT_DURATION, - worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}, -}; -use itp_sgx_externalities::SgxExternalitiesTrait; -use itp_stf_interface::system_pallet::SystemPalletEventInterface; -use itp_stf_state_handler::handle_state::HandleState; -use itp_test::mock::metrics_ocall_mock::MetricsOCallMock; -use itp_time_utils::duration_now; -use itp_top_pool_author::top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}; -use itp_types::Block as ParentchainBlock; -use its_block_verification::slot::slot_from_timestamp_and_duration; -use its_primitives::types::SignedBlock as SignedSidechainBlock; -use its_sidechain::{aura::proposer_factory::ProposerFactory, slots::SlotInfo}; -use lc_scheduled_enclave::ScheduledEnclaveMock; -use log::*; -use primitive_types::H256; -use sgx_crypto_helper::RsaKeyPair; -use sp_core::Pair; -use std::{sync::Arc, vec}; - -/// Integration test to ensure the events are reset upon block import. -/// Otherwise we will have an ever growing state. -/// (requires Sidechain mode) -pub fn ensure_events_get_reset_upon_block_proposal() { - // Test can only be run in Sidechain mode - if WorkerModeProvider::worker_mode() != WorkerMode::Sidechain { - info!("Ignoring sidechain block production test: Not in sidechain mode"); - return - } - - let _ = env_logger::builder().is_test(true).try_init(); - info!("Setting up test."); - - let signer = TestSigner::from_seed(b"42315678901234567890123456789012"); - let shielding_key = TestShieldingKey::new().unwrap(); - let state_key = TestStateKey::new([3u8; 16], [1u8; 16]); - let shielding_key_repo = Arc::new(TestShieldingKeyRepo::new(shielding_key)); - let state_key_repo = Arc::new(TestStateKeyRepo::new(state_key)); - let parentchain_header = ParentchainHeaderBuilder::default().build(); - - let ocall_api = create_ocall_api(&parentchain_header, &signer); - - info!("Initializing state and shard.."); - let state_handler = Arc::new(TestStateHandler::default()); - let enclave_call_signer = enclave_call_signer(&shielding_key); - let (_, shard_id) = init_state(state_handler.as_ref(), enclave_call_signer.public().into()); - let shards = vec![shard_id]; - - let node_metadata_repo = Arc::new(NodeMetadataRepository::new(NodeMetadataMock::new())); - let stf_executor = Arc::new(TestStfExecutor::new( - ocall_api.clone(), - state_handler.clone(), - node_metadata_repo, - )); - let top_pool = create_top_pool(); - let (sender, _receiver) = std::sync::mpsc::sync_channel(1000); - - let enclave_metrics_ocall_mock = Arc::new(MetricsOCallMock::default()); - - let top_pool_author = Arc::new(TestTopPoolAuthor::new( - top_pool, - AllowAllTopsFilter::::new(), - DirectCallsOnlyFilter::::new(), - state_handler.clone(), - shielding_key_repo, - enclave_metrics_ocall_mock.clone(), - Arc::new(sender), - )); - let parentchain_block_import_trigger = Arc::new(TestParentchainBlockImportTrigger::default()); - let peer_updater_mock = Arc::new(PeerUpdaterMock {}); - let block_importer = Arc::new(TestBlockImporter::new( - state_handler.clone(), - state_key_repo.clone(), - top_pool_author.clone(), - parentchain_block_import_trigger.clone(), - ocall_api.clone(), - peer_updater_mock, - )); - let block_composer = Arc::new(TestBlockComposer::new(signer, state_key_repo)); - let proposer_environment = ProposerFactory::new( - top_pool_author, - stf_executor, - block_composer, - enclave_metrics_ocall_mock, - ); - - // Add some events to the state. - let topic_hash = H256::from([7; 32]); - let event = frame_system::Event::::CodeUpdated; - let (lock, mut state) = state_handler.load_for_mutation(&shard_id).unwrap(); - state.execute_with(|| { - set_block_number(10); - frame_system::Pallet::::deposit_event_indexed( - &[topic_hash], - ita_sgx_runtime::RuntimeEvent::System(event), - ) - }); - state_handler.write_after_mutation(state.clone(), lock, &shard_id).unwrap(); - - // Check if state now really contains events and topics. - let (mut state, _) = state_handler.load_cloned(&shard_id).unwrap(); - assert_eq!(TestStf::get_event_count(&mut state), 1); - assert_eq!(TestStf::get_events(&mut state).len(), 1); - assert_eq!(TestStf::get_event_topics(&mut state, &topic_hash).len(), 1); - - info!("Setup AURA SlotInfo"); - let timestamp = duration_now(); - let slot = slot_from_timestamp_and_duration(duration_now(), SLOT_DURATION); - let ends_at = timestamp + SLOT_DURATION; - let slot_info = SlotInfo::new( - slot, - timestamp, - SLOT_DURATION, - ends_at, - parentchain_header.clone(), - None, - None, - ); - - let scheduled_enclave = Arc::new(ScheduledEnclaveMock::default()); - info!("Executing AURA on slot.."); - let (blocks, opaque_calls) = - exec_aura_on_slot::<_, ParentchainBlock, SignedSidechainBlock, _, _, _, _, _, _, _>( - slot_info, - signer, - ocall_api, - parentchain_block_import_trigger, - None::>, - None::>, - proposer_environment, - shards, - scheduled_enclave, - state_handler.clone(), - ) - .unwrap(); - - info!("Executed AURA successfully. Sending blocks and extrinsics.."); - let propose_to_block_import_ocall_api = - Arc::new(ProposeToImportOCallApi::new(parentchain_header, block_importer)); - - send_blocks_and_extrinsics::( - blocks, - opaque_calls, - propose_to_block_import_ocall_api, - ) - .unwrap(); - - // Ensure events have been reset. - let (mut state, _) = state_handler.load_cloned(&shard_id).unwrap(); - assert_eq!(TestStf::get_event_count(&mut state), 0); - assert_eq!(TestStf::get_event_topics(&mut state, &topic_hash).len(), 0); - assert_eq!(TestStf::get_events(&mut state).len(), 0); -} diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index e24dd7198d..9c5a86d6dd 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -26,22 +26,21 @@ use crate::{ fixtures::test_setup::{ enclave_call_signer, test_setup, TestStf, TestStfExecutor, TestTopPoolAuthor, }, - mocks::types::TestStateKeyRepo, - sidechain_aura_tests, sidechain_event_tests, state_getter_tests, top_pool_tests, + state_getter_tests, top_pool_tests, }, tls_ra, }; use codec::Decode; use ita_sgx_runtime::Parentchain; use ita_stf::{ - helpers::{account_key_hash, set_block_number}, + helpers::set_block_number, stf_sgx_tests, test_genesis::{endowed_account as funded_pair, unendowed_account}, - AccountInfo, Getter, State, TrustedCall, TrustedCallSigned, TrustedGetter, + Getter, State, TrustedCall, TrustedCallSigned, TrustedGetter, }; use itp_node_api::metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}; use itp_sgx_crypto::{Aes, StateCrypto}; -use itp_sgx_externalities::{SgxExternalitiesDiffType, SgxExternalitiesTrait, StateHash}; +use itp_sgx_externalities::{SgxExternalitiesDiffType, SgxExternalitiesTrait}; use itp_stf_executor::{ executor_tests as stf_executor_tests, traits::StateUpdateProposer, BatchExecutionResult, }; @@ -57,18 +56,7 @@ use itp_stf_primitives::{ use itp_stf_state_handler::handle_state::HandleState; use itp_test::mock::handle_state_mock; use itp_top_pool_author::{test_utils::submit_operation_to_top_pool, traits::AuthorApi}; -use itp_types::{AccountId, Balance, Block, Header}; -use its_primitives::{ - traits::{ - Block as BlockTrait, BlockData, Header as SidechainHeaderTrait, - SignedBlock as SignedBlockTrait, - }, - types::block::SignedBlock, -}; -use its_sidechain::{ - block_composer::{BlockComposer, ComposeBlock}, - state::SidechainSystemExt, -}; +use itp_types::{AccountId, Header}; use litentry_primitives::Identity; use sgx_tunittest::*; use sgx_types::size_t; @@ -107,12 +95,9 @@ pub extern "C" fn test_main_entrance() -> size_t { itp_sgx_crypto::tests::schnorr_seal_init_should_create_new_key_if_not_present, itp_sgx_crypto::tests::schnorr_seal_init_should_not_change_key_if_exists, itp_sgx_crypto::tests::schnorr_sign_should_produce_valid_signature, - test_compose_block, test_submit_trusted_call_to_top_pool, test_submit_trusted_getter_to_top_pool, test_differentiate_getter_and_call_works, - test_create_block_and_confirmation_works, - test_create_state_diff, test_executing_call_updates_account_nonce, test_call_set_update_parentchain_block, test_invalid_nonce_call_is_not_executed, @@ -148,8 +133,6 @@ pub extern "C" fn test_main_entrance() -> size_t { enclave_signer_tests::nonce_is_computed_correctly, state_getter_tests::state_getter_works, // sidechain integration tests - sidechain_aura_tests::produce_sidechain_block_and_import_it, - sidechain_event_tests::ensure_events_get_reset_upon_block_proposal, top_pool_tests::process_indirect_call_in_top_pool, // TODO: Litentry disables it for now (P-494) // top_pool_tests::submit_shielding_call_to_top_pool, @@ -193,36 +176,6 @@ fn run_evm_tests() { #[cfg(not(feature = "evm"))] fn run_evm_tests() {} -fn test_compose_block() { - // given - let (_, _, shard, _, _, state_handler, _) = test_setup(); - let block_composer = BlockComposer::::new( - test_account(), - Arc::new(TestStateKeyRepo::new(state_key())), - ); - - let signed_top_hashes: Vec = vec![[94; 32].into(), [1; 32].into()].to_vec(); - - let (mut state, _) = state_handler.load_cloned(&shard).unwrap(); - state.set_block_number(&1); - let state_hash_before_execution = state.hash(); - - // when - let signed_block = block_composer - .compose_block( - &latest_parentchain_header(), - signed_top_hashes, - shard, - state_hash_before_execution, - &state, - ) - .unwrap(); - - // then - assert!(signed_block.verify_signature()); - assert_eq!(signed_block.block().header().block_number(), 1); -} - fn test_submit_trusted_call_to_top_pool() { // given let (top_pool_author, _, shard, mrenclave, shielding_key, ..) = test_setup(); @@ -335,122 +288,6 @@ fn test_differentiate_getter_and_call_works() { ); } -fn test_create_block_and_confirmation_works() { - // given - let (top_pool_author, _, shard, mrenclave, shielding_key, _, stf_executor) = test_setup(); - - let block_composer = BlockComposer::::new( - test_account(), - Arc::new(TestStateKeyRepo::new(state_key())), - ); - - let sender = funded_pair(); - let receiver = unfunded_public(); - - let signed_call = TrustedCall::balance_transfer( - Identity::Substrate(sender.public().into()), - receiver.into(), - 1000, - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - let trusted_operation = direct_top(signed_call); - - let (top_hash, _) = submit_operation_to_top_pool( - top_pool_author.as_ref(), - &trusted_operation, - &shielding_key, - shard, - false, - ) - .unwrap(); - - // when - let execution_result = execute_trusted_calls(&shard, stf_executor.as_ref(), &top_pool_author); - - let executed_operation_hashes = execution_result.get_executed_operation_hashes().to_vec(); - - let signed_block = block_composer - .compose_block( - &latest_parentchain_header(), - executed_operation_hashes, - shard, - execution_result.state_hash_before_execution, - &execution_result.state_after_execution, - ) - .unwrap(); - - // then - assert!(signed_block.verify_signature()); - assert_eq!(signed_block.block().header().block_number(), 1); - assert_eq!(signed_block.block().block_data().signed_top_hashes()[0], top_hash); -} - -fn test_create_state_diff() { - // given - let (top_pool_author, _, shard, mrenclave, shielding_key, _, stf_executor) = test_setup(); - - let block_composer = BlockComposer::::new( - test_account(), - Arc::new(TestStateKeyRepo::new(state_key())), - ); - - let sender = funded_pair(); - let receiver = unfunded_public(); - const TX_AMOUNT: Balance = 1_000_000_000_000; - let signed_call = TrustedCall::balance_transfer( - Identity::Substrate(sender.public().into()), - receiver.into(), - TX_AMOUNT, - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - let trusted_operation = direct_top(signed_call); - - submit_operation_to_top_pool( - top_pool_author.as_ref(), - &trusted_operation, - &shielding_key, - shard, - false, - ) - .unwrap(); - - // when - let execution_result = execute_trusted_calls(&shard, stf_executor.as_ref(), &top_pool_author); - - let executed_operation_hashes = execution_result.get_executed_operation_hashes().to_vec(); - - let signed_block = block_composer - .compose_block( - &latest_parentchain_header(), - executed_operation_hashes, - shard, - execution_result.state_hash_before_execution, - &execution_result.state_after_execution, - ) - .unwrap(); - - let encrypted_state_diff = encrypted_state_diff_from_encrypted( - signed_block.block().block_data().encrypted_state_diff(), - ); - let state_diff = encrypted_state_diff.state_update(); - - // then - let sender_acc_info: AccountInfo = - get_from_state_diff(state_diff, &account_key_hash::(&sender.public().into())); - - let receiver_acc_info: AccountInfo = - get_from_state_diff(state_diff, &account_key_hash::(&receiver.into())); - - // state diff should consist of the following updates: - // (last_hash, sidechain block_number, sender_funds, receiver_funds, fee_recipient account [no clear, after polkadot_v0.9.26 update], events, frame_system::LastRuntimeUpgradeInfo,) - assert_eq!(state_diff.len(), 8); - assert_eq!(receiver_acc_info.data.free, TX_AMOUNT); - assert_eq!( - sender_acc_info.data.free, - ita_stf::test_genesis::ENDOWED_ACC_FUNDS - TX_AMOUNT - ita_stf::STF_TX_FEE - ); -} - fn test_executing_call_updates_account_nonce() { // given let (top_pool_author, _, shard, mrenclave, shielding_key, _, stf_executor) = test_setup(); @@ -750,10 +587,7 @@ fn execute_trusted_calls( &latest_parentchain_header(), shard, Duration::from_millis(600), - |mut s| { - s.set_block_number(&s.get_block_number().map_or(1, |n| n + 1)); - s - }, + |s| s, ) .unwrap() } diff --git a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs index 49e39239e2..03362680d7 100644 --- a/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/top_pool_tests.rs @@ -27,23 +27,11 @@ use crate::test::{ TestShieldingKey, TestShieldingKeyRepo, TestSigner, TestStateHandler, TestTopPoolAuthor, }, }; -use codec::Encode; use ita_stf::{ test_genesis::{endowed_account, unendowed_account}, Getter, TrustedCall, TrustedCallSigned, }; -use itc_parentchain_test::{ - parentchain_block_builder::ParentchainBlockBuilder, - parentchain_header_builder::ParentchainHeaderBuilder, -}; -use itp_node_api::{ - api_client::{ - ExtrinsicParams, ParentchainAdditionalParams, ParentchainExtrinsicParams, - ParentchainUncheckedExtrinsic, - }, - metadata::metadata_mocks::NodeMetadataMock, -}; -use itp_node_api_metadata::pallet_teebag::TeebagCallIndexes; +use itc_parentchain_test::ParentchainHeaderBuilder; use itp_ocall_api::EnclaveAttestationOCallApi; use itp_sgx_crypto::ShieldingCryptoEncrypt; use itp_stf_primitives::types::TrustedOperation; @@ -52,14 +40,14 @@ use itp_top_pool_author::{ top_filter::{AllowAllTopsFilter, DirectCallsOnlyFilter}, traits::AuthorApi, }; -use itp_types::{parentchain::Address, Block, RsaRequest, ShardIdentifier, H256}; +use itp_types::{RsaRequest, ShardIdentifier}; use jsonrpc_core::futures::executor; use litentry_primitives::Identity; use log::*; use sgx_crypto_helper::RsaKeyPair; -use sp_core::{ed25519, Pair}; -use sp_runtime::{MultiSignature, OpaqueExtrinsic}; +use sp_core::Pair; use std::{sync::Arc, vec::Vec}; + pub fn process_indirect_call_in_top_pool() { let _ = env_logger::builder().is_test(true).try_init(); info!("Setting up test."); diff --git a/bitacross-worker/enclave-runtime/src/tls_ra/tests.rs b/bitacross-worker/enclave-runtime/src/tls_ra/tests.rs index 5cdbd2a184..eb83438d74 100644 --- a/bitacross-worker/enclave-runtime/src/tls_ra/tests.rs +++ b/bitacross-worker/enclave-runtime/src/tls_ra/tests.rs @@ -27,7 +27,6 @@ use crate::{ }; use ita_stf::State; use itc_parentchain::light_client::mocks::validator_mock_seal::LightValidationStateSealMock; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; use itp_sgx_crypto::{mocks::KeyRepositoryMock, Aes}; use itp_stf_interface::InitState; use itp_stf_primitives::types::AccountId; @@ -55,7 +54,7 @@ fn run_state_provisioning_server(seal_handler: impl UnsealStateAndKeys, port: u1 let (socket, _addr) = listener.accept().unwrap(); let sgx_target_info: sgx_target_info_t = sgx_target_info_t::default(); - run_state_provisioning_server_internal::<_, WorkerModeProvider>( + run_state_provisioning_server_internal::<_>( socket.as_raw_fd(), SIGN_TYPE, Some(&sgx_target_info), @@ -128,14 +127,8 @@ pub fn test_tls_ra_server_client_networking() { assert_eq!(*client_shielding_key.read().unwrap(), shielding_key_encoded); assert_eq!(*client_light_client_state.read().unwrap(), light_client_state_encoded); - // State and state-key are provisioned only in sidechain mode - if WorkerModeProvider::worker_mode() == WorkerMode::Sidechain { - assert_eq!(*client_state.read().unwrap(), state_encoded); - assert_eq!(*client_state_key.read().unwrap(), state_key_encoded); - } else { - assert_eq!(*client_state.read().unwrap(), initial_client_state); - assert_eq!(*client_state_key.read().unwrap(), initial_client_state_key); - } + assert_eq!(*client_state.read().unwrap(), initial_client_state); + assert_eq!(*client_state_key.read().unwrap(), initial_client_state_key); } // Test state and key provisioning with 'real' data structures. diff --git a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs index e5fbed0a09..b72347afb2 100644 --- a/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs +++ b/bitacross-worker/enclave-runtime/src/tls_ra/tls_ra_server.rs @@ -34,8 +34,8 @@ use codec::Decode; use itp_attestation_handler::RemoteAttestationType; use itp_component_container::ComponentGetter; use itp_ocall_api::EnclaveAttestationOCallApi; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; use itp_types::ShardIdentifier; +use litentry_primitives::WorkerMode; use log::*; use rustls::{ServerConfig, ServerSession, StreamOwned}; use sgx_types::*; @@ -231,7 +231,7 @@ pub unsafe extern "C" fn run_state_provisioning_server( light_client_seal, ); - if let Err(e) = run_state_provisioning_server_internal::<_, WorkerModeProvider>( + if let Err(e) = run_state_provisioning_server_internal::<_>( socket_fd, sign_type, quoting_enclave_target_info, @@ -247,10 +247,7 @@ pub unsafe extern "C" fn run_state_provisioning_server( } /// Internal [`run_state_provisioning_server`] function to be able to use the handy `?` operator. -pub(crate) fn run_state_provisioning_server_internal< - StateAndKeyUnsealer: UnsealStateAndKeys, - WorkerModeProvider: ProvideWorkerMode, ->( +pub(crate) fn run_state_provisioning_server_internal( socket_fd: c_int, sign_type: sgx_quote_sign_type_t, quoting_enclave_target_info: Option<&sgx_target_info_t>, @@ -267,7 +264,7 @@ pub(crate) fn run_state_provisioning_server_internal< )?; let (server_session, tcp_stream) = tls_server_session_stream(socket_fd, server_config)?; - let provisioning = ProvisioningPayload::from(WorkerModeProvider::worker_mode()); + let provisioning = ProvisioningPayload::ShieldingKeyAndLightClient; let mut server = TlsServer::new(StreamOwned::new(server_session, tcp_stream), seal_handler, provisioning); diff --git a/bitacross-worker/enclave-runtime/src/top_pool_execution.rs b/bitacross-worker/enclave-runtime/src/top_pool_execution.rs deleted file mode 100644 index e80611707b..0000000000 --- a/bitacross-worker/enclave-runtime/src/top_pool_execution.rs +++ /dev/null @@ -1,414 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(not(feature = "production"))] -use crate::initialization::global_components::GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT; -use crate::{ - error::Result, - initialization::global_components::{ - GLOBAL_OCALL_API_COMPONENT, GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT, - GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT, GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT, - GLOBAL_STATE_HANDLER_COMPONENT, GLOBAL_TOP_POOL_AUTHOR_COMPONENT, - }, - sync::{EnclaveLock, EnclaveStateRWLock}, - utils::{ - get_extrinsic_factory_from_integritee_solo_or_parachain, - get_extrinsic_factory_from_target_a_solo_or_parachain, - get_extrinsic_factory_from_target_b_solo_or_parachain, - get_stf_executor_from_solo_or_parachain, - get_triggered_dispatcher_from_integritee_solo_or_parachain, - get_triggered_dispatcher_from_target_a_solo_or_parachain, - get_triggered_dispatcher_from_target_b_solo_or_parachain, - get_validator_accessor_from_integritee_solo_or_parachain, - get_validator_accessor_from_target_a_solo_or_parachain, - get_validator_accessor_from_target_b_solo_or_parachain, - }, -}; -use codec::Encode; -use itc_parentchain::{ - block_import_dispatcher::triggered_dispatcher::TriggerParentchainBlockImport, - light_client::{ - concurrent_access::ValidatorAccess, BlockNumberOps, ExtrinsicSender, LightClientState, - NumberFor, - }, -}; -use itp_component_container::ComponentGetter; -use itp_enclave_metrics::EnclaveMetric; -use itp_extrinsics_factory::CreateExtrinsics; -use itp_ocall_api::{EnclaveMetricsOCallApi, EnclaveOnChainOCallApi, EnclaveSidechainOCallApi}; -use itp_settings::sidechain::SLOT_DURATION; -use itp_sgx_crypto::key_repository::AccessKey; -use itp_sgx_externalities::SgxExternalities; -use itp_stf_state_handler::{handle_state::HandleState, query_shard_state::QueryShardState}; -use itp_time_utils::duration_now; -use itp_types::{parentchain::ParentchainCall, Block, OpaqueCall, H256}; -use its_primitives::{ - traits::{ - Block as SidechainBlockTrait, Header as HeaderTrait, ShardIdentifierFor, SignedBlock, - }, - types::block::SignedBlock as SignedSidechainBlock, -}; -use its_sidechain::{ - aura::{proposer_factory::ProposerFactory, Aura, SlotClaimStrategy}, - consensus_common::{Environment, Error as ConsensusError, ProcessBlockImportQueue}, - slots::{yield_next_slot, LastSlot, PerShardSlotWorkerScheduler, SlotInfo}, - validateer_fetch::ValidateerFetch, -}; -use lc_scheduled_enclave::{ScheduledEnclaveUpdater, GLOBAL_SCHEDULED_ENCLAVE}; -use litentry_macros::if_not_production; -use log::*; -use sgx_types::sgx_status_t; -use sp_core::{crypto::UncheckedFrom, Pair}; -use sp_runtime::{ - generic::SignedBlock as SignedParentchainBlock, traits::Block as BlockTrait, MultiSignature, -}; -use std::{sync::Arc, time::Instant, vec::Vec}; - -#[no_mangle] -pub unsafe extern "C" fn execute_trusted_calls() -> sgx_status_t { - if let Err(e) = execute_top_pool_trusted_calls_internal() { - return e.into() - } - - sgx_status_t::SGX_SUCCESS -} - -/// Internal [`execute_trusted_calls`] function to be able to use the `?` operator. -/// -/// Executes `Aura::on_slot() for `slot` if it is this enclave's `Slot`. -/// -/// This function makes an ocall that does the following: -/// -/// * Import all pending parentchain blocks. -/// * Sends sidechain `confirm_block` xt's with the produced sidechain blocks. -/// * Broadcast produced sidechain blocks to peer validateers. -fn execute_top_pool_trusted_calls_internal() -> Result<()> { - let start_time = Instant::now(); - - debug!("----------------------------------------"); - debug!("Start sidechain block production cycle"); - - // We acquire lock explicitly (variable binding), since '_' will drop the lock after the statement. - // See https://medium.com/codechain/rust-underscore-does-not-bind-fec6a18115a8 - let _enclave_write_lock = EnclaveLock::write_all()?; - - let slot_beginning_timestamp = duration_now(); - - let integritee_parentchain_import_dispatcher = - get_triggered_dispatcher_from_integritee_solo_or_parachain()?; - let maybe_target_a_parentchain_import_dispatcher = - get_triggered_dispatcher_from_target_a_solo_or_parachain().ok(); - let maybe_target_b_parentchain_import_dispatcher = - get_triggered_dispatcher_from_target_b_solo_or_parachain().ok(); - - let maybe_latest_target_a_parentchain_header = - if let Some(ref _triggered_dispatcher) = maybe_target_a_parentchain_import_dispatcher { - let validator_access = get_validator_accessor_from_target_a_solo_or_parachain()?; - Some(validator_access.execute_on_validator(|v| { - let latest_parentchain_header = v.latest_finalized_header()?; - Ok(latest_parentchain_header) - })?) - } else { - None - }; - - let maybe_latest_target_b_parentchain_header = - if let Some(ref _triggered_dispatcher) = maybe_target_b_parentchain_import_dispatcher { - let validator_access = get_validator_accessor_from_target_b_solo_or_parachain()?; - Some(validator_access.execute_on_validator(|v| { - let latest_parentchain_header = v.latest_finalized_header()?; - Ok(latest_parentchain_header) - })?) - } else { - None - }; - - let integritee_validator_access = get_validator_accessor_from_integritee_solo_or_parachain()?; - - // This gets the latest imported block. We accept that all of AURA, up until the block production - // itself, will operate on a parentchain block that is potentially outdated by one block - // (in case we have a block in the queue, but not imported yet). - let current_integritee_parentchain_header = - integritee_validator_access.execute_on_validator(|v| { - let latest_parentchain_header = v.latest_finalized_header()?; - Ok(latest_parentchain_header) - })?; - - // Import any sidechain blocks that are in the import queue. In case we are missing blocks, - // a peer sync will happen. If that happens, the slot time might already be used up just by this import. - let sidechain_block_import_queue_worker = - GLOBAL_SIDECHAIN_IMPORT_QUEUE_WORKER_COMPONENT.get()?; - - let latest_integritee_parentchain_header = sidechain_block_import_queue_worker - .process_queue(¤t_integritee_parentchain_header)?; - - trace!( - "Elapsed time to process sidechain block import queue: {} ms", - start_time.elapsed().as_millis() - ); - - let stf_executor = get_stf_executor_from_solo_or_parachain()?; - - let top_pool_author = GLOBAL_TOP_POOL_AUTHOR_COMPONENT.get()?; - - let block_composer = GLOBAL_SIDECHAIN_BLOCK_COMPOSER_COMPONENT.get()?; - - let state_handler = GLOBAL_STATE_HANDLER_COMPONENT.get()?; - - let ocall_api = GLOBAL_OCALL_API_COMPONENT.get()?; - - let authority = GLOBAL_SIGNING_KEY_REPOSITORY_COMPONENT.get()?.retrieve_key()?; - - #[cfg(not(feature = "production"))] - let fail_on_demand = GLOBAL_SIDECHAIN_FAIL_SLOT_ON_DEMAND_COMPONENT.get()?; - - match yield_next_slot( - slot_beginning_timestamp, - SLOT_DURATION, - latest_integritee_parentchain_header, - maybe_latest_target_a_parentchain_header, - maybe_latest_target_b_parentchain_header, - &mut LastSlot, - )? { - Some(slot) => { - if slot.duration_remaining().is_none() { - warn!("No time remaining in slot, skipping AURA execution"); - return Ok(()) - } - log_remaining_slot_duration(&slot, "Before AURA"); - - let shards = state_handler.list_shards()?; - let env = ProposerFactory::::new( - top_pool_author, - stf_executor, - block_composer, - ocall_api.clone(), - ); - - if_not_production!({ - if let Some(ref fail_on_demand) = *fail_on_demand { - fail_on_demand.next_slot(); - if fail_on_demand.check_before_on_slot() { - Result::Err(crate::error::Error::Sgx(sgx_status_t::SGX_ERROR_UNEXPECTED))?; - } - } - }); - - let (blocks, parentchain_calls) = - exec_aura_on_slot::<_, _, SignedSidechainBlock, _, _, _, _, _, _, _>( - slot.clone(), - authority, - ocall_api.clone(), - integritee_parentchain_import_dispatcher, - maybe_target_a_parentchain_import_dispatcher, - maybe_target_b_parentchain_import_dispatcher, - env, - shards, - GLOBAL_SCHEDULED_ENCLAVE.clone(), - state_handler, - )?; - - if_not_production!({ - if let Some(ref fail_on_demand) = *fail_on_demand { - if fail_on_demand.check_after_on_slot() { - Result::Err(crate::error::Error::Sgx(sgx_status_t::SGX_ERROR_UNEXPECTED))?; - } - } - }); - - debug!("Aura executed successfully"); - - // Drop lock as soon as we don't need it anymore. - drop(_enclave_write_lock); - - log_remaining_slot_duration(&slot, "After AURA"); - - send_blocks_and_extrinsics::(blocks, parentchain_calls, ocall_api)?; - - log_remaining_slot_duration(&slot, "After broadcasting and sending extrinsic"); - }, - None => { - debug!("No slot yielded. Skipping block production."); - return Ok(()) - }, - }; - - debug!("End sidechain block production cycle"); - Ok(()) -} - -/// Executes aura for the given `slot`. -#[allow(clippy::too_many_arguments)] -pub(crate) fn exec_aura_on_slot< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - PEnvironment, - IntegriteeBlockImportTrigger, - TargetABlockImportTrigger, - TargetBBlockImportTrigger, - ScheduledEnclave, - StateHandler, ->( - slot: SlotInfo, - authority: Authority, - ocall_api: Arc, - integritee_block_import_trigger: Arc, - maybe_target_a_block_import_trigger: Option>, - maybe_target_b_block_import_trigger: Option>, - proposer_environment: PEnvironment, - shards: Vec>, - scheduled_enclave: Arc, - state_handler: Arc, -) -> Result<(Vec, Vec)> -where - ParentchainBlock: BlockTrait, - SignedSidechainBlock: - SignedBlock + 'static, // Setting the public type is necessary due to some non-generic downstream code. - SignedSidechainBlock::Block: SidechainBlockTrait, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - SignedSidechainBlock::Signature: From, - Authority: Pair, - Authority::Public: Encode + UncheckedFrom<[u8; 32]>, - OCallApi: ValidateerFetch + EnclaveOnChainOCallApi + EnclaveSidechainOCallApi + Send + 'static, - NumberFor: BlockNumberOps, - PEnvironment: - Environment + Send + Sync, - IntegriteeBlockImportTrigger: - TriggerParentchainBlockImport>, - TargetABlockImportTrigger: - TriggerParentchainBlockImport>, - TargetBBlockImportTrigger: - TriggerParentchainBlockImport>, - ScheduledEnclave: ScheduledEnclaveUpdater, - StateHandler: HandleState, -{ - debug!("[Aura] Executing aura for slot: {:?}", slot); - - let mut aura = - Aura::<_, ParentchainBlock, SignedSidechainBlock, PEnvironment, _, _, _, _, _, _>::new( - authority, - ocall_api.as_ref().clone(), - integritee_block_import_trigger, - maybe_target_a_block_import_trigger, - maybe_target_b_block_import_trigger, - proposer_environment, - scheduled_enclave, - state_handler, - ) - .with_claim_strategy(SlotClaimStrategy::RoundRobin); - - // We only check if there are more workers registered, which might not really mean they are - // online and syncing sidechain state but that should be enough for now. - let is_single_worker = match ocall_api.get_trusted_peers_urls() { - Ok(urls) => urls.is_empty(), - Err(e) => { - warn!("Could not get trusted peers urls, error: {:?}", e); - warn!("Falling back to non single worker mode"); - false - }, - }; - - let (blocks, pxts): (Vec<_>, Vec<_>) = - PerShardSlotWorkerScheduler::on_slot(&mut aura, slot, shards, is_single_worker) - .into_iter() - .map(|r| (r.block, r.parentchain_effects)) - .unzip(); - - let opaque_calls: Vec = pxts.into_iter().flatten().collect(); - Ok((blocks, opaque_calls)) -} - -/// Broadcasts sidechain blocks to fellow peers and sends opaque calls as extrinsic to the parentchain. -pub(crate) fn send_blocks_and_extrinsics( - blocks: Vec, - parentchain_calls: Vec, - ocall_api: Arc, -) -> Result<()> -where - ParentchainBlock: BlockTrait, - SignedSidechainBlock: SignedBlock + 'static, - OCallApi: EnclaveSidechainOCallApi + EnclaveMetricsOCallApi, - NumberFor: BlockNumberOps, -{ - let started = std::time::Instant::now(); - debug!("Proposing {} sidechain block(s) (broadcasting to peers)", blocks.len()); - ocall_api.propose_sidechain_blocks(blocks)?; - if let Err(e) = - ocall_api.update_metric(EnclaveMetric::SidechainBlockBroadcastingTime(started.elapsed())) - { - warn!("Failed to update metric for sidechain block broadcasting time: {:?}", e); - }; - - let calls: Vec = parentchain_calls - .iter() - .filter_map(|parentchain_call| parentchain_call.as_litentry()) - .collect(); - debug!("Enclave wants to send {} extrinsics to Integritee Parentchain", calls.len()); - if !calls.is_empty() { - let extrinsics_factory = get_extrinsic_factory_from_integritee_solo_or_parachain()?; - let xts = extrinsics_factory.create_extrinsics(calls.as_slice(), None)?; - let validator_access = get_validator_accessor_from_integritee_solo_or_parachain()?; - validator_access.execute_mut_on_validator(|v| v.send_extrinsics(xts))?; - } - let calls: Vec = parentchain_calls - .iter() - .filter_map(|parentchain_call| parentchain_call.as_target_a()) - .collect(); - debug!("Enclave wants to send {} extrinsics to TargetA Parentchain", calls.len()); - if !calls.is_empty() { - let extrinsics_factory = get_extrinsic_factory_from_target_a_solo_or_parachain()?; - let xts = extrinsics_factory.create_extrinsics(calls.as_slice(), None)?; - let validator_access = get_validator_accessor_from_target_a_solo_or_parachain()?; - validator_access.execute_mut_on_validator(|v| v.send_extrinsics(xts))?; - } - let calls: Vec = parentchain_calls - .iter() - .filter_map(|parentchain_call| parentchain_call.as_target_b()) - .collect(); - debug!("Enclave wants to send {} extrinsics to TargetB Parentchain", calls.len()); - if !calls.is_empty() { - let extrinsics_factory = get_extrinsic_factory_from_target_b_solo_or_parachain()?; - let xts = extrinsics_factory.create_extrinsics(calls.as_slice(), None)?; - let validator_access = get_validator_accessor_from_target_b_solo_or_parachain()?; - validator_access.execute_mut_on_validator(|v| v.send_extrinsics(xts))?; - } - - Ok(()) -} - -fn log_remaining_slot_duration>( - slot_info: &SlotInfo, - stage_name: &str, -) { - match slot_info.duration_remaining() { - None => { - info!("No time remaining in slot (id: {:?}, stage: {})", slot_info.slot, stage_name); - }, - Some(remainder) => { - trace!( - "Remaining time in slot (id: {:?}, stage {}): {} ms, {}% of slot time", - slot_info.slot, - stage_name, - remainder.as_millis(), - (remainder.as_millis() as f64 / slot_info.duration.as_millis() as f64) * 100f64 - ); - }, - }; -} diff --git a/bitacross-worker/enclave-runtime/src/utils.rs b/bitacross-worker/enclave-runtime/src/utils.rs index 47ff73ce26..640ec99e88 100644 --- a/bitacross-worker/enclave-runtime/src/utils.rs +++ b/bitacross-worker/enclave-runtime/src/utils.rs @@ -18,11 +18,7 @@ use crate::{ error::{Error, Result}, initialization::global_components::{ EnclaveExtrinsicsFactory, EnclaveNodeMetadataRepository, EnclaveStfEnclaveSigner, - EnclaveStfExecutor, EnclaveValidatorAccessor, - IntegriteeParentchainTriggeredBlockImportDispatcher, - TargetAParentchainTriggeredBlockImportDispatcher, - TargetBParentchainTriggeredBlockImportDispatcher, - GLOBAL_INTEGRITEE_PARACHAIN_HANDLER_COMPONENT, + EnclaveValidatorAccessor, GLOBAL_INTEGRITEE_PARACHAIN_HANDLER_COMPONENT, GLOBAL_INTEGRITEE_SOLOCHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_A_PARACHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_A_SOLOCHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT, GLOBAL_TARGET_B_SOLOCHAIN_HANDLER_COMPONENT, @@ -65,65 +61,6 @@ unsafe impl DecodeRaw for D { } } -pub unsafe fn utf8_str_from_raw<'a>( - data: *const u8, - len: usize, -) -> StdResult<&'a str, std::str::Utf8Error> { - let bytes = slice::from_raw_parts(data, len); - - std::str::from_utf8(bytes) -} - -// FIXME: When solving #1080, these helper functions should be obsolete, because no dynamic allocation -// is necessary anymore. -pub(crate) fn get_triggered_dispatcher_from_integritee_solo_or_parachain( -) -> Result> { - let dispatcher = - if let Ok(solochain_handler) = GLOBAL_INTEGRITEE_SOLOCHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(solochain_handler.import_dispatcher.clone())? - } else if let Ok(parachain_handler) = GLOBAL_INTEGRITEE_PARACHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(parachain_handler.import_dispatcher.clone())? - } else { - return Err(Error::NoLitentryParentchainAssigned) - }; - Ok(dispatcher) -} - -pub(crate) fn get_triggered_dispatcher_from_target_a_solo_or_parachain( -) -> Result> { - let dispatcher = - if let Ok(solochain_handler) = GLOBAL_TARGET_A_SOLOCHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(solochain_handler.import_dispatcher.clone())? - } else if let Ok(parachain_handler) = GLOBAL_TARGET_A_PARACHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(parachain_handler.import_dispatcher.clone())? - } else { - return Err(Error::NoTargetAParentchainAssigned) - }; - Ok(dispatcher) -} - -pub(crate) fn get_triggered_dispatcher_from_target_b_solo_or_parachain( -) -> Result> { - let dispatcher = - if let Ok(solochain_handler) = GLOBAL_TARGET_B_SOLOCHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(solochain_handler.import_dispatcher.clone())? - } else if let Ok(parachain_handler) = GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT.get() { - get_triggered_dispatcher(parachain_handler.import_dispatcher.clone())? - } else { - return Err(Error::NoTargetBParentchainAssigned) - }; - Ok(dispatcher) -} - -pub(crate) fn get_triggered_dispatcher( - dispatcher: Arc>, -) -> Result> { - let triggered_dispatcher = dispatcher - .triggered_dispatcher() - .ok_or(Error::ExpectedTriggeredImportDispatcher)?; - Ok(triggered_dispatcher) -} - pub(crate) fn get_validator_accessor_from_integritee_solo_or_parachain( ) -> Result> { let validator_accessor = @@ -137,32 +74,6 @@ pub(crate) fn get_validator_accessor_from_integritee_solo_or_parachain( Ok(validator_accessor) } -pub(crate) fn get_validator_accessor_from_target_a_solo_or_parachain( -) -> Result> { - let validator_accessor = - if let Ok(solochain_handler) = GLOBAL_TARGET_A_SOLOCHAIN_HANDLER_COMPONENT.get() { - solochain_handler.validator_accessor.clone() - } else if let Ok(parachain_handler) = GLOBAL_TARGET_A_PARACHAIN_HANDLER_COMPONENT.get() { - parachain_handler.validator_accessor.clone() - } else { - return Err(Error::NoTargetAParentchainAssigned) - }; - Ok(validator_accessor) -} - -pub(crate) fn get_validator_accessor_from_target_b_solo_or_parachain( -) -> Result> { - let validator_accessor = - if let Ok(solochain_handler) = GLOBAL_TARGET_B_SOLOCHAIN_HANDLER_COMPONENT.get() { - solochain_handler.validator_accessor.clone() - } else if let Ok(parachain_handler) = GLOBAL_TARGET_B_PARACHAIN_HANDLER_COMPONENT.get() { - parachain_handler.validator_accessor.clone() - } else { - return Err(Error::NoTargetBParentchainAssigned) - }; - Ok(validator_accessor) -} - pub(crate) fn get_node_metadata_repository_from_integritee_solo_or_parachain( ) -> Result> { let metadata_repository = @@ -241,18 +152,6 @@ pub(crate) fn get_extrinsic_factory_from_target_b_solo_or_parachain( Ok(extrinsics_factory) } -pub(crate) fn get_stf_executor_from_solo_or_parachain() -> Result> { - let stf_executor = - if let Ok(solochain_handler) = GLOBAL_INTEGRITEE_SOLOCHAIN_HANDLER_COMPONENT.get() { - solochain_handler.stf_executor.clone() - } else if let Ok(parachain_handler) = GLOBAL_INTEGRITEE_PARACHAIN_HANDLER_COMPONENT.get() { - parachain_handler.stf_executor.clone() - } else { - return Err(Error::NoLitentryParentchainAssigned) - }; - Ok(stf_executor) -} - pub(crate) fn get_stf_enclave_signer_from_solo_or_parachain() -> Result> { let stf_enclave_signer = diff --git a/bitacross-worker/litentry/core/direct-call/Cargo.toml b/bitacross-worker/litentry/core/direct-call/Cargo.toml index 14214a5355..9e055e6c80 100644 --- a/bitacross-worker/litentry/core/direct-call/Cargo.toml +++ b/bitacross-worker/litentry/core/direct-call/Cargo.toml @@ -8,7 +8,6 @@ version = "0.1.0" codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } # internal dependencies bc-relayer-registry = { path = "../../../bitacross/core/bc-relayer-registry", default-features = false } @@ -42,7 +41,6 @@ std = [ "litentry-primitives/std", "sp-core/std", "sp-io/std", - "sp-runtime/std", "parentchain-primitives/std", ] test = [ diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs b/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs index 251d56b435..b8065fcf03 100644 --- a/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs +++ b/bitacross-worker/litentry/core/direct-call/src/handler/sign_bitcoin.rs @@ -26,8 +26,8 @@ pub mod test { use bc_relayer_registry::{RelayerRegistry, RelayerRegistryUpdater}; use itp_sgx_crypto::mocks::KeyRepositoryMock; use k256::elliptic_curve::rand_core; - use parentchain_primitives::{Address32, Identity}; - use sp_core::{crypto::Ss58Codec, sr25519 as sr25519_core, sr25519, Pair}; + use parentchain_primitives::Identity; + use sp_core::{sr25519, Pair}; #[test] pub fn it_should_return_ok_for_relayer_signer() { diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs index 12bbf397fa..91be8d636f 100644 --- a/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs +++ b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs @@ -26,8 +26,8 @@ pub mod test { use bc_relayer_registry::{RelayerRegistry, RelayerRegistryUpdater}; use itp_sgx_crypto::mocks::KeyRepositoryMock; use k256::elliptic_curve::rand_core; - use parentchain_primitives::{Address32, Identity}; - use sp_core::{crypto::Ss58Codec, sr25519 as sr25519_core, sr25519, Pair}; + use parentchain_primitives::Identity; + use sp_core::{sr25519, Pair}; #[test] pub fn it_should_return_ok_for_relayer_signer() { diff --git a/bitacross-worker/litentry/primitives/Cargo.toml b/bitacross-worker/litentry/primitives/Cargo.toml index 23cf074100..e0b645a3eb 100644 --- a/bitacross-worker/litentry/primitives/Cargo.toml +++ b/bitacross-worker/litentry/primitives/Cargo.toml @@ -8,9 +8,7 @@ version = "0.1.0" bitcoin = { version = "0.31.0", default-features = false, features = ["secp-recovery", "no-std"] } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } hex = { version = "0.4.3", default-features = false } -lazy_static = { version = "1.1.0", features = ["spin_no_std"] } log = { version = "0.4", default-features = false } -pallet-evm = { default-features = false, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } rand = { version = "0.7", optional = true } rand-sgx = { package = "rand", git = "https://github.com/mesalock-linux/rand-sgx", tag = "sgx_1.1.3", features = ["sgx_tstd"], optional = true } ring = { version = "0.16.20", default-features = false } @@ -21,19 +19,12 @@ sp-core = { git = "https://github.com/paritytech/substrate", branch = "polkadot- sp-io = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-runtime = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } sp-std = { git = "https://github.com/paritytech/substrate", branch = "polkadot-v0.9.42", default-features = false } -strum = { version = "0.26", default-features = false } -strum_macros = { version = "0.26", default-features = false } -thiserror = { version = "1.0.26", optional = true } # sgx dependencies -base64_sgx = { package = "base64", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base64-sgx", optional = true } sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master", optional = true, features = ["net", "thread"] } -thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } # internal dependencies itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } -itp-sgx-io = { path = "../../core-primitives/sgx/io", default-features = false } -itp-utils = { path = "../../core-primitives/utils", default-features = false } litentry-hex-utils = { path = "../../../primitives/hex", default-features = false } pallet-teebag = { path = "../../../pallets/teebag", default-features = false } parentchain-primitives = { package = "core-primitives", path = "../../../primitives/core", default-features = false } @@ -50,15 +41,11 @@ sgx = [ "sgx_tstd", "rand-sgx", "itp-sgx-crypto/sgx", - "thiserror-sgx", - "itp-sgx-io/sgx", ] std = [ - "strum/std", "hex/std", "serde/std", "itp-sgx-crypto/std", - "itp-utils/std", "sp-core/std", "sp-std/std", "sp-io/std", @@ -70,6 +57,4 @@ std = [ "log/std", "bitcoin/std", "secp256k1/std", - "thiserror", - "itp-sgx-io/std", ] diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index 1c3315b960..77a79434c2 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -33,7 +33,6 @@ warp = "0.3" ipfs-api = "0.11.0" codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -primitive-types = { version = "0.12.1", default-features = false, features = ["codec"] } sgx_crypto_helper = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git" } sgx_types = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git" } @@ -42,7 +41,6 @@ sgx_types = { branch = "master", git = "https://github.com/apache/teaclave-sgx-s itc-parentchain = { path = "../core/parentchain/parentchain-crate" } itc-rest-client = { path = "../core/rest-client" } itc-rpc-client = { path = "../core/rpc-client" } -itc-rpc-server = { path = "../core/rpc-server" } itp-api-client-types = { path = "../core-primitives/node-api/api-client-types" } itp-enclave-api = { path = "../core-primitives/enclave-api" } itp-enclave-metrics = { path = "../core-primitives/enclave-metrics" } @@ -51,18 +49,12 @@ itp-settings = { path = "../core-primitives/settings" } itp-storage = { path = "../core-primitives/storage" } itp-types = { path = "../core-primitives/types" } itp-utils = { path = "../core-primitives/utils" } -its-consensus-slots = { path = "../sidechain/consensus/slots" } -its-peer-fetch = { path = "../sidechain/peer-fetch" } -its-primitives = { path = "../sidechain/primitives" } -its-rpc-handler = { path = "../sidechain/rpc-handler" } -its-storage = { path = "../sidechain/storage" } # `default-features = false` to remove the jsonrpsee dependency. substrate-api-client = { default-features = false, features = ["std", "sync-api"], git = "https://github.com/scs/substrate-api-client.git", branch = "polkadot-v0.9.42-tag-v0.14.0" } # Substrate dependencies frame-support = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -pallet-balances = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-consensus-grandpa = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42", features = ["full_crypto"] } sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } @@ -70,8 +62,6 @@ sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "po # litentry config = "0.13.3" -ita-stf = { path = "../app-libs/stf", default-features = false } -litentry-hex-utils = { path = "../../primitives/hex", default-features = false } litentry-macros = { path = "../../primitives/core/macros", default-features = false } litentry-primitives = { path = "../litentry/primitives" } my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/rococo" } @@ -79,10 +69,8 @@ my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/ [features] default = [] evm = [] -sidechain = ["itp-settings/sidechain"] offchain-worker = ["itp-settings/offchain-worker"] production = [ - "ita-stf/production", "itp-settings/production", "litentry-macros/production", "litentry-primitives/production", @@ -107,5 +95,3 @@ anyhow = "1.0.40" mockall = "0.11" # local itc-parentchain-test = { path = "../core/parentchain/test" } -its-peer-fetch = { path = "../sidechain/peer-fetch", features = ["mocks"] } -its-test = { path = "../sidechain/test" } diff --git a/bitacross-worker/service/src/cli.yml b/bitacross-worker/service/src/cli.yml index 959798e17e..7e411b3181 100644 --- a/bitacross-worker/service/src/cli.yml +++ b/bitacross-worker/service/src/cli.yml @@ -114,17 +114,6 @@ args: takes_value: true required: false default_value: "0" - - fail-slot-mode: - long: fail-slot-mode - help: Set the mode of failing a slot, values [BeforeOnSlot, AfterOnSlot] - takes_value: true - required: false - - fail-at: - long: fail-at - help: Set the slot to fail on - takes_value: true - required: false - default_value: "0" subcommands: - run: diff --git a/bitacross-worker/service/src/config.rs b/bitacross-worker/service/src/config.rs index fd33d09957..6498bf86fd 100644 --- a/bitacross-worker/service/src/config.rs +++ b/bitacross-worker/service/src/config.rs @@ -33,7 +33,6 @@ static DEFAULT_MU_RA_PORT: &str = "3443"; static DEFAULT_METRICS_PORT: &str = "8787"; static DEFAULT_UNTRUSTED_HTTP_PORT: &str = "4545"; static DEFAULT_PARENTCHAIN_START_BLOCK: &str = "0"; -static DEFAULT_FAIL_AT: &str = "0"; #[derive(Clone, Debug, PartialEq)] pub struct Config { @@ -69,10 +68,6 @@ pub struct Config { /// the parentchain block number to start syncing with pub parentchain_start_block: String, - /// mode to use for failing sidechain slot - pub fail_slot_mode: Option, - /// slot number to fail at - pub fail_at: u64, } #[allow(clippy::too_many_arguments)] @@ -97,8 +92,6 @@ impl Config { data_dir: PathBuf, run_config: Option, parentchain_start_block: String, - fail_slot_mode: Option, - fail_at: u64, ) -> Self { Self { litentry_rpc_url, @@ -120,8 +113,6 @@ impl Config { data_dir, run_config, parentchain_start_block, - fail_slot_mode, - fail_at, } } @@ -253,8 +244,6 @@ impl From<&ArgMatches<'_>> for Config { let parentchain_start_block = m.value_of("parentchain-start-block").unwrap_or(DEFAULT_PARENTCHAIN_START_BLOCK); - let fail_slot_mode = m.value_of("fail-slot-mode").map(|v| v.to_string()); - let fail_at = m.value_of("fail-at").unwrap_or(DEFAULT_FAIL_AT).parse().unwrap(); Self::new( m.value_of("node-url").unwrap_or(DEFAULT_NODE_URL).into(), m.value_of("node-port").unwrap_or(DEFAULT_NODE_PORT).into(), @@ -278,8 +267,6 @@ impl From<&ArgMatches<'_>> for Config { data_dir, run_config, parentchain_start_block.to_string(), - fail_slot_mode, - fail_at, ) } } @@ -391,8 +378,6 @@ mod test { assert_eq!(config.data_dir, pwd()); assert!(config.run_config.is_none()); assert_eq!(config.parentchain_start_block, DEFAULT_PARENTCHAIN_START_BLOCK); - assert_matches!(config.fail_slot_mode, Option::None); - assert_eq!(config.fail_at, DEFAULT_FAIL_AT.parse::().unwrap()) } #[test] diff --git a/bitacross-worker/service/src/initialized_service.rs b/bitacross-worker/service/src/initialized_service.rs index f35a17fa28..2aca3876ac 100644 --- a/bitacross-worker/service/src/initialized_service.rs +++ b/bitacross-worker/service/src/initialized_service.rs @@ -19,7 +19,6 @@ //! hosted on a http server. use crate::error::ServiceResult; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use log::*; use parking_lot::RwLock; use std::{default::Default, marker::PhantomData, net::SocketAddr, sync::Arc}; @@ -61,62 +60,30 @@ pub trait IsInitialized { pub trait TrackInitialization { fn registered_on_parentchain(&self); - fn sidechain_block_produced(&self); - fn worker_for_shard_registered(&self); } -pub struct InitializationHandler { +#[derive(Default)] +pub struct InitializationHandler { registered_on_parentchain: RwLock, - sidechain_block_produced: RwLock, worker_for_shard_registered: RwLock, - _phantom: PhantomData, -} - -// Cannot use #[derive(Default)], because the compiler complains that WorkerModeProvider then -// also needs to implement Default. Which does not make sense, since it's only used in PhantomData. -// Explicitly implementing Default solves the problem -// (see https://stackoverflow.com/questions/59538071/the-trait-bound-t-stddefaultdefault-is-not-satisfied-when-using-phantomda). -impl Default for InitializationHandler { - fn default() -> Self { - Self { - registered_on_parentchain: Default::default(), - sidechain_block_produced: Default::default(), - worker_for_shard_registered: Default::default(), - _phantom: Default::default(), - } - } } -impl TrackInitialization for InitializationHandler { +impl TrackInitialization for InitializationHandler { fn registered_on_parentchain(&self) { let mut registered_lock = self.registered_on_parentchain.write(); *registered_lock = true; } - fn sidechain_block_produced(&self) { - let mut block_produced_lock = self.sidechain_block_produced.write(); - *block_produced_lock = true; - } - fn worker_for_shard_registered(&self) { let mut registered_lock = self.worker_for_shard_registered.write(); *registered_lock = true; } } -impl IsInitialized for InitializationHandler -where - WorkerModeProvider: ProvideWorkerMode, -{ +impl IsInitialized for InitializationHandler { fn is_initialized(&self) -> bool { - match WorkerModeProvider::worker_mode() { - WorkerMode::Sidechain => - *self.registered_on_parentchain.read() - && *self.worker_for_shard_registered.read() - && *self.sidechain_block_produced.read(), - _ => *self.registered_on_parentchain.read(), - } + *self.registered_on_parentchain.read() } } @@ -125,48 +92,18 @@ mod tests { use super::*; - struct OffchainWorkerMode; - impl ProvideWorkerMode for OffchainWorkerMode { - fn worker_mode() -> WorkerMode { - WorkerMode::OffChainWorker - } - } - - struct SidechainWorkerMode; - impl ProvideWorkerMode for SidechainWorkerMode { - fn worker_mode() -> WorkerMode { - WorkerMode::Sidechain - } - } - #[test] fn default_handler_is_initialized_returns_false() { - let offchain_worker_handler = InitializationHandler::::default(); - let sidechain_handler = InitializationHandler::::default(); + let offchain_worker_handler = InitializationHandler::default(); assert!(!offchain_worker_handler.is_initialized()); - assert!(!sidechain_handler.is_initialized()); } #[test] - fn in_offchain_worker_mode_parentchain_registration_is_enough_for_initialized() { - let initialization_handler = InitializationHandler::::default(); + fn parentchain_registration_is_enough_for_initialized() { + let initialization_handler = InitializationHandler::default(); initialization_handler.registered_on_parentchain(); assert!(initialization_handler.is_initialized()); } - - #[test] - fn in_sidechain_mode_all_condition_have_to_be_met() { - let sidechain_handler = InitializationHandler::::default(); - - sidechain_handler.registered_on_parentchain(); - assert!(!sidechain_handler.is_initialized()); - - sidechain_handler.worker_for_shard_registered(); - assert!(!sidechain_handler.is_initialized()); - - sidechain_handler.sidechain_block_produced(); - assert!(sidechain_handler.is_initialized()); - } } diff --git a/bitacross-worker/service/src/main.rs b/bitacross-worker/service/src/main.rs index feab3398ea..cb7f43d4c8 100644 --- a/bitacross-worker/service/src/main.rs +++ b/bitacross-worker/service/src/main.rs @@ -28,8 +28,6 @@ mod ocall_bridge; mod parentchain_handler; mod prometheus_metrics; mod setup; -mod sidechain_setup; -mod sync_block_broadcaster; mod sync_state; mod tests; mod utils; diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 0c90fe9d36..dcc7404187 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -17,10 +17,7 @@ use crate::{ }, parentchain_handler::{HandleParentchain, ParentchainHandler}, prometheus_metrics::{start_metrics_server, EnclaveMetricsReceiver, MetricsHandler}, - setup, - sidechain_setup::{sidechain_init_block_production, sidechain_start_untrusted_rpc_server}, - sync_block_broadcaster::SyncBlockBroadcaster, - sync_state, tests, + setup, sync_state, tests, utils::extract_shard, worker::Worker, worker_peers_registry::WorkerPeersRegistry, @@ -29,7 +26,6 @@ use base58::ToBase58; use clap::{load_yaml, App, ArgMatches}; use codec::{Decode, Encode}; use itp_enclave_api::{ - direct_request::DirectRequest, enclave_base::EnclaveBase, remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, sidechain::Sidechain, @@ -39,12 +35,6 @@ use itp_node_api::{ metadata::NodeMetadata, node_api_factory::{CreateNodeApi, NodeApiFactory}, }; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode, WorkerModeProvider}; -use its_peer_fetch::{ - block_fetch_client::BlockFetcher, untrusted_peer_fetch::UntrustedPeerFetcher, -}; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use its_storage::{interface::FetchBlocks, BlockPruner, SidechainStorageLock}; use litentry_macros::if_production_or; use litentry_primitives::{Enclave as TeebagEnclave, ShardIdentifier, WorkerType}; use log::*; @@ -75,8 +65,7 @@ use substrate_api_client::ac_node_api::{EventRecord, Phase::ApplyExtrinsic}; const VERSION: &str = env!("CARGO_PKG_VERSION"); #[cfg(feature = "link-binary")] -pub type EnclaveWorker = - Worker>; +pub type EnclaveWorker = Worker; pub type Event = substrate_api_client::ac_node_api::EventRecord; pub(crate) fn main() { @@ -97,8 +86,6 @@ pub(crate) fn main() { #[cfg(not(feature = "production"))] info!("*** Starting service in SGX debug mode"); - info!("*** Running worker in mode: {:?} \n", WorkerModeProvider::worker_mode()); - let clean_reset = matches.is_present("clean-reset"); if clean_reset { crate::setup::purge_files_from_dir(config.data_dir()).unwrap(); @@ -106,12 +93,6 @@ pub(crate) fn main() { // build the entire dependency tree let tokio_handle = Arc::new(GlobalTokioHandle {}); - let sidechain_blockstorage = Arc::new( - SidechainStorageLock::::from_base_path( - config.data_dir().to_path_buf(), - ) - .unwrap(), - ); let node_api_factory = Arc::new(NodeApiFactory::new(config.litentry_rpc_endpoint(), AccountKeyring::Alice.pair())); let enclave = Arc::new(enclave_init(&config).unwrap()); @@ -123,12 +104,7 @@ pub(crate) fn main() { initialization_handler.clone(), HashSet::new(), )); - let sync_block_broadcaster = - Arc::new(SyncBlockBroadcaster::new(tokio_handle.clone(), worker.clone())); let peer_updater = Arc::new(WorkerPeersRegistry::new(worker)); - let untrusted_peer_fetcher = UntrustedPeerFetcher::new(node_api_factory.clone()); - let peer_sidechain_block_fetcher = - Arc::new(BlockFetcher::::new(untrusted_peer_fetcher)); let enclave_metrics_receiver = Arc::new(EnclaveMetricsReceiver {}); let maybe_target_a_parentchain_api_factory = config @@ -144,11 +120,8 @@ pub(crate) fn main() { node_api_factory.clone(), maybe_target_a_parentchain_api_factory, maybe_target_b_parentchain_api_factory, - sync_block_broadcaster, enclave.clone(), - sidechain_blockstorage.clone(), peer_updater, - peer_sidechain_block_fetcher, tokio_handle.clone(), enclave_metrics_receiver, ))); @@ -188,7 +161,7 @@ pub(crate) fn main() { node_api_factory.create_api().expect("Failed to create parentchain node API"); if run_config.request_state() { - sync_state::sync_state::<_, _, WorkerModeProvider>( + sync_state::sync_state::<_, _>( &node_api, &shard, enclave.as_ref(), @@ -196,11 +169,10 @@ pub(crate) fn main() { ); } - start_worker::<_, _, _, _, WorkerModeProvider>( + start_worker::<_, _, _>( config, &shard, enclave, - sidechain_blockstorage, node_api, tokio_handle, initialization_handler, @@ -211,7 +183,7 @@ pub(crate) fn main() { println!("*** Requesting state from a registered worker \n"); let node_api = node_api_factory.create_api().expect("Failed to create parentchain node API"); - sync_state::sync_state::<_, _, WorkerModeProvider>( + sync_state::sync_state::<_, _>( &node_api, &extract_shard(smatches.value_of("shard"), enclave.as_ref()), enclave.as_ref(), @@ -303,11 +275,10 @@ pub(crate) fn main() { /// FIXME: needs some discussion (restructuring?) #[allow(clippy::too_many_arguments)] -fn start_worker( +fn start_worker( config: Config, shard: &ShardIdentifier, enclave: Arc, - sidechain_storage: Arc, litentry_rpc_api: ParentchainApi, tokio_handle_getter: Arc, initialization_handler: Arc, @@ -315,19 +286,12 @@ fn start_worker( quote_size: Option, ) where T: GetTokioHandle, - E: EnclaveBase + DirectRequest + Sidechain + RemoteAttestation + TlsRemoteAttestation + Clone, - D: BlockPruner + FetchBlocks + Sync + Send + 'static, + E: EnclaveBase + Sidechain + RemoteAttestation + TlsRemoteAttestation + Clone, InitializationHandler: TrackInitialization + IsInitialized + Sync + Send + 'static, - WorkerModeProvider: ProvideWorkerMode, { let run_config = config.run_config().clone().expect("Run config missing"); let skip_ra = run_config.skip_ra(); - #[cfg(feature = "sidechain")] - let flavor_str = "sidechain"; - #[cfg(feature = "offchain-worker")] - let flavor_str = "offchain-worker"; - #[cfg(not(any(feature = "offchain-worker", feature = "sidechain")))] let flavor_str = "offchain-worker"; println!("Litentry Worker for {} v{}", flavor_str, VERSION); @@ -418,34 +382,18 @@ fn start_worker( // ------------------------------------------------------------------------ // Start trusted worker rpc server - if WorkerModeProvider::worker_mode() == WorkerMode::Sidechain - || WorkerModeProvider::worker_mode() == WorkerMode::OffChainWorker - { - let direct_invocation_server_addr = config.trusted_worker_url_internal(); - let enclave_for_direct_invocation = enclave.clone(); - thread::spawn(move || { - println!( - "[+] Trusted RPC direct invocation server listening on {}", - direct_invocation_server_addr - ); - enclave_for_direct_invocation - .init_direct_invocation_server(direct_invocation_server_addr) - .unwrap(); - println!("[+] RPC direct invocation server shut down"); - }); - } - - // ------------------------------------------------------------------------ - // Start untrusted worker rpc server. - // i.e move sidechain block importing to trusted worker. - if WorkerModeProvider::worker_mode() == WorkerMode::Sidechain { - sidechain_start_untrusted_rpc_server( - &config, - enclave.clone(), - sidechain_storage.clone(), - &tokio_handle, + let direct_invocation_server_addr = config.trusted_worker_url_internal(); + let enclave_for_direct_invocation = enclave.clone(); + thread::spawn(move || { + println!( + "[+] Trusted RPC direct invocation server listening on {}", + direct_invocation_server_addr ); - } + enclave_for_direct_invocation + .init_direct_invocation_server(direct_invocation_server_addr) + .unwrap(); + println!("[+] RPC direct invocation server shut down"); + }); // ------------------------------------------------------------------------ // Init parentchain specific stuff. Needed for parentchain communication. @@ -541,53 +489,15 @@ fn start_worker( initialization_handler.registered_on_parentchain(); - match WorkerModeProvider::worker_mode() { - WorkerMode::OffChainWorker => { - println!("*** [+] Finished initializing light client, syncing parentchain..."); - - // Syncing all parentchain blocks, this might take a while.. - let last_synced_header = - parentchain_handler.sync_parentchain(last_synced_header, 0, true).unwrap(); - - start_parentchain_header_subscription_thread(parentchain_handler, last_synced_header); - - info!("skipping shard vault check because not yet supported for offchain worker"); - }, - WorkerMode::Sidechain => { - println!("*** [+] Finished initializing light client, syncing parentchain..."); - - // Litentry: apply skipped parentchain block - let parentchain_start_block = config - .try_parse_parentchain_start_block() - .expect("parentchain start block to be a valid number"); - - println!( - "*** [+] last_synced_header: {}, config.parentchain_start_block: {}", - last_synced_header.number, parentchain_start_block - ); - - // ------------------------------------------------------------------------ - // Initialize the sidechain - let last_synced_header = sidechain_init_block_production( - enclave.clone(), - register_enclave_xt_header, - is_primary_enclave, - parentchain_handler.clone(), - sidechain_storage, - &last_synced_header, - parentchain_start_block, - config.clone().fail_slot_mode, - config.fail_at, - ) - .unwrap(); + println!("*** [+] Finished initializing light client, syncing parentchain..."); - start_parentchain_header_subscription_thread(parentchain_handler, last_synced_header); + // Syncing all parentchain blocks, this might take a while.. + let last_synced_header = + parentchain_handler.sync_parentchain(last_synced_header, 0, true).unwrap(); - init_provided_shard_vault(shard, &enclave, is_primary_enclave); + start_parentchain_header_subscription_thread(parentchain_handler, last_synced_header); - spawn_worker_for_shard_polling(shard, litentry_rpc_api.clone(), initialization_handler); - }, - } + info!("skipping shard vault check because not yet supported for offchain worker"); if let Some(url) = config.target_a_parentchain_rpc_endpoint() { init_target_parentchain( diff --git a/bitacross-worker/service/src/ocall_bridge/bridge_api.rs b/bitacross-worker/service/src/ocall_bridge/bridge_api.rs index 71899760c1..085b9b9fb4 100644 --- a/bitacross-worker/service/src/ocall_bridge/bridge_api.rs +++ b/bitacross-worker/service/src/ocall_bridge/bridge_api.rs @@ -52,14 +52,6 @@ impl Bridge { .get_ra_api() } - pub fn get_sidechain_api() -> Arc { - COMPONENT_FACTORY - .read() - .as_ref() - .expect("Component factory has not been set. Use `initialize()`") - .get_sidechain_api() - } - pub fn get_oc_api() -> Arc { debug!("Requesting WorkerOnChain OCall API instance"); @@ -101,9 +93,6 @@ pub trait GetOCallBridgeComponents { /// remote attestation OCall API fn get_ra_api(&self) -> Arc; - /// side chain OCall API - fn get_sidechain_api(&self) -> Arc; - /// on chain (parentchain) OCall API fn get_oc_api(&self) -> Arc; @@ -223,23 +212,6 @@ pub trait MetricsBridge { fn update_metric(&self, metric_encoded: Vec) -> OCallBridgeResult<()>; } -/// Trait for all the OCalls related to sidechain operations -#[cfg_attr(test, automock)] -pub trait SidechainBridge { - fn propose_sidechain_blocks(&self, signed_blocks_encoded: Vec) -> OCallBridgeResult<()>; - - fn store_sidechain_blocks(&self, signed_blocks_encoded: Vec) -> OCallBridgeResult<()>; - - fn fetch_sidechain_blocks_from_peer( - &self, - last_imported_block_hash_encoded: Vec, - maybe_until_block_hash_encoded: Vec, - shard_identifier_encoded: Vec, - ) -> OCallBridgeResult>; - - fn get_trusted_peers_urls(&self) -> OCallBridgeResult>; -} - /// type for IPFS pub type Cid = [u8; 46]; diff --git a/bitacross-worker/service/src/ocall_bridge/component_factory.rs b/bitacross-worker/service/src/ocall_bridge/component_factory.rs index e23c509101..506f50a0a3 100644 --- a/bitacross-worker/service/src/ocall_bridge/component_factory.rs +++ b/bitacross-worker/service/src/ocall_bridge/component_factory.rs @@ -21,23 +21,18 @@ use crate::{ ocall_bridge::{ bridge_api::{ GetOCallBridgeComponents, IpfsBridge, MetricsBridge, RemoteAttestationBridge, - SidechainBridge, WorkerOnChainBridge, + WorkerOnChainBridge, }, ipfs_ocall::IpfsOCall, metrics_ocall::MetricsOCall, remote_attestation_ocall::RemoteAttestationOCall, - sidechain_ocall::SidechainOCall, worker_on_chain_ocall::WorkerOnChainOCall, }, prometheus_metrics::ReceiveEnclaveMetrics, - sync_block_broadcaster::BroadcastBlocks, worker_peers_registry::PeersRegistry, }; use itp_enclave_api::{enclave_base::EnclaveBase, remote_attestation::RemoteAttestationCallBacks}; use itp_node_api::node_api_factory::CreateNodeApi; -use its_peer_fetch::FetchBlocksFromPeer; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use its_storage::BlockStorage; use std::sync::Arc; /// Concrete implementation, should be moved out of the OCall Bridge, into the worker @@ -45,57 +40,30 @@ use std::sync::Arc; /// our dependency graph is worker -> ocall bridge pub struct OCallBridgeComponentFactory< NodeApi, - Broadcaster, EnclaveApi, - Storage, WorkerPeersRegistry, - PeerBlockFetcher, TokioHandle, MetricsReceiver, > { integritee_rpc_api_factory: Arc, target_a_parentchain_rpc_api_factory: Option>, target_b_parentchain_rpc_api_factory: Option>, - block_broadcaster: Arc, enclave_api: Arc, - block_storage: Arc, peers_registry: Arc, - peer_block_fetcher: Arc, tokio_handle: Arc, metrics_receiver: Arc, } -impl< - NodeApi, - Broadcaster, - EnclaveApi, - Storage, - WorkerPeersRegistry, - PeerBlockFetcher, - TokioHandle, - MetricsReceiver, - > - OCallBridgeComponentFactory< - NodeApi, - Broadcaster, - EnclaveApi, - Storage, - WorkerPeersRegistry, - PeerBlockFetcher, - TokioHandle, - MetricsReceiver, - > +impl + OCallBridgeComponentFactory { #[allow(clippy::too_many_arguments)] pub fn new( integritee_rpc_api_factory: Arc, target_a_parentchain_rpc_api_factory: Option>, target_b_parentchain_rpc_api_factory: Option>, - block_broadcaster: Arc, enclave_api: Arc, - block_storage: Arc, peers_registry: Arc, - peer_block_fetcher: Arc, tokio_handle: Arc, metrics_receiver: Arc, ) -> Self { @@ -103,43 +71,26 @@ impl< integritee_rpc_api_factory, target_a_parentchain_rpc_api_factory, target_b_parentchain_rpc_api_factory, - block_broadcaster, enclave_api, - block_storage, peers_registry, - peer_block_fetcher, tokio_handle, metrics_receiver, } } } -impl< - NodeApi, - Broadcaster, - EnclaveApi, - Storage, - WorkerPeersRegistry, - PeerBlockFetcher, - TokioHandle, - MetricsReceiver, - > GetOCallBridgeComponents +impl + GetOCallBridgeComponents for OCallBridgeComponentFactory< NodeApi, - Broadcaster, EnclaveApi, - Storage, WorkerPeersRegistry, - PeerBlockFetcher, TokioHandle, MetricsReceiver, > where NodeApi: CreateNodeApi + 'static, - Broadcaster: BroadcastBlocks + 'static, EnclaveApi: EnclaveBase + RemoteAttestationCallBacks + 'static, - Storage: BlockStorage + 'static, WorkerPeersRegistry: PeersRegistry + 'static, - PeerBlockFetcher: FetchBlocksFromPeer + 'static, TokioHandle: GetTokioHandle + 'static, MetricsReceiver: ReceiveEnclaveMetrics + 'static, { @@ -147,16 +98,6 @@ impl< Arc::new(RemoteAttestationOCall::new(self.enclave_api.clone())) } - fn get_sidechain_api(&self) -> Arc { - Arc::new(SidechainOCall::new( - self.block_broadcaster.clone(), - self.block_storage.clone(), - self.peers_registry.clone(), - self.peer_block_fetcher.clone(), - self.tokio_handle.clone(), - )) - } - fn get_oc_api(&self) -> Arc { Arc::new(WorkerOnChainOCall::new( self.enclave_api.clone(), diff --git a/bitacross-worker/service/src/ocall_bridge/ffi/fetch_sidechain_blocks_from_peer.rs b/bitacross-worker/service/src/ocall_bridge/ffi/fetch_sidechain_blocks_from_peer.rs deleted file mode 100644 index c6c8b9e89e..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/ffi/fetch_sidechain_blocks_from_peer.rs +++ /dev/null @@ -1,193 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::ocall_bridge::bridge_api::{Bridge, SidechainBridge}; -use itp_utils::write_slice_and_whitespace_pad; -use log::*; -use sgx_types::sgx_status_t; -use std::{slice, sync::Arc}; - -/// # Safety -/// -/// FFI are always unsafe -#[no_mangle] -pub unsafe extern "C" fn ocall_fetch_sidechain_blocks_from_peer( - last_imported_block_hash_ptr: *const u8, - last_imported_block_hash_size: u32, - maybe_until_block_hash_ptr: *const u8, - maybe_until_block_hash_size: u32, - shard_identifier_ptr: *const u8, - shard_identifier_size: u32, - sidechain_blocks_ptr: *mut u8, - sidechain_blocks_size: u32, -) -> sgx_status_t { - fetch_sidechain_blocks_from_peer( - last_imported_block_hash_ptr, - last_imported_block_hash_size, - maybe_until_block_hash_ptr, - maybe_until_block_hash_size, - shard_identifier_ptr, - shard_identifier_size, - sidechain_blocks_ptr, - sidechain_blocks_size, - Bridge::get_sidechain_api(), - ) -} - -#[allow(clippy::too_many_arguments)] -fn fetch_sidechain_blocks_from_peer( - last_imported_block_hash_ptr: *const u8, - last_imported_block_hash_size: u32, - maybe_until_block_hash_ptr: *const u8, - maybe_until_block_hash_size: u32, - shard_identifier_ptr: *const u8, - shard_identifier_size: u32, - sidechain_blocks_ptr: *mut u8, - sidechain_blocks_size: u32, - sidechain_api: Arc, -) -> sgx_status_t { - let last_imported_block_hash_encoded = unsafe { - Vec::from(slice::from_raw_parts( - last_imported_block_hash_ptr, - last_imported_block_hash_size as usize, - )) - }; - let maybe_until_block_hash = unsafe { - Vec::from(slice::from_raw_parts( - maybe_until_block_hash_ptr, - maybe_until_block_hash_size as usize, - )) - }; - let shard_identifier_encoded = unsafe { - Vec::from(slice::from_raw_parts(shard_identifier_ptr, shard_identifier_size as usize)) - }; - - let sidechain_blocks_encoded = match sidechain_api.fetch_sidechain_blocks_from_peer( - last_imported_block_hash_encoded, - maybe_until_block_hash, - shard_identifier_encoded, - ) { - Ok(r) => r, - Err(e) => { - error!("fetch sidechain blocks from peer failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let sidechain_blocks_encoded_slice = - unsafe { slice::from_raw_parts_mut(sidechain_blocks_ptr, sidechain_blocks_size as usize) }; - if let Err(e) = - write_slice_and_whitespace_pad(sidechain_blocks_encoded_slice, sidechain_blocks_encoded) - { - error!("Failed to transfer encoded sidechain blocks to o-call buffer: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - } - - sgx_status_t::SGX_SUCCESS -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::ocall_bridge::test::mocks::sidechain_bridge_mock::SidechainBridgeMock; - use codec::{Decode, Encode}; - use its_primitives::types::block::SignedBlock; - use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; - use primitive_types::H256; - - #[test] - fn fetch_sidechain_blocks_from_peer_works() { - let sidechain_blocks = vec![ - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - ]; - - let sidechain_bridge_mock = - Arc::new(SidechainBridgeMock::default().with_peer_blocks(sidechain_blocks.encode())); - - let last_known_block_hash = H256::random(); - let shard_identifier = H256::random(); - let mut block_buffer = vec![0; 16 * 4096]; - - let result = call_fetch_sidechain_blocks_from_peer( - last_known_block_hash, - None, - shard_identifier, - &mut block_buffer, - sidechain_bridge_mock, - ); - - let decoded_blocks: Vec = - Decode::decode(&mut block_buffer.as_slice()).unwrap(); - - assert_eq!(result, sgx_status_t::SGX_SUCCESS); - assert_eq!(sidechain_blocks, decoded_blocks); - } - - #[test] - fn returns_error_if_buffer_is_too_small() { - let sidechain_blocks = vec![ - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - ]; - - let sidechain_bridge_mock = - Arc::new(SidechainBridgeMock::default().with_peer_blocks(sidechain_blocks.encode())); - - let last_known_block_hash = H256::random(); - let shard_identifier = H256::random(); - let mut block_buffer = vec![0; 16]; // way too small to hold the encoded blocks - - let result = call_fetch_sidechain_blocks_from_peer( - last_known_block_hash, - None, - shard_identifier, - &mut block_buffer, - sidechain_bridge_mock, - ); - - assert_eq!(result, sgx_status_t::SGX_ERROR_UNEXPECTED); - } - - fn call_fetch_sidechain_blocks_from_peer( - last_imported_block_hash: H256, - maybe_until_block_hash: Option, - shard_identifier: H256, - buffer: &mut Vec, - sidechain_bridge: Arc, - ) -> sgx_status_t { - let last_imported_block_hash_encoded = last_imported_block_hash.encode(); - let maybe_until_block_hash_encoded = maybe_until_block_hash.encode(); - let shard_identifier_encoded = shard_identifier.encode(); - - fetch_sidechain_blocks_from_peer( - last_imported_block_hash_encoded.as_ptr(), - last_imported_block_hash_encoded.len() as u32, - maybe_until_block_hash_encoded.as_ptr(), - maybe_until_block_hash_encoded.len() as u32, - shard_identifier_encoded.as_ptr(), - shard_identifier_encoded.len() as u32, - buffer.as_mut_ptr(), - buffer.len() as u32, - sidechain_bridge, - ) - } -} diff --git a/bitacross-worker/service/src/ocall_bridge/ffi/get_peers.rs b/bitacross-worker/service/src/ocall_bridge/ffi/get_peers.rs deleted file mode 100644 index 2cc380d6e4..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/ffi/get_peers.rs +++ /dev/null @@ -1,37 +0,0 @@ -use crate::ocall_bridge::bridge_api::{Bridge, SidechainBridge}; -use itp_utils::write_slice_and_whitespace_pad; -use log::*; -use sgx_types::sgx_status_t; -use std::{slice, sync::Arc}; - -#[no_mangle] -pub unsafe extern "C" fn ocall_get_trusted_peers_urls( - peers_ptr: *mut u8, - peers_size: u32, -) -> sgx_status_t { - get_trusted_peers_urls(peers_ptr, peers_size, Bridge::get_sidechain_api()) -} - -fn get_trusted_peers_urls( - peers_ptr: *mut u8, - peers_size: u32, - sidechain_api: Arc, -) -> sgx_status_t { - debug!(" Entering ocall_get_trusted_peers_urls"); - - let peers_encoded = match sidechain_api.get_trusted_peers_urls() { - Ok(r) => r, - Err(e) => { - error!("get peers failed: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - }, - }; - - let peers_encoded_slice = unsafe { slice::from_raw_parts_mut(peers_ptr, peers_size as usize) }; - if let Err(e) = write_slice_and_whitespace_pad(peers_encoded_slice, peers_encoded) { - error!("Failed to transfer encoded peers to o-call buffer: {:?}", e); - return sgx_status_t::SGX_ERROR_UNEXPECTED - } - - sgx_status_t::SGX_SUCCESS -} diff --git a/bitacross-worker/service/src/ocall_bridge/ffi/mod.rs b/bitacross-worker/service/src/ocall_bridge/ffi/mod.rs index d146db1046..b18f24bf15 100644 --- a/bitacross-worker/service/src/ocall_bridge/ffi/mod.rs +++ b/bitacross-worker/service/src/ocall_bridge/ffi/mod.rs @@ -21,16 +21,12 @@ //! These should just be wrappers that transform the C-API structures and call the //! actual implementation of the OCalls (using the traits defined in the bridge_api). -pub mod fetch_sidechain_blocks_from_peer; pub mod get_ias_socket; -pub mod get_peers; pub mod get_quote; pub mod get_qve_report_on_quote; pub mod get_update_info; pub mod init_quote; pub mod ipfs; -pub mod propose_sidechain_blocks; pub mod send_to_parentchain; -pub mod store_sidechain_blocks; pub mod update_metric; pub mod worker_request; diff --git a/bitacross-worker/service/src/ocall_bridge/ffi/propose_sidechain_blocks.rs b/bitacross-worker/service/src/ocall_bridge/ffi/propose_sidechain_blocks.rs deleted file mode 100644 index 21ff07d0bb..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/ffi/propose_sidechain_blocks.rs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::ocall_bridge::bridge_api::{Bridge, SidechainBridge}; -use log::*; -use sgx_types::sgx_status_t; -use std::{slice, sync::Arc}; - -/// # Safety -/// -/// FFI are always unsafe -#[no_mangle] -pub unsafe extern "C" fn ocall_propose_sidechain_blocks( - signed_blocks_ptr: *const u8, - signed_blocks_size: u32, -) -> sgx_status_t { - propose_sidechain_blocks(signed_blocks_ptr, signed_blocks_size, Bridge::get_sidechain_api()) -} - -fn propose_sidechain_blocks( - signed_blocks_ptr: *const u8, - signed_blocks_size: u32, - sidechain_api: Arc, -) -> sgx_status_t { - let signed_blocks_vec: Vec = - unsafe { Vec::from(slice::from_raw_parts(signed_blocks_ptr, signed_blocks_size as usize)) }; - - match sidechain_api.propose_sidechain_blocks(signed_blocks_vec) { - Ok(_) => sgx_status_t::SGX_SUCCESS, - Err(e) => { - error!("send sidechain blocks failed: {:?}", e); - sgx_status_t::SGX_ERROR_UNEXPECTED - }, - } -} diff --git a/bitacross-worker/service/src/ocall_bridge/ffi/store_sidechain_blocks.rs b/bitacross-worker/service/src/ocall_bridge/ffi/store_sidechain_blocks.rs deleted file mode 100644 index 70361d8fd7..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/ffi/store_sidechain_blocks.rs +++ /dev/null @@ -1,50 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::ocall_bridge::bridge_api::{Bridge, SidechainBridge}; -use log::*; -use sgx_types::sgx_status_t; -use std::{slice, sync::Arc}; - -/// # Safety -/// -/// FFI are always unsafe -#[no_mangle] -pub unsafe extern "C" fn ocall_store_sidechain_blocks( - signed_blocks_ptr: *const u8, - signed_blocks_size: u32, -) -> sgx_status_t { - store_sidechain_blocks(signed_blocks_ptr, signed_blocks_size, Bridge::get_sidechain_api()) -} - -fn store_sidechain_blocks( - signed_blocks_ptr: *const u8, - signed_blocks_size: u32, - sidechain_api: Arc, -) -> sgx_status_t { - let signed_blocks_vec: Vec = - unsafe { Vec::from(slice::from_raw_parts(signed_blocks_ptr, signed_blocks_size as usize)) }; - - match sidechain_api.store_sidechain_blocks(signed_blocks_vec) { - Ok(_) => sgx_status_t::SGX_SUCCESS, - Err(e) => { - error!("store sidechain blocks failed: {:?}", e); - sgx_status_t::SGX_ERROR_UNEXPECTED - }, - } -} diff --git a/bitacross-worker/service/src/ocall_bridge/mod.rs b/bitacross-worker/service/src/ocall_bridge/mod.rs index 91a5f8887f..db5775cec3 100644 --- a/bitacross-worker/service/src/ocall_bridge/mod.rs +++ b/bitacross-worker/service/src/ocall_bridge/mod.rs @@ -25,8 +25,4 @@ mod ffi; mod ipfs_ocall; mod metrics_ocall; mod remote_attestation_ocall; -mod sidechain_ocall; mod worker_on_chain_ocall; - -#[cfg(test)] -pub mod test; diff --git a/bitacross-worker/service/src/ocall_bridge/sidechain_ocall.rs b/bitacross-worker/service/src/ocall_bridge/sidechain_ocall.rs deleted file mode 100644 index 667901cced..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/sidechain_ocall.rs +++ /dev/null @@ -1,282 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::{ - globals::tokio_handle::GetTokioHandle, - ocall_bridge::bridge_api::{OCallBridgeError, OCallBridgeResult, SidechainBridge}, - sync_block_broadcaster::BroadcastBlocks, - worker_peers_registry::PeersRegistry, -}; -use codec::{Decode, Encode}; -use itp_types::{BlockHash, ShardIdentifier}; -use its_peer_fetch::FetchBlocksFromPeer; -use its_primitives::{traits::Block, types::SignedBlock as SignedSidechainBlock}; -use its_storage::BlockStorage; -use log::*; -use std::sync::Arc; - -pub struct SidechainOCall< - BlockBroadcaster, - Storage, - WorkerPeerRegistry, - PeerBlockFetcher, - TokioHandle, -> { - block_broadcaster: Arc, - block_storage: Arc, - peer_registry: Arc, - peer_block_fetcher: Arc, - tokio_handle: Arc, -} - -impl - SidechainOCall -{ - pub fn new( - block_broadcaster: Arc, - block_storage: Arc, - peer_registry: Arc, - peer_block_fetcher: Arc, - tokio_handle: Arc, - ) -> Self { - SidechainOCall { - block_broadcaster, - block_storage, - peer_registry, - peer_block_fetcher, - tokio_handle, - } - } -} - -impl SidechainBridge - for SidechainOCall -where - BlockBroadcaster: BroadcastBlocks, - Storage: BlockStorage, - WorkerPeerRegistry: PeersRegistry, - PeerBlockFetcher: FetchBlocksFromPeer, - TokioHandle: GetTokioHandle, -{ - fn propose_sidechain_blocks(&self, signed_blocks_encoded: Vec) -> OCallBridgeResult<()> { - // TODO: improve error handling, using a mut status is not good design? - let mut status: OCallBridgeResult<()> = Ok(()); - - // handle blocks - let signed_blocks: Vec = - match Decode::decode(&mut signed_blocks_encoded.as_slice()) { - Ok(blocks) => blocks, - Err(_) => { - status = Err(OCallBridgeError::ProposeSidechainBlock( - "Could not decode signed blocks".to_string(), - )); - vec![] - }, - }; - - if !signed_blocks.is_empty() { - info!( - "Enclave produced sidechain blocks: {:?}", - signed_blocks - .iter() - .map(|b| b.block.header().block_number) - .collect::>() - ); - } else { - debug!("Enclave did not produce sidechain blocks"); - } - - // FIXME: When & where should peers be updated? - debug!("Updating peers.."); - if let Err(e) = self.peer_registry.update_peers() { - error!("Error updating peers: {:?}", e); - // Fixme: returning an error here results in a `HeaderAncestryMismatch` error. - // status = sgx_status_t::SGX_ERROR_UNEXPECTED; - } else { - info!("Successfully updated peers"); - } - - debug!("Broadcasting sidechain blocks ..."); - if let Err(e) = self.block_broadcaster.broadcast_blocks(signed_blocks) { - error!("Error broadcasting blocks: {:?}", e); - // Fixme: returning an error here results in a `HeaderAncestryMismatch` error. - // status = sgx_status_t::SGX_ERROR_UNEXPECTED; - } else { - info!("Successfully broadcast blocks"); - } - - status - } - - fn store_sidechain_blocks(&self, signed_blocks_encoded: Vec) -> OCallBridgeResult<()> { - // TODO: improve error handling, using a mut status is not good design? - let mut status: OCallBridgeResult<()> = Ok(()); - - let signed_blocks: Vec = - match Decode::decode(&mut signed_blocks_encoded.as_slice()) { - Ok(blocks) => blocks, - Err(_) => { - status = Err(OCallBridgeError::ProposeSidechainBlock( - "Could not decode signed blocks".to_string(), - )); - vec![] - }, - }; - - if let Err(e) = self.block_storage.store_blocks(signed_blocks) { - error!("Error storing blocks: {:?}", e); - } - - status - } - - fn fetch_sidechain_blocks_from_peer( - &self, - last_imported_block_hash_encoded: Vec, - maybe_until_block_hash_encoded: Vec, - shard_identifier_encoded: Vec, - ) -> OCallBridgeResult> { - let last_imported_block_hash: BlockHash = - Decode::decode(&mut last_imported_block_hash_encoded.as_slice()).map_err(|_| { - OCallBridgeError::FetchSidechainBlocksFromPeer( - "Failed to decode last imported block hash".to_string(), - ) - })?; - - let maybe_until_block_hash: Option = - Decode::decode(&mut maybe_until_block_hash_encoded.as_slice()).map_err(|_| { - OCallBridgeError::FetchSidechainBlocksFromPeer( - "Failed to decode optional until block hash".to_string(), - ) - })?; - - let shard_identifier: ShardIdentifier = - Decode::decode(&mut shard_identifier_encoded.as_slice()).map_err(|_| { - OCallBridgeError::FetchSidechainBlocksFromPeer( - "Failed to decode shard identifier".to_string(), - ) - })?; - - info!("[O-call] fetching blocks from peer.."); - - let tokio_handle = self.tokio_handle.get_handle(); - - let signed_sidechain_blocks = tokio_handle - .block_on(self.peer_block_fetcher.fetch_blocks_from_peer( - last_imported_block_hash, - maybe_until_block_hash, - shard_identifier, - )) - .map_err(|e| { - OCallBridgeError::FetchSidechainBlocksFromPeer(format!( - "Failed to execute block fetching from peer: {:?}", - e - )) - })?; - - info!("[O-call] successfully fetched {} blocks from peer", signed_sidechain_blocks.len()); - - Ok(signed_sidechain_blocks.encode()) - } - - fn get_trusted_peers_urls(&self) -> OCallBridgeResult> { - let peers = self.peer_registry.read_trusted_peers().unwrap(); - Ok(peers.encode()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - globals::tokio_handle::ScopedTokioHandle, - tests::mocks::{ - broadcast_blocks_mock::BroadcastBlocksMock, - update_worker_peers_mock::WorkerPeersRegistryMock, - }, - }; - use codec::Decode; - use its_peer_fetch::mocks::fetch_blocks_from_peer_mock::FetchBlocksFromPeerMock; - use its_primitives::types::block::SignedBlock as SignedSidechainBlock; - use its_storage::{interface::BlockStorage, Result as StorageResult}; - use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; - use primitive_types::H256; - use std::{collections::HashMap, vec::Vec}; - - struct BlockStorageMock; - impl BlockStorage for BlockStorageMock { - fn store_blocks(&self, _blocks: Vec) -> StorageResult<()> { - Ok(()) - } - } - - type TestSidechainOCall = SidechainOCall< - BroadcastBlocksMock, - BlockStorageMock, - WorkerPeersRegistryMock, - FetchBlocksFromPeerMock, - ScopedTokioHandle, - >; - - #[test] - fn fetch_sidechain_blocks_from_peer_works() { - let last_imported_block_hash = H256::random(); - let until_block_hash: Option = None; - let shard_identifier = H256::random(); - let blocks = vec![ - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - ]; - let peer_blocks_map = HashMap::from([(shard_identifier, blocks.clone())]); - let sidechain_ocall = setup_sidechain_ocall_with_peer_blocks(peer_blocks_map); - - let fetched_blocks_encoded = sidechain_ocall - .fetch_sidechain_blocks_from_peer( - last_imported_block_hash.encode(), - until_block_hash.encode(), - shard_identifier.encode(), - ) - .unwrap(); - - let fetched_blocks_decoded: Vec = - Decode::decode(&mut fetched_blocks_encoded.as_slice()).unwrap(); - - assert_eq!(blocks, fetched_blocks_decoded); - } - - fn setup_sidechain_ocall_with_peer_blocks( - peer_blocks_map: HashMap>, - ) -> TestSidechainOCall { - let block_broadcaster_mock = Arc::new(BroadcastBlocksMock {}); - let block_storage_mock = Arc::new(BlockStorageMock {}); - let worker_peers_registry_mock = Arc::new(WorkerPeersRegistryMock {}); - let peer_block_fetcher_mock = Arc::new( - FetchBlocksFromPeerMock::::default() - .with_signed_blocks(peer_blocks_map), - ); - let scoped_tokio_handle = Arc::new(ScopedTokioHandle::default()); - - SidechainOCall::new( - block_broadcaster_mock, - block_storage_mock, - worker_peers_registry_mock, - peer_block_fetcher_mock, - scoped_tokio_handle, - ) - } -} diff --git a/bitacross-worker/service/src/ocall_bridge/test/mocks/mod.rs b/bitacross-worker/service/src/ocall_bridge/test/mocks/mod.rs deleted file mode 100644 index 298b05435a..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/test/mocks/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -pub mod sidechain_bridge_mock; diff --git a/bitacross-worker/service/src/ocall_bridge/test/mocks/sidechain_bridge_mock.rs b/bitacross-worker/service/src/ocall_bridge/test/mocks/sidechain_bridge_mock.rs deleted file mode 100644 index dc1ba7d8da..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/test/mocks/sidechain_bridge_mock.rs +++ /dev/null @@ -1,54 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -use crate::ocall_bridge::bridge_api::{OCallBridgeResult, SidechainBridge}; - -#[derive(Default)] -pub struct SidechainBridgeMock { - peer_blocks_encoded: Vec, -} - -impl SidechainBridgeMock { - pub fn with_peer_blocks(mut self, blocks_encoded: Vec) -> Self { - self.peer_blocks_encoded = blocks_encoded; - self - } -} - -impl SidechainBridge for SidechainBridgeMock { - fn propose_sidechain_blocks(&self, _signed_blocks_encoded: Vec) -> OCallBridgeResult<()> { - Ok(()) - } - - fn store_sidechain_blocks(&self, _signed_blocks_encoded: Vec) -> OCallBridgeResult<()> { - Ok(()) - } - - fn fetch_sidechain_blocks_from_peer( - &self, - _last_imported_block_hash_encoded: Vec, - _maybe_until_block_hash_encoded: Vec, - _shard_identifier_encoded: Vec, - ) -> OCallBridgeResult> { - Ok(self.peer_blocks_encoded.clone()) - } - - fn get_trusted_peers_urls(&self) -> OCallBridgeResult> { - Ok(vec![]) - } -} diff --git a/bitacross-worker/service/src/ocall_bridge/test/mod.rs b/bitacross-worker/service/src/ocall_bridge/test/mod.rs deleted file mode 100644 index 0c205a3799..0000000000 --- a/bitacross-worker/service/src/ocall_bridge/test/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -pub mod mocks; diff --git a/bitacross-worker/service/src/prometheus_metrics.rs b/bitacross-worker/service/src/prometheus_metrics.rs index 7ce1befc3a..84448f552a 100644 --- a/bitacross-worker/service/src/prometheus_metrics.rs +++ b/bitacross-worker/service/src/prometheus_metrics.rs @@ -211,16 +211,6 @@ impl ReceiveEnclaveMetrics for EnclaveMetricsReceiver { }, EnclaveMetric::ParentchainBlockImportTime(time) => ENCLAVE_PARENTCHAIN_BLOCK_IMPORT_TIME.observe(time.as_secs_f64()), - EnclaveMetric::SidechainBlockImportTime(time) => - ENCLAVE_SIDECHAIN_BLOCK_IMPORT_TIME.observe(time.as_secs_f64()), - EnclaveMetric::SidechainSlotPrepareTime(time) => - ENCLAVE_SIDECHAIN_SLOT_PREPARE_TIME.observe(time.as_secs_f64()), - EnclaveMetric::SidechainSlotStfExecutionTime(time) => - ENCLAVE_SIDECHAIN_SLOT_STF_EXECUTION_TIME.observe(time.as_secs_f64()), - EnclaveMetric::SidechainSlotBlockCompositionTime(time) => - ENCLAVE_SIDECHAIN_SLOT_BLOCK_COMPOSITION_TIME.observe(time.as_secs_f64()), - EnclaveMetric::SidechainBlockBroadcastingTime(time) => - ENCLAVE_SIDECHAIN_BLOCK_BROADCASTING_TIME.observe(time.as_secs_f64()), } Ok(()) } diff --git a/bitacross-worker/service/src/setup.rs b/bitacross-worker/service/src/setup.rs index 4bd056edd8..c636a18284 100644 --- a/bitacross-worker/service/src/setup.rs +++ b/bitacross-worker/service/src/setup.rs @@ -19,8 +19,7 @@ use crate::error::{Error, ServiceResult}; use itp_settings::files::{ LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, SCHEDULED_ENCLAVE_FILE, SHARDS_PATH, - SIDECHAIN_STORAGE_PATH, TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, - TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, + TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, }; use std::{fs, path::Path}; @@ -37,7 +36,7 @@ mod needs_enclave { use itp_enclave_api::{enclave_base::EnclaveBase, Enclave}; use itp_settings::files::{ LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, SHARDS_PATH, SHIELDING_KEY_FILE, - SIDECHAIN_STORAGE_PATH, SIGNING_KEY_FILE, TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, + SIGNING_KEY_FILE, TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, }; use itp_types::ShardIdentifier; @@ -139,7 +138,6 @@ pub(crate) fn purge_files_from_dir(dir: &Path) -> ServiceResult<()> { /// Purge all worker files in a given path. fn purge_files(root_directory: &Path) -> ServiceResult<()> { remove_dir_if_it_exists(root_directory, SHARDS_PATH)?; - remove_dir_if_it_exists(root_directory, SIDECHAIN_STORAGE_PATH)?; remove_dir_if_it_exists(root_directory, LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)?; remove_dir_if_it_exists(root_directory, TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)?; @@ -182,12 +180,6 @@ mod tests { fs::File::create(&shards_path.join("state_1.bin")).unwrap(); fs::File::create(&shards_path.join("state_2.bin")).unwrap(); - let sidechain_db_path = root_directory.join(SIDECHAIN_STORAGE_PATH); - fs::create_dir_all(&sidechain_db_path).unwrap(); - fs::File::create(&sidechain_db_path.join("sidechain_db_1.bin")).unwrap(); - fs::File::create(&sidechain_db_path.join("sidechain_db_2.bin")).unwrap(); - fs::File::create(&sidechain_db_path.join("sidechain_db_3.bin")).unwrap(); - fs::create_dir_all(&root_directory.join(LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)) .unwrap(); fs::create_dir_all(&root_directory.join(TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)) @@ -198,7 +190,6 @@ mod tests { purge_files(&root_directory).unwrap(); assert!(!shards_path.exists()); - assert!(!sidechain_db_path.exists()); assert!(!root_directory.join(LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); assert!(!root_directory.join(TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); assert!(!root_directory.join(TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); diff --git a/bitacross-worker/service/src/sidechain_setup.rs b/bitacross-worker/service/src/sidechain_setup.rs deleted file mode 100644 index 55a17cdbe1..0000000000 --- a/bitacross-worker/service/src/sidechain_setup.rs +++ /dev/null @@ -1,129 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - config::Config, - error::{Error, ServiceResult}, - parentchain_handler::HandleParentchain, -}; -use futures::executor::block_on; -use itp_enclave_api::{ - direct_request::DirectRequest, enclave_base::EnclaveBase, sidechain::Sidechain, -}; -use itp_settings::{ - files::{SIDECHAIN_PURGE_INTERVAL, SIDECHAIN_PURGE_LIMIT}, - sidechain::SLOT_DURATION, -}; -use itp_types::Header; -use its_consensus_slots::start_slot_worker; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use its_storage::{interface::FetchBlocks, start_sidechain_pruning_loop, BlockPruner}; -use log::*; -use std::{sync::Arc, thread}; -use tokio::runtime::Handle; - -pub(crate) fn sidechain_start_untrusted_rpc_server( - config: &Config, - enclave: Arc, - sidechain_storage: Arc, - tokio_handle: &Handle, -) where - Enclave: DirectRequest + Clone, - SidechainStorage: BlockPruner + FetchBlocks + Sync + Send + 'static, -{ - let untrusted_url = config.untrusted_worker_url(); - println!("[+] Untrusted RPC server listening on {}", &untrusted_url); - let _untrusted_rpc_join_handle = tokio_handle.spawn(async move { - itc_rpc_server::run_server(&untrusted_url, enclave, sidechain_storage) - .await - .unwrap(); - }); -} - -#[allow(clippy::too_many_arguments)] -pub(crate) fn sidechain_init_block_production( - enclave: Arc, - register_enclave_xt_header: Header, - we_are_primary_validateer: bool, - parentchain_handler: Arc, - sidechain_storage: Arc, - last_synced_header: &Header, - overriden_start_block: u32, - fail_mode: Option, - fail_at: u64, -) -> ServiceResult
-where - Enclave: EnclaveBase + Sidechain, - SidechainStorage: BlockPruner + FetchBlocks + Sync + Send + 'static, - ParentchainHandler: HandleParentchain, -{ - // If we're the first validateer to register, also trigger parentchain block import. - let mut updated_header: Option
= None; - - if we_are_primary_validateer { - info!( - "We're the first validateer to be registered, syncing parentchain blocks until the one we have registered ourselves on." - ); - updated_header = Some(parentchain_handler.sync_and_import_parentchain_until( - last_synced_header, - ®ister_enclave_xt_header, - overriden_start_block, - )?); - } - - // ------------------------------------------------------------------------ - // Initialize sidechain components (has to be AFTER init_parentchain_components() - enclave.init_enclave_sidechain_components(fail_mode, fail_at).unwrap(); - - // ------------------------------------------------------------------------ - // Start interval sidechain block production (execution of trusted calls, sidechain block production). - let sidechain_enclave_api = enclave; - println!("[+] Spawning thread for sidechain block production"); - thread::Builder::new() - .name("interval_block_production_timer".to_owned()) - .spawn(move || { - let future = start_slot_worker( - || execute_trusted_calls(sidechain_enclave_api.as_ref()), - SLOT_DURATION, - ); - block_on(future); - println!("[!] Sidechain block production loop has terminated"); - }) - .map_err(|e| Error::Custom(Box::new(e)))?; - - // ------------------------------------------------------------------------ - // start sidechain pruning loop - thread::Builder::new() - .name("sidechain_pruning_loop".to_owned()) - .spawn(move || { - start_sidechain_pruning_loop( - &sidechain_storage, - SIDECHAIN_PURGE_INTERVAL, - SIDECHAIN_PURGE_LIMIT, - ); - }) - .map_err(|e| Error::Custom(Box::new(e)))?; - - Ok(updated_header.unwrap_or_else(|| last_synced_header.clone())) -} - -/// Execute trusted operations in the enclave. -fn execute_trusted_calls(enclave_api: &E) { - if let Err(e) = enclave_api.execute_trusted_calls() { - error!("{:?}", e); - }; -} diff --git a/bitacross-worker/service/src/sync_block_broadcaster.rs b/bitacross-worker/service/src/sync_block_broadcaster.rs deleted file mode 100644 index b0752c900d..0000000000 --- a/bitacross-worker/service/src/sync_block_broadcaster.rs +++ /dev/null @@ -1,57 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -#[cfg(test)] -use mockall::predicate::*; -#[cfg(test)] -use mockall::*; - -use crate::{ - globals::tokio_handle::GetTokioHandle, - worker::{AsyncBlockBroadcaster, WorkerResult}, -}; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use std::sync::Arc; - -/// Allows to broadcast blocks, does it in a synchronous (i.e. blocking) manner -#[cfg_attr(test, automock)] -pub trait BroadcastBlocks { - fn broadcast_blocks(&self, blocks: Vec) -> WorkerResult<()>; -} - -pub struct SyncBlockBroadcaster { - tokio_handle: Arc, - worker: Arc, -} - -impl SyncBlockBroadcaster { - pub fn new(tokio_handle: Arc, worker: Arc) -> Self { - SyncBlockBroadcaster { tokio_handle, worker } - } -} - -impl BroadcastBlocks for SyncBlockBroadcaster -where - T: GetTokioHandle, - W: AsyncBlockBroadcaster, -{ - fn broadcast_blocks(&self, blocks: Vec) -> WorkerResult<()> { - let handle = self.tokio_handle.get_handle(); - handle.block_on(self.worker.broadcast_blocks(blocks)) - } -} diff --git a/bitacross-worker/service/src/sync_state.rs b/bitacross-worker/service/src/sync_state.rs index e17545d770..287721e687 100644 --- a/bitacross-worker/service/src/sync_state.rs +++ b/bitacross-worker/service/src/sync_state.rs @@ -27,7 +27,6 @@ use itp_enclave_api::{ remote_attestation::{RemoteAttestation, TlsRemoteAttestation}, }; use itp_node_api::api_client::PalletTeebagApi; -use itp_settings::worker_mode::{ProvideWorkerMode, WorkerMode}; use itp_types::{ShardIdentifier, WorkerType}; use sgx_types::sgx_quote_sign_type_t; use std::string::String; @@ -35,7 +34,6 @@ use std::string::String; pub(crate) fn sync_state< E: TlsRemoteAttestation + EnclaveBase + RemoteAttestation, NodeApi: PalletTeebagApi, - WorkerModeProvider: ProvideWorkerMode, >( node_api: &NodeApi, shard: &ShardIdentifier, @@ -43,13 +41,9 @@ pub(crate) fn sync_state< skip_ra: bool, ) { // FIXME: we now assume that keys are equal for all shards. - let provider_url = match WorkerModeProvider::worker_mode() { - WorkerMode::Sidechain => - executor::block_on(get_author_url_of_last_finalized_sidechain_block(node_api, shard)) - .expect("Author of last finalized sidechain block could not be found"), - _ => executor::block_on(get_enclave_url_of_first_registered(node_api, enclave_api)) - .expect("Author of last finalized sidechain block could not be found"), - }; + let provider_url = + executor::block_on(get_enclave_url_of_first_registered(node_api, enclave_api)) + .expect("Author of last finalized sidechain block could not be found"); println!("Requesting state provisioning from worker at {}", &provider_url); @@ -64,24 +58,6 @@ pub(crate) fn sync_state< println!("[+] State provisioning successfully performed."); } -/// Returns the url of the last sidechain block author that has been stored -/// in the parentchain state as "worker for shard". -/// -/// Note: The sidechainblock author will only change whenever a new parentchain block is -/// produced. And even then, it might be the same as the last block. So if several workers -/// are started in a timely manner, they will all get the same url. -async fn get_author_url_of_last_finalized_sidechain_block( - node_api: &NodeApi, - shard: &ShardIdentifier, -) -> Result { - let enclave = node_api - .primary_enclave_for_shard(WorkerType::BitAcross, shard, None)? - .ok_or_else(|| Error::NoWorkerForShardFound(*shard))?; - let worker_api_direct = - DirectWorkerApi::new(String::from_utf8_lossy(enclave.url.as_slice()).to_string()); - Ok(worker_api_direct.get_mu_ra_url()?) -} - /// Returns the url of the first Enclave that matches our own MRENCLAVE. /// /// This should be run before we register ourselves as enclave, to ensure we don't get our own url. diff --git a/bitacross-worker/service/src/tests/commons.rs b/bitacross-worker/service/src/tests/commons.rs index df6b5c9172..068576e8d7 100644 --- a/bitacross-worker/service/src/tests/commons.rs +++ b/bitacross-worker/service/src/tests/commons.rs @@ -57,7 +57,5 @@ pub fn local_worker_config( crate::config::pwd(), None, "0".to_string(), - None, - 0, ) } diff --git a/bitacross-worker/service/src/tests/mocks/broadcast_blocks_mock.rs b/bitacross-worker/service/src/tests/mocks/broadcast_blocks_mock.rs deleted file mode 100644 index 2df5f65506..0000000000 --- a/bitacross-worker/service/src/tests/mocks/broadcast_blocks_mock.rs +++ /dev/null @@ -1,28 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{sync_block_broadcaster::BroadcastBlocks, worker::WorkerResult}; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use std::vec::Vec; - -pub struct BroadcastBlocksMock; - -impl BroadcastBlocks for BroadcastBlocksMock { - fn broadcast_blocks(&self, _blocks: Vec) -> WorkerResult<()> { - Ok(()) - } -} diff --git a/bitacross-worker/service/src/tests/mocks/direct_request_mock.rs b/bitacross-worker/service/src/tests/mocks/direct_request_mock.rs deleted file mode 100644 index a2c572dfc6..0000000000 --- a/bitacross-worker/service/src/tests/mocks/direct_request_mock.rs +++ /dev/null @@ -1,26 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use itp_enclave_api::{direct_request::DirectRequest, EnclaveResult}; - -pub struct DirectRequestMock; - -impl DirectRequest for DirectRequestMock { - fn rpc(&self, request: Vec) -> EnclaveResult> { - Ok(request) - } -} diff --git a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs index 565f33df5a..a1de0ba1c8 100644 --- a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs +++ b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs @@ -36,11 +36,7 @@ impl EnclaveBase for EnclaveMock { Ok(()) } - fn init_enclave_sidechain_components( - &self, - _fail_mode: Option, - _fail_at: u64, - ) -> EnclaveResult<()> { + fn init_enclave_sidechain_components(&self) -> EnclaveResult<()> { Ok(()) } @@ -117,10 +113,6 @@ impl Sidechain for EnclaveMock { Ok(()) } - fn execute_trusted_calls(&self) -> EnclaveResult<()> { - todo!() - } - fn ignore_parentchain_block_import_validation_until(&self, _until: u32) -> EnclaveResult<()> { todo!() } diff --git a/bitacross-worker/service/src/tests/mocks/initialization_handler_mock.rs b/bitacross-worker/service/src/tests/mocks/initialization_handler_mock.rs index e4539afc0e..79b2797d7b 100644 --- a/bitacross-worker/service/src/tests/mocks/initialization_handler_mock.rs +++ b/bitacross-worker/service/src/tests/mocks/initialization_handler_mock.rs @@ -22,8 +22,6 @@ pub struct TrackInitializationMock; impl TrackInitialization for TrackInitializationMock { fn registered_on_parentchain(&self) {} - fn sidechain_block_produced(&self) {} - fn worker_for_shard_registered(&self) {} } diff --git a/bitacross-worker/service/src/tests/mocks/mod.rs b/bitacross-worker/service/src/tests/mocks/mod.rs index cfe0d6fc76..406392ef62 100644 --- a/bitacross-worker/service/src/tests/mocks/mod.rs +++ b/bitacross-worker/service/src/tests/mocks/mod.rs @@ -15,8 +15,6 @@ */ -pub mod broadcast_blocks_mock; -pub mod direct_request_mock; pub mod enclave_api_mock; pub mod initialization_handler_mock; pub mod parentchain_api_mock; diff --git a/bitacross-worker/service/src/worker.rs b/bitacross-worker/service/src/worker.rs index db638ec998..89bc4c382f 100644 --- a/bitacross-worker/service/src/worker.rs +++ b/bitacross-worker/service/src/worker.rs @@ -26,8 +26,6 @@ use codec::{Decode, Encode}; use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; use itp_enclave_api::enclave_base::EnclaveBase; use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; -use its_primitives::types::SignedBlock as SignedSidechainBlock; -use its_rpc_handler::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; use jsonrpsee::{ types::{to_json_value, traits::Client}, ws_client::WsClientBuilder, @@ -84,67 +82,6 @@ impl } } -#[async_trait] -/// Broadcast Sidechain blocks to peers. -pub trait AsyncBlockBroadcaster { - async fn broadcast_blocks(&self, blocks: Vec) -> WorkerResult<()>; -} - -#[async_trait] -impl AsyncBlockBroadcaster - for Worker -where - NodeApiFactory: CreateNodeApi + Send + Sync, - Enclave: Send + Sync, - InitializationHandler: TrackInitialization + Send + Sync, -{ - async fn broadcast_blocks(&self, blocks: Vec) -> WorkerResult<()> { - if blocks.is_empty() { - debug!("No blocks to broadcast, returning"); - return Ok(()) - } - - let blocks_json = vec![to_json_value(blocks)?]; - let peers = self - .peer_urls - .read() - .map_err(|e| { - Error::Custom(format!("Encountered poisoned lock for peers: {:?}", e).into()) - }) - .map(|l| l.clone())?; - - self.initialization_handler.sidechain_block_produced(); - - for url in peers { - let blocks = blocks_json.clone(); - - tokio::spawn(async move { - let untrusted_peer_url = url.untrusted; - - debug!("Broadcasting block to peer with address: {:?}", untrusted_peer_url); - // FIXME: Websocket connection to a worker should stay, once established. - let client = match WsClientBuilder::default().build(&untrusted_peer_url).await { - Ok(c) => c, - Err(e) => { - error!("Failed to create websocket client for block broadcasting (target url: {}): {:?}", untrusted_peer_url, e); - return - }, - }; - - if let Err(e) = - client.request::>(RPC_METHOD_NAME_IMPORT_BLOCKS, blocks.into()).await - { - error!( - "Broadcast block request ({}) to {} failed: {:?}", - RPC_METHOD_NAME_IMPORT_BLOCKS, untrusted_peer_url, e - ); - } - }); - } - Ok(()) - } -} - /// Looks for new peers and updates them. pub trait UpdatePeers { fn search_peers(&self) -> WorkerResult>; @@ -218,81 +155,3 @@ where Ok(()) } } -#[cfg(test)] -mod tests { - use super::*; - use crate::{ - tests::{ - commons::local_worker_config, - mock::{W1_URL, W2_URL}, - mocks::initialization_handler_mock::TrackInitializationMock, - }, - worker::{AsyncBlockBroadcaster, Worker}, - }; - use frame_support::assert_ok; - use itp_node_api::node_api_factory::NodeApiFactory; - use its_primitives::types::block::SignedBlock as SignedSidechainBlock; - use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; - use jsonrpsee::{ws_server::WsServerBuilder, RpcModule}; - use log::debug; - use sp_keyring::AccountKeyring; - use std::{net::SocketAddr, sync::Arc}; - use tokio::net::ToSocketAddrs; - - fn init() { - let _ = env_logger::builder().is_test(true).try_init(); - } - - async fn run_server(addr: impl ToSocketAddrs) -> anyhow::Result { - let mut server = WsServerBuilder::default().build(addr).await?; - let mut module = RpcModule::new(()); - - module.register_method(RPC_METHOD_NAME_IMPORT_BLOCKS, |params, _| { - debug!("{} params: {:?}", RPC_METHOD_NAME_IMPORT_BLOCKS, params); - let _blocks: Vec = params.one()?; - Ok("ok".as_bytes().to_vec()) - })?; - - server.register_module(module).unwrap(); - - let socket_addr = server.local_addr()?; - tokio::spawn(async move { server.start().await }); - Ok(socket_addr) - } - - #[tokio::test] - async fn broadcast_blocks_works() { - init(); - run_server(W1_URL).await.unwrap(); - run_server(W2_URL).await.unwrap(); - let untrusted_worker_port = "4000".to_string(); - let mut peer_urls: HashSet = HashSet::new(); - - peer_urls.insert(PeerUrls { - untrusted: format!("ws://{}", W1_URL), - trusted: format!("ws://{}", W1_URL), - me: false, - }); - peer_urls.insert(PeerUrls { - untrusted: format!("ws://{}", W2_URL), - trusted: format!("ws://{}", W2_URL), - me: false, - }); - - let worker = Worker::new( - local_worker_config(W1_URL.into(), untrusted_worker_port.clone(), "30".to_string()), - Arc::new(()), - Arc::new(NodeApiFactory::new( - "ws://invalid.url".to_string(), - AccountKeyring::Alice.pair(), - )), - Arc::new(TrackInitializationMock {}), - peer_urls, - ); - - let resp = worker - .broadcast_blocks(vec![SidechainBlockBuilder::default().build_signed()]) - .await; - assert_ok!(resp); - } -} diff --git a/bitacross-worker/sidechain/block-composer/Cargo.toml b/bitacross-worker/sidechain/block-composer/Cargo.toml deleted file mode 100644 index f1be550d61..0000000000 --- a/bitacross-worker/sidechain/block-composer/Cargo.toml +++ /dev/null @@ -1,64 +0,0 @@ -[package] -name = "its-block-composer" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -# sgx dependencies -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } -sgx_types = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git" } - -# local dependencies -itp-node-api = { path = "../../core-primitives/node-api", default-features = false } -itp-settings = { path = "../../core-primitives/settings", default-features = false } -itp-sgx-crypto = { path = "../../core-primitives/sgx/crypto", default-features = false } -itp-sgx-externalities = { path = "../../core-primitives/substrate-sgx/externalities", default-features = false } -itp-stf-executor = { path = "../../core-primitives/stf-executor", default-features = false } -itp-stf-primitives = { path = "../../core-primitives/stf-primitives", default-features = false } -itp-time-utils = { path = "../../core-primitives/time-utils", default-features = false } -itp-top-pool-author = { path = "../../core-primitives/top-pool-author", default-features = false } -itp-types = { path = "../../core-primitives/types", default-features = false } -its-primitives = { path = "../primitives", default-features = false, features = ["full_crypto"] } -its-state = { path = "../state", default-features = false } - -# sgx enabled external libraries -thiserror_sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } - -# std compatible external libraries (make sure these versions match with the sgx-enabled ones above) -thiserror = { version = "1.0", optional = true } - -# no-std compatible libraries -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -log = { version = "0.4", default-features = false } -sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - - -[features] -default = ["std"] -std = [ - "itp-node-api/std", - "itp-sgx-crypto/std", - "itp-sgx-externalities/std", - "itp-stf-executor/std", - "itp-stf-primitives/std", - "itp-time-utils/std", - "itp-top-pool-author/std", - "itp-types/std", - "its-primitives/std", - "its-state/std", - "log/std", - "thiserror", -] -sgx = [ - "sgx_tstd", - "itp-node-api/sgx", - "itp-sgx-crypto/sgx", - "itp-sgx-externalities/sgx", - "itp-stf-executor/sgx", - "itp-time-utils/sgx", - "itp-top-pool-author/sgx", - "its-state/sgx", - "thiserror_sgx", -] diff --git a/bitacross-worker/sidechain/block-composer/src/block_composer.rs b/bitacross-worker/sidechain/block-composer/src/block_composer.rs deleted file mode 100644 index d87f7e61d3..0000000000 --- a/bitacross-worker/sidechain/block-composer/src/block_composer.rs +++ /dev/null @@ -1,185 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, Result}; -use codec::Encode; -use itp_settings::worker::BLOCK_NUMBER_FINALIZATION_DIFF; -use itp_sgx_crypto::{key_repository::AccessKey, StateCrypto}; -use itp_sgx_externalities::{SgxExternalitiesTrait, StateHash}; -use itp_stf_primitives::types::StatePayload; -use itp_time_utils::now_as_millis; -use itp_types::{ShardIdentifier, H256}; -use its_primitives::traits::{ - Block as SidechainBlockTrait, BlockData, Header as HeaderTrait, SignBlock, - SignedBlock as SignedSidechainBlockTrait, -}; -use its_state::{LastBlockExt, SidechainState, SidechainSystemExt}; -use log::*; -use sp_core::Pair; -use sp_runtime::{ - traits::{Block as ParentchainBlockTrait, Header}, - MultiSignature, -}; -use std::{format, marker::PhantomData, sync::Arc, vec::Vec}; - -/// Compose a sidechain block and corresponding confirmation extrinsic for the parentchain -/// -pub trait ComposeBlock { - type SignedSidechainBlock: SignedSidechainBlockTrait; - - fn compose_block( - &self, - latest_parentchain_header: &::Header, - top_call_hashes: Vec, - shard: ShardIdentifier, - state_hash_apriori: H256, - aposteriori_state: &Externalities, - ) -> Result; -} - -/// Block composer implementation for the sidechain -pub struct BlockComposer { - signer: Signer, - state_key_repository: Arc, - _phantom: PhantomData<(ParentchainBlock, SignedSidechainBlock)>, -} - -impl - BlockComposer -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait, - SignedSidechainBlock::Block: SidechainBlockTrait, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - SignedSidechainBlock::Signature: From, - Signer: Pair, - Signer::Public: Encode, - StateKeyRepository: AccessKey, - ::KeyType: StateCrypto, -{ - pub fn new(signer: Signer, state_key_repository: Arc) -> Self { - BlockComposer { signer, state_key_repository, _phantom: Default::default() } - } -} - -type HeaderTypeOf = <::Block as SidechainBlockTrait>::HeaderType; -type BlockDataTypeOf = - <::Block as SidechainBlockTrait>::BlockDataType; - -impl - ComposeBlock - for BlockComposer -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait, - SignedSidechainBlock::Block: SidechainBlockTrait, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - SignedSidechainBlock::Signature: From, - Externalities: SgxExternalitiesTrait - + SidechainState - + SidechainSystemExt - + StateHash - + LastBlockExt - + Encode, - ::SgxExternalitiesType: Encode, - ::SgxExternalitiesDiffType: Encode, - Signer: Pair, - Signer::Public: Encode, - StateKeyRepository: AccessKey, - ::KeyType: StateCrypto, -{ - type SignedSidechainBlock = SignedSidechainBlock; - - fn compose_block( - &self, - latest_parentchain_header: &ParentchainBlock::Header, - top_call_hashes: Vec, - shard: ShardIdentifier, - state_hash_apriori: H256, - aposteriori_state: &Externalities, - ) -> Result { - let author_public = self.signer.public(); - - let state_hash_new = aposteriori_state.hash(); - - let (block_number, parent_hash, next_finalization_block_number) = - match aposteriori_state.get_last_block() { - Some(block) => ( - block.header().block_number() + 1, - block.hash(), - block.header().next_finalization_block_number(), - ), - None => { - info!("Seems to be first sidechain block."); - (1, Default::default(), 1) - }, - }; - - if block_number != aposteriori_state.get_block_number().unwrap_or(0) { - return Err(Error::Other("[Sidechain] BlockNumber is not LastBlock's Number + 1".into())) - } - - // create encrypted payload - let mut payload: Vec = - StatePayload::new(state_hash_apriori, state_hash_new, aposteriori_state.state_diff()) - .encode(); - - let state_key = self - .state_key_repository - .retrieve_key() - .map_err(|e| Error::Other(format!("Failed to retrieve state key: {:?}", e).into()))?; - - state_key.encrypt(&mut payload).map_err(|e| { - Error::Other(format!("Failed to encrypt state payload: {:?}", e).into()) - })?; - - let block_data = BlockDataTypeOf::::new( - author_public, - latest_parentchain_header.hash(), - top_call_hashes, - payload, - now_as_millis(), - ); - - let mut finalization_candidate = next_finalization_block_number; - if block_number == 1 { - finalization_candidate = 1; - } else if block_number > finalization_candidate { - finalization_candidate += BLOCK_NUMBER_FINALIZATION_DIFF; - } - - let header = HeaderTypeOf::::new( - block_number, - parent_hash, - shard, - block_data.hash(), - finalization_candidate, - ); - - let block = SignedSidechainBlock::Block::new(header.clone(), block_data); - - debug!("Block header hash {}", header.hash()); - - let signed_block = block.sign_block(&self.signer); - - Ok(signed_block) - } -} diff --git a/bitacross-worker/sidechain/block-composer/src/error.rs b/bitacross-worker/sidechain/block-composer/src/error.rs deleted file mode 100644 index 6baba32eb7..0000000000 --- a/bitacross-worker/sidechain/block-composer/src/error.rs +++ /dev/null @@ -1,59 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use sgx_types::sgx_status_t; -use std::{boxed::Box, format}; - -pub type Result = core::result::Result; - -/// Block composer error -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("SGX error, status: {0}")] - Sgx(sgx_status_t), - #[error("STF execution error: {0}")] - StfExecution(#[from] itp_stf_executor::error::Error), - #[error("TOP pool RPC author error: {0}")] - TopPoolAuthor(#[from] itp_top_pool_author::error::Error), - #[error("Node Metadata error: {0:?}")] - NodeMetadata(itp_node_api::metadata::Error), - #[error("Node metadata provider error: {0:?}")] - NodeMetadataProvider(#[from] itp_node_api::metadata::provider::Error), - #[error(transparent)] - Other(#[from] Box), -} - -impl From for Error { - fn from(sgx_status: sgx_status_t) -> Self { - Self::Sgx(sgx_status) - } -} - -impl From for Error { - fn from(e: codec::Error) -> Self { - Self::Other(format!("{:?}", e).into()) - } -} - -impl From for Error { - fn from(e: itp_node_api::metadata::Error) -> Self { - Self::NodeMetadata(e) - } -} diff --git a/bitacross-worker/sidechain/block-composer/src/lib.rs b/bitacross-worker/sidechain/block-composer/src/lib.rs deleted file mode 100644 index 038f348c1d..0000000000 --- a/bitacross-worker/sidechain/block-composer/src/lib.rs +++ /dev/null @@ -1,36 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -//! Sidechain block composing logic. -#![feature(trait_alias)] -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -extern crate sgx_tstd as std; - -// re-export module to properly feature gate sgx and regular std environment -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub mod sgx_reexport_prelude { - pub use thiserror_sgx as thiserror; -} - -pub mod block_composer; -pub mod error; - -pub use block_composer::*; diff --git a/bitacross-worker/sidechain/block-verification/Cargo.toml b/bitacross-worker/sidechain/block-verification/Cargo.toml deleted file mode 100644 index 9265b86517..0000000000 --- a/bitacross-worker/sidechain/block-verification/Cargo.toml +++ /dev/null @@ -1,52 +0,0 @@ -[package] -name = "its-block-verification" -description = "Verification logic for sidechain blocks" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -homepage = "https://litentry.com/" -repository = "https://github.com/litentry/litentry-parachain" -license = "Apache-2.0" -edition = "2021" - -[dependencies] -log = { version = "0.4.17", default-features = false } -thiserror = { version = "1.0.26", optional = true } - -# local deps -itp-types = { default-features = false, path = "../../core-primitives/types" } -itp-utils = { default-features = false, path = "../../core-primitives/utils" } -its-primitives = { default-features = false, path = "../primitives" } - -# substrate deps -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-consensus-slots = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# sgx deps -sgx_tstd = { branch = "master", features = ["untrusted_fs", "net", "backtrace"], git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } -thiserror-sgx = { package = "thiserror", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } - -[features] -default = ["std"] -std = [ - "log/std", - "thiserror", - # local - "itp-types/std", - "its-primitives/std", - # substrate - "frame-support/std", - "sp-consensus-slots/std", - "sp-core/std", - "sp-runtime/std", -] -sgx = [ - "sgx_tstd", - "thiserror-sgx", -] - -[dev-dependencies] -itc-parentchain-test = { path = "../../core/parentchain/test" } -its-test = { path = "../../sidechain/test" } -sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } diff --git a/bitacross-worker/sidechain/block-verification/src/error.rs b/bitacross-worker/sidechain/block-verification/src/error.rs deleted file mode 100644 index bac9b8d60b..0000000000 --- a/bitacross-worker/sidechain/block-verification/src/error.rs +++ /dev/null @@ -1,46 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Error types in sidechain consensus - -use itp_types::BlockHash as ParentchainBlockHash; -use its_primitives::types::{block::BlockHash as SidechainBlockHash, BlockNumber}; -use std::string::String; - -pub type Result = std::result::Result; - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub use thiserror_sgx as thiserror; - -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum Error { - #[error("Message sender {0} is not a valid authority")] - InvalidAuthority(String), - #[error("Could not get authorities: {0:?}.")] - CouldNotGetAuthorities(String), - #[error("Bad parentchain block (Hash={0}). Reason: {1}")] - BadParentchainBlock(ParentchainBlockHash, String), - #[error("Bad sidechain block (Hash={0}). Reason: {1}")] - BadSidechainBlock(SidechainBlockHash, String), - #[error("Could not import new block due to {2}. (Last imported by number: {0:?})")] - BlockAncestryMismatch(BlockNumber, SidechainBlockHash, String), - #[error("Could not import new block. Expected first block, but found {0}. {1:?}")] - InvalidFirstBlock(BlockNumber, String), - #[error("Could not import block (number: {0}). A block with this number is already imported (current state block number: {1})")] - BlockAlreadyImported(BlockNumber, BlockNumber), -} diff --git a/bitacross-worker/sidechain/block-verification/src/lib.rs b/bitacross-worker/sidechain/block-verification/src/lib.rs deleted file mode 100644 index b496bcc0be..0000000000 --- a/bitacross-worker/sidechain/block-verification/src/lib.rs +++ /dev/null @@ -1,492 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -#![feature(assert_matches)] -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), not(feature = "sgx")))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be disabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -use crate::slot::{slot_author, slot_from_timestamp_and_duration}; -use error::Error as ConsensusError; -use frame_support::ensure; -use itp_utils::stringify::public_to_string; -use its_primitives::{ - traits::{ - Block as SidechainBlockTrait, BlockData, Header as HeaderTrait, - SignedBlock as SignedSidechainBlockTrait, SignedBlock, - }, - types::block::BlockHash, -}; -use log::*; -pub use sp_consensus_slots::Slot; -use sp_core::ByteArray; -use sp_runtime::{ - app_crypto::Pair, - traits::{Block as ParentchainBlockTrait, Header as ParentchainHeaderTrait}, -}; -use std::{fmt::Debug, time::Duration}; - -pub mod error; -pub mod slot; - -type AuthorityId

=

::Public; - -pub fn verify_sidechain_block( - signed_block: SignedSidechainBlock, - slot_duration: Duration, - last_block: &Option<::Block>, - parentchain_header: &ParentchainBlock::Header, - authorities: &[AuthorityId], -) -> Result -where - AuthorityPair: Pair, - AuthorityPair::Public: Debug, - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: 'static + SignedSidechainBlockTrait, - SignedSidechainBlock::Block: SidechainBlockTrait, -{ - ensure!( - signed_block.verify_signature(), - ConsensusError::BadSidechainBlock(signed_block.block().hash(), "bad signature".into()) - ); - - let slot = slot_from_timestamp_and_duration( - Duration::from_millis(signed_block.block().block_data().timestamp()), - slot_duration, - ); - - // We need to check the ancestry first to ensure that an already imported block does not result - // in an author verification error, but rather a `BlockAlreadyImported` error. - match last_block { - Some(last_block) => - verify_block_ancestry::(signed_block.block(), last_block)?, - None => ensure_first_block(signed_block.block())?, - } - - if let Err(e) = verify_author::( - &slot, - signed_block.block(), - parentchain_header, - authorities, - ) { - error!( - "Author verification for block (number: {}) failed, block will be discarded", - signed_block.block().header().block_number() - ); - return Err(e) - } - - Ok(signed_block) -} - -/// Verify that the `blocks` author is the expected author when comparing with onchain data. -fn verify_author( - slot: &Slot, - block: &SignedSidechainBlock::Block, - parentchain_head: &ParentchainHeader, - authorities: &[AuthorityId], -) -> Result<(), ConsensusError> -where - AuthorityPair: Pair, - AuthorityPair::Public: Debug, - SignedSidechainBlock: SignedSidechainBlockTrait + 'static, - ParentchainHeader: ParentchainHeaderTrait, -{ - ensure!( - parentchain_head.hash() == block.block_data().layer_one_head(), - ConsensusError::BadParentchainBlock( - parentchain_head.hash(), - "Invalid parentchain head".into(), - ) - ); - - let expected_author = slot_author::(*slot, authorities) - .ok_or_else(|| ConsensusError::CouldNotGetAuthorities("No authorities found".into()))?; - - ensure!( - expected_author == block.block_data().block_author(), - ConsensusError::InvalidAuthority(format!( - "Expected author: {}, author found in block: {}", - public_to_string(&expected_author.to_raw_vec()), - public_to_string(&block.block_data().block_author().to_raw_vec()) - )) - ); - - Ok(()) -} - -fn verify_block_ancestry( - block: &SidechainBlock, - last_block: &SidechainBlock, -) -> Result<(), ConsensusError> { - // These next two checks might seem redundant at first glance. However, they are distinct (see comments). - - // We have already imported this block. - ensure!( - block.header().block_number() > last_block.header().block_number(), - ConsensusError::BlockAlreadyImported( - block.header().block_number(), - last_block.header().block_number() - ) - ); - - // We are missing some blocks between our last known block and the one we're trying to import. - ensure!( - last_block.header().block_number() + 1 == block.header().block_number(), - ConsensusError::BlockAncestryMismatch( - last_block.header().block_number(), - last_block.hash(), - format!( - "Invalid block number, {} does not succeed {}", - block.header().block_number(), - last_block.header().block_number() - ) - ) - ); - - ensure!( - last_block.hash() == block.header().parent_hash(), - ConsensusError::BlockAncestryMismatch( - last_block.header().block_number(), - last_block.hash(), - "Parent hash does not match".into(), - ) - ); - - Ok(()) -} - -fn ensure_first_block( - block: &SidechainBlock, -) -> Result<(), ConsensusError> { - ensure!( - block.header().block_number() == 1, - ConsensusError::InvalidFirstBlock( - block.header().block_number(), - "No last block found, expecting first block. But block to import has number != 1" - .into() - ) - ); - ensure!( - block.header().parent_hash() == Default::default(), - ConsensusError::InvalidFirstBlock( - block.header().block_number(), - "No last block found, excepting first block. But block to import has parent_hash != 0" - .into() - ) - ); - - Ok(()) -} - -#[cfg(test)] -mod tests { - use super::*; - use core::assert_matches::assert_matches; - use frame_support::assert_ok; - use itc_parentchain_test::ParentchainHeaderBuilder; - use itp_types::{AccountId, Block as ParentchainBlock}; - use its_primitives::types::{block::SignedBlock, header::SidechainHeader as Header}; - use its_test::{ - sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}, - sidechain_block_data_builder::SidechainBlockDataBuilder, - sidechain_header_builder::SidechainHeaderBuilder, - }; - use sp_core::{ed25519::Pair, ByteArray, H256}; - use sp_keyring::ed25519::Keyring; - - pub const SLOT_DURATION: Duration = Duration::from_millis(300); - - fn assert_ancestry_mismatch_err(result: Result) { - assert_matches!(result, Err(ConsensusError::BlockAncestryMismatch(_, _, _,))) - } - - fn block(signer: Keyring, header: Header) -> SignedBlock { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let block_data = SidechainBlockDataBuilder::default() - .with_signer(signer.pair()) - .with_timestamp(0) - .with_layer_one_head(parentchain_header.hash()) - .build(); - - SidechainBlockBuilder::default() - .with_header(header) - .with_block_data(block_data) - .with_signer(signer.pair()) - .build_signed() - } - - fn block1(signer: Keyring) -> SignedBlock { - let header = SidechainHeaderBuilder::default().with_block_number(1).build(); - - block(signer, header) - } - - fn block2(signer: Keyring, parent_hash: H256) -> SignedBlock { - let header = SidechainHeaderBuilder::default() - .with_parent_hash(parent_hash) - .with_block_number(2) - .build(); - - block(signer, header) - } - - fn block3(signer: Keyring, parent_hash: H256, block_number: u64) -> SignedBlock { - let header = SidechainHeaderBuilder::default() - .with_parent_hash(parent_hash) - .with_block_number(block_number) - .build(); - - block(signer, header) - } - - #[test] - fn ensure_first_block_works() { - let block = SidechainBlockBuilder::default().build(); - assert_ok!(ensure_first_block(&block)); - } - - #[test] - fn ensure_first_block_errs_with_invalid_block_number() { - let header = SidechainHeaderBuilder::default().with_block_number(2).build(); - let block = SidechainBlockBuilder::default().with_header(header).build(); - assert_matches!(ensure_first_block(&block), Err(ConsensusError::InvalidFirstBlock(2, _))) - } - - #[test] - fn ensure_first_block_errs_with_invalid_parent_hash() { - let parent = H256::random(); - let header = SidechainHeaderBuilder::default().with_parent_hash(parent).build(); - let block = SidechainBlockBuilder::default().with_header(header).build(); - - assert_matches!(ensure_first_block(&block), Err(ConsensusError::InvalidFirstBlock(_, _))); - } - - #[test] - fn verify_block_ancestry_works() { - let last_block = SidechainBlockBuilder::default().build(); - let header = SidechainHeaderBuilder::default() - .with_parent_hash(last_block.hash()) - .with_block_number(2) - .build(); - let curr_block = SidechainBlockBuilder::default().with_header(header).build(); - - assert_ok!(verify_block_ancestry(&curr_block, &last_block)); - } - - #[test] - fn verify_block_ancestry_errs_with_invalid_parent_block_number() { - let last_block = SidechainBlockBuilder::default().build(); - let header = SidechainHeaderBuilder::default() - .with_parent_hash(last_block.hash()) - .with_block_number(5) - .build(); - let curr_block = SidechainBlockBuilder::default().with_header(header).build(); - - assert_ancestry_mismatch_err(verify_block_ancestry(&curr_block, &last_block)); - } - - #[test] - fn verify_block_ancestry_errs_with_invalid_parent_hash() { - let last_block = SidechainBlockBuilder::default().build(); - let header = SidechainHeaderBuilder::default().with_block_number(2).build(); - let curr_block = SidechainBlockBuilder::default().with_header(header).build(); - - assert_ancestry_mismatch_err(verify_block_ancestry(&curr_block, &last_block)); - } - - #[test] - fn verify_works() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let last_block = SidechainBlockBuilder::default().build(); - let curr_block = block2(signer, last_block.hash()); - - assert_ok!(verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &Some(last_block), - &parentchain_header, - &authorities, - )); - } - - #[test] - fn verify_works_for_first_block() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let curr_block = block1(signer); - - assert_ok!(verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &None, - &parentchain_header, - &authorities, - )); - } - - #[test] - fn verify_errs_on_wrong_authority() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let bob_account: AccountId = Keyring::Bob.public().into(); - let authorities = [ - AuthorityId::::from_slice(bob_account.as_ref()).unwrap(), - AuthorityId::::from_slice(signer_account.as_ref()).unwrap(), - ]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let last_block = SidechainBlockBuilder::default().build(); - let curr_block = block2(signer, last_block.hash()); - - assert_matches!( - verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &Some(last_block), - &parentchain_header, - &authorities, - ) - .unwrap_err(), - ConsensusError::InvalidAuthority(_) - ); - } - - #[test] - fn verify_errs_on_invalid_ancestry() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let last_block = SidechainBlockBuilder::default().build(); - let curr_block = block2(signer, Default::default()); - - assert_ancestry_mismatch_err(verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &Some(last_block), - &parentchain_header, - &authorities, - )); - } - - #[test] - fn verify_errs_on_wrong_first_block() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let curr_block = block2(signer, Default::default()); - - assert_matches!( - verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &None, - &parentchain_header, - &authorities, - ) - .unwrap_err(), - ConsensusError::InvalidFirstBlock(2, _) - ); - } - - #[test] - fn verify_errs_on_already_imported_block() { - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let last_block = SidechainBlockBuilder::default().build(); - // Current block has also number 1, same as last. So import should return an error - // that a block with this number is already imported. - let curr_block = block3(signer, last_block.hash(), 1); - - assert_matches!( - verify_sidechain_block::( - curr_block, - SLOT_DURATION, - &Some(last_block), - &parentchain_header, - &authorities, - ) - .unwrap_err(), - ConsensusError::BlockAlreadyImported(1, 1) - ); - } - - #[test] - fn verify_block_already_imported_error_even_if_parentchain_block_mismatches() { - // This test is to ensure that we get a 'AlreadyImported' error, when the sidechain block - // is already imported, and the parentchain block that is passed into the verifier is newer. - // Important because client of the verifier acts differently for an 'AlreadyImported' error than an 'AncestryErrorMismatch'. - - let signer = Keyring::Alice; - let signer_account: AccountId = signer.public().into(); - let authorities = [AuthorityId::::from_slice(signer_account.as_ref()).unwrap()]; - - let parentchain_header_1 = ParentchainHeaderBuilder::default().with_number(1).build(); - let parentchain_header_2 = ParentchainHeaderBuilder::default().with_number(2).build(); - - let block_data = SidechainBlockDataBuilder::default() - .with_layer_one_head(parentchain_header_1.hash()) - .with_signer(signer.pair()) - .build(); - let last_block = SidechainBlockBuilder::default() - .with_block_data(block_data) - .with_signer(signer.pair()) - .build(); - - let block_data_for_signed_block = SidechainBlockDataBuilder::default() - .with_layer_one_head(parentchain_header_1.hash()) - .with_signer(signer.pair()) - .build(); - let signed_block_to_verify = SidechainBlockBuilder::default() - .with_block_data(block_data_for_signed_block) - .with_signer(signer.pair()) - .build_signed(); - - assert_matches!( - verify_sidechain_block::( - signed_block_to_verify, - SLOT_DURATION, - &Some(last_block), - &parentchain_header_2, - &authorities, - ) - .unwrap_err(), - ConsensusError::BlockAlreadyImported(1, 1) - ); - } -} diff --git a/bitacross-worker/sidechain/block-verification/src/slot.rs b/bitacross-worker/sidechain/block-verification/src/slot.rs deleted file mode 100644 index 5eb2ede417..0000000000 --- a/bitacross-worker/sidechain/block-verification/src/slot.rs +++ /dev/null @@ -1,45 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::AuthorityId; -pub use sp_consensus_slots::Slot; -use sp_runtime::app_crypto::Pair; -use std::time::Duration; - -/// Get slot author for given block along with authorities. -pub fn slot_author(slot: Slot, authorities: &[AuthorityId

]) -> Option<&AuthorityId

> { - if authorities.is_empty() { - log::warn!("Authorities list is empty, cannot determine slot author"); - return None - } - - let idx = *slot % (authorities.len() as u64); - assert!( - idx <= usize::MAX as u64, - "It is impossible to have a vector with length beyond the address space; qed", - ); - - let current_author = authorities.get(idx as usize).expect( - "authorities not empty; index constrained to list length;this is a valid index; qed", - ); - - Some(current_author) -} - -pub fn slot_from_timestamp_and_duration(timestamp: Duration, duration: Duration) -> Slot { - ((timestamp.as_millis() / duration.as_millis()) as u64).into() -} diff --git a/bitacross-worker/sidechain/consensus/aura/Cargo.toml b/bitacross-worker/sidechain/consensus/aura/Cargo.toml deleted file mode 100644 index edb3ed9916..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/Cargo.toml +++ /dev/null @@ -1,106 +0,0 @@ -[package] -name = "its-consensus-aura" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "chain-error"] } -finality-grandpa = { version = "0.16.0", default-features = false, features = ["derive-codec"] } -log = { version = "0.4", default-features = false } - -# sgx deps -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } - -# substrate deps -sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# local deps -ita-stf = { path = "../../../app-libs/stf", default-features = false } -itc-parentchain-block-import-dispatcher = { path = "../../../core/parentchain/block-import-dispatcher", default-features = false } -itc-peer-top-broadcaster = { path = "../../../core/peer-top-broadcaster", default-features = false } -itp-enclave-metrics = { path = "../../../core-primitives/enclave-metrics", default-features = false } -itp-ocall-api = { path = "../../../core-primitives/ocall-api", default-features = false } -itp-settings = { path = "../../../core-primitives/settings" } -itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } -itp-sgx-externalities = { path = "../../../core-primitives/substrate-sgx/externalities", default-features = false } -itp-stf-executor = { path = "../../../core-primitives/stf-executor", default-features = false } -itp-stf-primitives = { path = "../../../core-primitives/stf-primitives", default-features = false } -itp-stf-state-handler = { path = "../../../core-primitives/stf-state-handler", default-features = false } -itp-time-utils = { path = "../../../core-primitives/time-utils", default-features = false } -itp-top-pool-author = { path = "../../../core-primitives/top-pool-author", default-features = false } -itp-types = { path = "../../../core-primitives/types", default-features = false } -its-block-composer = { path = "../../block-composer", default-features = false } -its-block-verification = { path = "../../block-verification", optional = true, default-features = false } -its-consensus-common = { path = "../common", default-features = false } -its-consensus-slots = { path = "../slots", default-features = false } -its-primitives = { path = "../../primitives", default-features = false } -its-state = { path = "../../state", default-features = false } -its-validateer-fetch = { path = "../../validateer-fetch", default-features = false } - -# litentry -itp-utils = { path = "../../../core-primitives/utils", default-features = false } -lc-scheduled-enclave = { path = "../../../litentry/core/scheduled-enclave", default-features = false } -litentry-hex-utils = { path = "../../../../primitives/hex", default-features = false } - -[dev-dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } -env_logger = "0.9.0" -itc-parentchain-block-import-dispatcher = { path = "../../../core/parentchain/block-import-dispatcher", features = ["mocks"] } -itc-parentchain-test = { path = "../../../core/parentchain/test" } -itp-storage = { path = "../../../core-primitives/storage" } -itp-test = { path = "../../../core-primitives/test" } -its-test = { path = "../../../sidechain/test" } -sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[features] -default = ["std"] -std = [ - #crates.io - "codec/std", - "finality-grandpa/std", - "log/std", - #substrate - "sp-core/std", - "sp-runtime/std", - #local - "ita-stf/std", - "itc-parentchain-block-import-dispatcher/std", - "itc-peer-top-broadcaster/std", - "itp-enclave-metrics/std", - "itp-ocall-api/std", - "itp-sgx-crypto/std", - "itp-sgx-externalities/std", - "itp-stf-executor/std", - "itp-stf-primitives/std", - "itp-stf-state-handler/std", - "itp-time-utils/std", - "itp-types/std", - "its-block-composer/std", - "its-block-verification/std", - "its-consensus-common/std", - "its-consensus-slots/std", - "its-state/std", - "its-validateer-fetch/std", - "its-primitives/std", - "lc-scheduled-enclave/std", -] -sgx = [ - "sgx_tstd", - "ita-stf/sgx", - "itc-parentchain-block-import-dispatcher/sgx", - "itc-peer-top-broadcaster/sgx", - "itp-enclave-metrics/sgx", - "itp-sgx-crypto/sgx", - "itp-sgx-externalities/sgx", - "itp-stf-executor/sgx", - "itp-stf-state-handler/sgx", - "itp-time-utils/sgx", - "its-block-composer/sgx", - "its-consensus-common/sgx", - "its-consensus-slots/sgx", - "its-state/sgx", - "its-block-verification/sgx", - "lc-scheduled-enclave/sgx", -] diff --git a/bitacross-worker/sidechain/consensus/aura/src/block_importer.rs b/bitacross-worker/sidechain/consensus/aura/src/block_importer.rs deleted file mode 100644 index fb6f4e246a..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/block_importer.rs +++ /dev/null @@ -1,367 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -//! Implementation of the sidechain block importer struct. -//! Imports sidechain blocks and applies the accompanying state diff to its state. - -use codec::{Decode, Encode}; -use core::fmt::Debug; -// Reexport BlockImport trait which implements fn block_import() -use crate::{AuraVerifier, EnclaveOnChainOCallApi, SidechainBlockTrait}; -use itc_parentchain_block_import_dispatcher::triggered_dispatcher::TriggerParentchainBlockImport; -use itc_peer_top_broadcaster::PeerUpdater; -use itp_enclave_metrics::EnclaveMetric; -use itp_ocall_api::{EnclaveMetricsOCallApi, EnclaveSidechainOCallApi}; -use itp_settings::sidechain::SLOT_DURATION; -use itp_sgx_crypto::{key_repository::AccessKey, StateCrypto}; -use itp_sgx_externalities::SgxExternalities; -use itp_stf_primitives::{traits::TrustedCallVerification, types::TrustedOperationOrHash}; -use itp_stf_state_handler::handle_state::HandleState; -use itp_top_pool_author::traits::{AuthorApi, OnBlockImported}; -use itp_types::H256; -pub use its_consensus_common::BlockImport; -use its_consensus_common::Error as ConsensusError; -use its_primitives::traits::{ - BlockData, Header as HeaderTrait, ShardIdentifierFor, SignedBlock as SignedBlockTrait, -}; -use its_validateer_fetch::ValidateerFetch; -use log::*; -use sp_core::{crypto::UncheckedFrom, Pair}; -use sp_runtime::{ - generic::SignedBlock as SignedParentchainBlock, - traits::{Block as ParentchainBlockTrait, Header}, -}; -use std::{marker::PhantomData, sync::Arc}; - -/// Implements `BlockImport`. -#[derive(Clone)] -pub struct BlockImporter< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - StateHandler, - StateKeyRepository, - TopPoolAuthor, - ParentchainBlockImporter, - PeersUpdater, - TCS, - G, -> { - state_handler: Arc, - state_key_repository: Arc, - top_pool_author: Arc, - parentchain_block_importer: Arc, - ocall_api: Arc, - peer_updater: Arc, - _phantom: PhantomData<(Authority, ParentchainBlock, SignedSidechainBlock, TCS, G)>, -} - -impl< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - StateHandler, - StateKeyRepository, - TopPoolAuthor, - ParentchainBlockImporter, - PeersUpdater, - TCS, - G, - > - BlockImporter< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - StateHandler, - StateKeyRepository, - TopPoolAuthor, - ParentchainBlockImporter, - PeersUpdater, - TCS, - G, - > where - Authority: Pair, - Authority::Public: std::fmt::Debug + UncheckedFrom<[u8; 32]>, - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedBlockTrait + 'static, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - OCallApi: EnclaveSidechainOCallApi - + ValidateerFetch - + EnclaveOnChainOCallApi - + EnclaveMetricsOCallApi - + Send - + Sync, - StateHandler: HandleState, - StateKeyRepository: AccessKey, - ::KeyType: StateCrypto, - TopPoolAuthor: AuthorApi + OnBlockImported, - ParentchainBlockImporter: TriggerParentchainBlockImport> - + Send - + Sync, - PeersUpdater: PeerUpdater, - TCS: PartialEq + Encode + Decode + Debug + Clone + Send + Sync + TrustedCallVerification, - G: PartialEq + Encode + Decode + Debug + Clone + Send + Sync, -{ - pub fn new( - state_handler: Arc, - state_key_repository: Arc, - top_pool_author: Arc, - parentchain_block_importer: Arc, - ocall_api: Arc, - peer_updater: Arc, - ) -> Self { - Self { - state_handler, - state_key_repository, - top_pool_author, - parentchain_block_importer, - ocall_api, - peer_updater, - _phantom: Default::default(), - } - } - - fn update_top_pool(&self, sidechain_block: &SignedSidechainBlock::Block) { - // Notify pool about imported block for status updates of the calls. - self.top_pool_author.on_block_imported( - sidechain_block.block_data().signed_top_hashes(), - sidechain_block.hash(), - ); - - // Remove calls from pool. - let executed_operations = sidechain_block - .block_data() - .signed_top_hashes() - .iter() - .map(|hash| (TrustedOperationOrHash::Hash(*hash), true)) - .collect(); - - let _calls_failed_to_remove = self - .top_pool_author - .remove_calls_from_pool(sidechain_block.header().shard_id(), executed_operations); - - // In case the executed call did not originate in our own TOP pool, we will not be able to remove it from our TOP pool. - // So this error will occur frequently, without it meaning that something really went wrong. - // TODO: Once the TOP pools are synchronized, we will want this check again! - // for call_failed_to_remove in _calls_failed_to_remove { - // error!("Could not remove call {:?} from top pool", call_failed_to_remove); - // } - } -} - -impl< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - StateHandler, - StateKeyRepository, - TopPoolAuthor, - ParentchainBlockImporter, - PeersUpdater, - TCS, - G, - > BlockImport - for BlockImporter< - Authority, - ParentchainBlock, - SignedSidechainBlock, - OCallApi, - StateHandler, - StateKeyRepository, - TopPoolAuthor, - ParentchainBlockImporter, - PeersUpdater, - TCS, - G, - > where - Authority: Pair, - Authority::Public: std::fmt::Debug + UncheckedFrom<[u8; 32]>, - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedBlockTrait + 'static, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - OCallApi: EnclaveSidechainOCallApi - + ValidateerFetch - + EnclaveOnChainOCallApi - + EnclaveMetricsOCallApi - + Send - + Sync, - StateHandler: HandleState, - StateKeyRepository: AccessKey, - ::KeyType: StateCrypto, - TopPoolAuthor: AuthorApi + OnBlockImported, - ParentchainBlockImporter: TriggerParentchainBlockImport> - + Send - + Sync, - PeersUpdater: PeerUpdater, - TCS: PartialEq + Encode + Decode + Debug + Clone + Send + Sync + TrustedCallVerification, - G: PartialEq + Encode + Decode + Debug + Clone + Send + Sync, -{ - type Verifier = AuraVerifier; - type SidechainState = SgxExternalities; - type StateCrypto = ::KeyType; - type Context = OCallApi; - - fn verifier( - &self, - maybe_last_sidechain_block: Option, - ) -> Self::Verifier { - AuraVerifier::::new( - SLOT_DURATION, - maybe_last_sidechain_block, - ) - } - - fn apply_state_update( - &self, - shard: &ShardIdentifierFor, - mutating_function: F, - ) -> Result<(), ConsensusError> - where - F: FnOnce(Self::SidechainState) -> Result, - { - let (write_lock, state) = self - .state_handler - .load_for_mutation(shard) - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - // We load a copy of the state and apply the update. In case the update fails, we don't write - // the state back to the state handler, and thus guaranteeing state integrity. - let updated_state = mutating_function(state)?; - - self.state_handler - .write_after_mutation(updated_state, write_lock, shard) - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - Ok(()) - } - - fn verify_import( - &self, - shard: &ShardIdentifierFor, - verifying_function: F, - ) -> Result - where - F: FnOnce(&Self::SidechainState) -> Result, - { - self.state_handler - .execute_on_current(shard, |state, _| verifying_function(state)) - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))? - } - - fn state_key(&self) -> Result { - self.state_key_repository - .retrieve_key() - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into())) - } - - fn get_context(&self) -> &Self::Context { - &self.ocall_api - } - - fn import_parentchain_block( - &self, - sidechain_block: &SignedSidechainBlock::Block, - last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> Result { - // get new peer list on each parentchain block import - if let Ok(peers) = self.ocall_api.get_trusted_peers_urls() { - self.peer_updater.update(peers); - } - - // We trigger the import of parentchain blocks up until the last one we've seen in the - // sidechain block that we're importing. This is done to prevent forks in the sidechain (#423) - let maybe_latest_imported_block = self - .parentchain_block_importer - .import_until(|signed_parentchain_block| { - signed_parentchain_block.block.hash() - == sidechain_block.block_data().layer_one_head() - }) - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - Ok(maybe_latest_imported_block - .map(|b| b.block.header().clone()) - .unwrap_or_else(|| last_imported_parentchain_header.clone())) - } - - fn peek_parentchain_header( - &self, - sidechain_block: &SignedSidechainBlock::Block, - last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> Result { - let last = last_imported_parentchain_header; - debug!("Peeking parentchain header"); - debug!( - "sidechain block parentchain head: {}", - sidechain_block.block_data().layer_one_head() - ); - debug!( - "last imported head: {}, number: {:?}, parenthash: {}", - last.hash(), - last.number(), - last.parent_hash() - ); - - let parentchain_header_hash_to_peek = sidechain_block.block_data().layer_one_head(); - if parentchain_header_hash_to_peek == last_imported_parentchain_header.hash() { - debug!("No queue peek necessary, sidechain block references latest imported parentchain block"); - return Ok(last_imported_parentchain_header.clone()) - } - - let maybe_signed_parentchain_block = self - .parentchain_block_importer - .peek(|parentchain_block| { - parentchain_block.block.header().hash() == parentchain_header_hash_to_peek - }) - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - maybe_signed_parentchain_block - .map(|signed_block| signed_block.block.header().clone()) - .ok_or_else(|| { - ConsensusError::Other( - format!( - "Failed to find parentchain header in import queue (hash: {}) that is \ - associated with the current sidechain block that is to be imported (number: {}, hash: {})", - parentchain_header_hash_to_peek, - sidechain_block.header().block_number(), - sidechain_block.hash() - ) - .into(), - ) - }) - } - - fn cleanup(&self, signed_sidechain_block: &SignedSidechainBlock) -> Result<(), ConsensusError> { - let sidechain_block = signed_sidechain_block.block(); - - // Remove all successfully applied trusted calls from the top pool. - self.update_top_pool(sidechain_block); - - // Send metric about sidechain block height (i.e. block number) - let block_height_metric = - EnclaveMetric::SetSidechainBlockHeight(sidechain_block.header().block_number()); - if let Err(e) = self.ocall_api.update_metric(block_height_metric) { - warn!("Failed to update sidechain block height metric: {:?}", e); - } - - Ok(()) - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/lib.rs b/bitacross-worker/sidechain/consensus/aura/src/lib.rs deleted file mode 100644 index 4114852c73..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/lib.rs +++ /dev/null @@ -1,776 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Aura worker for the sidechain. -//! -//! It is inspired by parity's implementation but has been greatly amended. - -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(test, feature(assert_matches))] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -use codec::Encode; -use core::marker::PhantomData; -use itc_parentchain_block_import_dispatcher::triggered_dispatcher::TriggerParentchainBlockImport; -use itp_ocall_api::EnclaveOnChainOCallApi; -use itp_sgx_externalities::SgxExternalities; -use itp_stf_state_handler::handle_state::HandleState; -use itp_time_utils::duration_now; - -use its_block_verification::slot::slot_author; -use its_consensus_common::{Environment, Error as ConsensusError, Proposer}; -use its_consensus_slots::{SimpleSlotWorker, Slot, SlotInfo}; -use its_primitives::{ - traits::{Block as SidechainBlockTrait, Header as HeaderTrait, SignedBlock}, - types::block::BlockHash, -}; -use its_validateer_fetch::ValidateerFetch; -use lc_scheduled_enclave::ScheduledEnclaveUpdater; -use litentry_hex_utils::hex_encode; -use sp_core::crypto::UncheckedFrom; -use sp_runtime::{ - app_crypto::{sp_core::H256, Pair}, - generic::SignedBlock as SignedParentchainBlock, - traits::{Block as ParentchainBlockTrait, Header as ParentchainHeaderTrait}, -}; -use std::{string::ToString, sync::Arc, time::Duration, vec::Vec}; - -pub mod block_importer; -pub mod proposer_factory; -pub mod slot_proposer; -mod verifier; - -pub use verifier::*; - -#[cfg(test)] -mod test; - -/// Aura consensus struct. -pub struct Aura< - AuthorityPair, - ParentchainBlock, - SidechainBlock, - Environment, - OcallApi, - IntegriteeImportTrigger, - TargetAImportTrigger, - TargetBImportTrigger, - ScheduledEnclave, - StateHandler, -> { - authority_pair: AuthorityPair, - ocall_api: OcallApi, - parentchain_integritee_import_trigger: Arc, - maybe_parentchain_target_a_import_trigger: Option>, - maybe_parentchain_target_b_import_trigger: Option>, - environment: Environment, - claim_strategy: SlotClaimStrategy, - scheduled_enclave: Arc, - state_handler: Arc, - _phantom: PhantomData<(AuthorityPair, ParentchainBlock, SidechainBlock)>, -} - -impl< - AuthorityPair, - ParentchainBlock, - SidechainBlock, - Environment, - OcallApi, - IntegriteeImportTrigger, - TargetAImportTrigger, - TargetBImportTrigger, - ScheduledEnclave, - StateHandler, - > - Aura< - AuthorityPair, - ParentchainBlock, - SidechainBlock, - Environment, - OcallApi, - IntegriteeImportTrigger, - TargetAImportTrigger, - TargetBImportTrigger, - ScheduledEnclave, - StateHandler, - > -{ - #[allow(clippy::too_many_arguments)] - pub fn new( - authority_pair: AuthorityPair, - ocall_api: OcallApi, - parentchain_integritee_import_trigger: Arc, - maybe_parentchain_target_a_import_trigger: Option>, - maybe_parentchain_target_b_import_trigger: Option>, - environment: Environment, - scheduled_enclave: Arc, - state_handler: Arc, - ) -> Self { - Self { - authority_pair, - ocall_api, - parentchain_integritee_import_trigger, - maybe_parentchain_target_a_import_trigger, - maybe_parentchain_target_b_import_trigger, - environment, - claim_strategy: SlotClaimStrategy::RoundRobin, - scheduled_enclave, - state_handler, - _phantom: Default::default(), - } - } - - pub fn with_claim_strategy(mut self, claim_strategy: SlotClaimStrategy) -> Self { - self.claim_strategy = claim_strategy; - - self - } -} - -/// The fraction of total block time we are allowed to be producing the block. So that we have -/// enough time send create and send the block to fellow validateers. -pub const BLOCK_PROPOSAL_SLOT_PORTION: f32 = 0.7; - -#[derive(PartialEq, Eq, Debug)] -pub enum SlotClaimStrategy { - /// try to produce a block always even if it's not the authors slot - /// Intended for first phase to see if aura production works - Always, - /// Proper Aura strategy: Only produce blocks, when it's the authors slot. - RoundRobin, -} - -type AuthorityId

=

::Public; -type ShardIdentifierFor = - <<::Block as SidechainBlockTrait>::HeaderType as HeaderTrait>::ShardIdentifier; - -impl< - AuthorityPair, - ParentchainBlock, - SignedSidechainBlock, - E, - OcallApi, - IntegriteeImportTrigger, - TargetAImportTrigger, - TargetBImportTrigger, - ScheduledEnclave, - StateHandler, - > SimpleSlotWorker - for Aura< - AuthorityPair, - ParentchainBlock, - SignedSidechainBlock, - E, - OcallApi, - IntegriteeImportTrigger, - TargetAImportTrigger, - TargetBImportTrigger, - ScheduledEnclave, - StateHandler, - > where - AuthorityPair: Pair, - AuthorityPair::Public: UncheckedFrom<[u8; 32]>, - // todo: Relax hash trait bound, but this needs a change to some other parts in the code. - ParentchainBlock: ParentchainBlockTrait, - E: Environment, - E::Proposer: Proposer, - SignedSidechainBlock: SignedBlock + Send + 'static, - OcallApi: ValidateerFetch + EnclaveOnChainOCallApi + Send + 'static, - IntegriteeImportTrigger: - TriggerParentchainBlockImport>, - TargetAImportTrigger: - TriggerParentchainBlockImport>, - TargetBImportTrigger: - TriggerParentchainBlockImport>, - ScheduledEnclave: ScheduledEnclaveUpdater, - StateHandler: HandleState, -{ - type Proposer = E::Proposer; - type Claim = AuthorityPair::Public; - type EpochData = Vec>; - type Output = SignedSidechainBlock; - type ScheduledEnclave = ScheduledEnclave; - type StateHandler = StateHandler; - - fn get_scheduled_enclave(&mut self) -> Arc { - self.scheduled_enclave.clone() - } - - fn get_state_handler(&mut self) -> Arc { - self.state_handler.clone() - } - - fn epoch_data( - &self, - header: &ParentchainBlock::Header, - _slot: Slot, - ) -> Result { - authorities::<_, AuthorityPair, ParentchainBlock::Header>(&self.ocall_api, header) - } - - fn authorities_len(&self, epoch_data: &Self::EpochData) -> Option { - Some(epoch_data.len()) - } - - // While the header is not used in aura, it is used in different consensus systems, so it should be left there. - fn claim_slot( - &self, - _header: &ParentchainBlock::Header, - slot: Slot, - epoch_data: &Self::EpochData, - ) -> Option { - let expected_author = slot_author::(slot, epoch_data)?; - - if expected_author == &self.authority_pair.public() { - log::info!("Claiming slot ({})", *slot); - return Some(self.authority_pair.public()) - } - - if self.claim_strategy == SlotClaimStrategy::Always { - log::debug!("Not our slot but we still claim it."); - return Some(self.authority_pair.public()) - } - - None - } - - fn proposer( - &mut self, - header: ParentchainBlock::Header, - shard: ShardIdentifierFor, - ) -> Result { - self.environment.init(header, shard) - } - - fn proposing_remaining_duration(&self, slot_info: &SlotInfo) -> Duration { - proposing_remaining_duration(slot_info, duration_now()) - } - - // Design remark: the following may seem too explicit and it certainly could be abstracted. - // however, as pretty soon we may not want to assume same Block types for all parentchains, - // it may make sense to abstract once we do that. - - fn import_integritee_parentchain_blocks_until( - &self, - parentchain_header_hash: &::Hash, - ) -> Result, ConsensusError> { - log::trace!( - "import Integritee blocks until {}", - hex_encode(parentchain_header_hash.encode().as_ref()) - ); - let maybe_parentchain_block = self - .parentchain_integritee_import_trigger - .import_until(|parentchain_block| { - parentchain_block.block.hash() == *parentchain_header_hash - }) - .map_err(|e| ConsensusError::Other(e.into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } - - fn import_target_a_parentchain_blocks_until( - &self, - parentchain_header_hash: &::Hash, - ) -> Result, ConsensusError> { - log::trace!( - "import TargetA blocks until {}", - hex_encode(parentchain_header_hash.encode().as_ref()) - ); - let maybe_parentchain_block = self - .maybe_parentchain_target_a_import_trigger - .clone() - .ok_or_else(|| ConsensusError::Other("no target_a assigned".into()))? - .import_until(|parentchain_block| { - parentchain_block.block.hash() == *parentchain_header_hash - }) - .map_err(|e| ConsensusError::Other(e.into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } - - fn import_target_b_parentchain_blocks_until( - &self, - parentchain_header_hash: &::Hash, - ) -> Result, ConsensusError> { - log::trace!( - "import TargetB blocks until {}", - hex_encode(parentchain_header_hash.encode().as_ref()) - ); - let maybe_parentchain_block = self - .maybe_parentchain_target_b_import_trigger - .clone() - .ok_or_else(|| ConsensusError::Other("no target_b assigned".into()))? - .import_until(|parentchain_block| { - parentchain_block.block.hash() == *parentchain_header_hash - }) - .map_err(|e| ConsensusError::Other(e.into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } - - fn peek_latest_integritee_parentchain_header( - &self, - ) -> Result, ConsensusError> { - let maybe_parentchain_block = self - .parentchain_integritee_import_trigger - .peek_latest() - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } - - fn peek_latest_target_a_parentchain_header( - &self, - ) -> Result, ConsensusError> { - let maybe_parentchain_block = self - .maybe_parentchain_target_a_import_trigger - .clone() - .ok_or_else(|| ConsensusError::Other("no target_a assigned".into()))? - .peek_latest() - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } - - fn peek_latest_target_b_parentchain_header( - &self, - ) -> Result, ConsensusError> { - let maybe_parentchain_block = self - .maybe_parentchain_target_b_import_trigger - .clone() - .ok_or_else(|| ConsensusError::Other("no target_b assigned".into()))? - .peek_latest() - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?; - - Ok(maybe_parentchain_block.map(|b| b.block.header().clone())) - } -} - -/// unit-testable remaining duration fn. -fn proposing_remaining_duration( - slot_info: &SlotInfo, - now: Duration, -) -> Duration { - // if a `now` before slot begin is passed such that `slot_remaining` would be bigger than `slot.slot_duration` - // we take the total `slot_duration` as reference value. - let proposing_duration = slot_info.duration.mul_f32(BLOCK_PROPOSAL_SLOT_PORTION); - - let slot_remaining = slot_info - .ends_at - .checked_sub(now) - .map(|remaining| remaining.mul_f32(BLOCK_PROPOSAL_SLOT_PORTION)) - .unwrap_or_default(); - - std::cmp::min(slot_remaining, proposing_duration) -} - -fn authorities( - ocall_api: &ValidateerFetcher, - header: &ParentchainHeader, -) -> Result>, ConsensusError> -where - ValidateerFetcher: ValidateerFetch + EnclaveOnChainOCallApi, - P: Pair, - P::Public: UncheckedFrom<[u8; 32]>, - ParentchainHeader: ParentchainHeaderTrait, -{ - Ok(ocall_api - .current_validateers::(header) - .map_err(|e| ConsensusError::CouldNotGetAuthorities(e.to_string()))? - .iter() - .map(|account| P::Public::unchecked_from(*account.as_ref())) - .collect()) -} - -pub enum AnyImportTrigger { - Integritee(Integritee), - TargetA(TargetA), - TargetB(TargetB), -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test::{ - fixtures::{types::TestAura, SLOT_DURATION}, - mocks::environment_mock::{EnvironmentMock, OutdatedBlockEnvironmentMock}, - }; - use itc_parentchain_block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; - use itc_parentchain_test::{ParentchainBlockBuilder, ParentchainHeaderBuilder}; - use itp_test::mock::{handle_state_mock::HandleStateMock, onchain_mock::OnchainMock}; - use itp_types::{ - AccountId, Block as ParentchainBlock, Header as ParentchainHeader, ShardIdentifier, - SignedBlock as SignedParentchainBlock, - }; - use its_consensus_slots::PerShardSlotWorkerScheduler; - use lc_scheduled_enclave::ScheduledEnclaveMock; - use sp_core::ed25519::Public; - use sp_keyring::ed25519::Keyring; - - fn get_aura( - onchain_mock: OnchainMock, - trigger_parentchain_import: Arc>, - ) -> TestAura { - Aura::new( - Keyring::Alice.pair(), - onchain_mock, - trigger_parentchain_import, - None, - None, - EnvironmentMock, - Arc::new(ScheduledEnclaveMock::default()), - Arc::new(HandleStateMock::from_shard(ShardIdentifier::default()).unwrap()), - ) - } - - fn get_aura_outdated( - onchain_mock: OnchainMock, - trigger_parentchain_import: Arc>, - ) -> TestAura { - Aura::new( - Keyring::Alice.pair(), - onchain_mock, - trigger_parentchain_import, - None, - None, - OutdatedBlockEnvironmentMock, - Arc::new(ScheduledEnclaveMock::default()), - Arc::new(HandleStateMock::from_shard(ShardIdentifier::default()).unwrap()), - ) - } - - fn get_default_aura() -> TestAura { - get_aura(Default::default(), Default::default()) - } - - fn now_slot(slot: Slot, header: &ParentchainHeader) -> SlotInfo { - let now = duration_now(); - SlotInfo { - slot, - timestamp: now, - duration: SLOT_DURATION, - ends_at: now + SLOT_DURATION, - last_imported_integritee_parentchain_head: header.clone(), - maybe_last_imported_target_a_parentchain_head: None, - maybe_last_imported_target_b_parentchain_head: None, - } - } - - fn now_slot_with_default_header(slot: Slot) -> SlotInfo { - now_slot(slot, &ParentchainHeaderBuilder::default().build()) - } - - fn default_authorities() -> Vec { - vec![Keyring::Alice.public(), Keyring::Bob.public(), Keyring::Charlie.public()] - } - - fn create_validateer_set_from_publics(authorities: Vec) -> Vec { - authorities.iter().map(|a| AccountId::from(a.clone())).collect() - } - - fn onchain_mock( - parentchain_header: &ParentchainHeader, - authorities: Vec, - ) -> OnchainMock { - let validateers = create_validateer_set_from_publics(authorities); - OnchainMock::default().add_validateer_set(parentchain_header, Some(validateers)) - } - - fn onchain_mock_with_default_authorities_and_header() -> OnchainMock { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - onchain_mock(&parentchain_header, default_authorities()) - } - - fn create_import_trigger_with_header( - header: ParentchainHeader, - ) -> Arc> { - let latest_parentchain_block = - ParentchainBlockBuilder::default().with_header(header).build_signed(); - Arc::new( - TriggerParentchainBlockImportMock::default() - .with_latest_imported(Some(latest_parentchain_block)), - ) - } - - #[test] - fn current_authority_should_claim_its_slot() { - let authorities = - vec![Keyring::Bob.public(), Keyring::Charlie.public(), Keyring::Alice.public()]; - let aura = get_default_aura(); - let header = ParentchainHeaderBuilder::default().build(); - - assert!(aura.claim_slot(&header, 0.into(), &authorities).is_none()); - assert!(aura.claim_slot(&header, 1.into(), &authorities).is_none()); - // this our authority - assert!(aura.claim_slot(&header, 2.into(), &authorities).is_some()); - - assert!(aura.claim_slot(&header, 3.into(), &authorities).is_none()); - assert!(aura.claim_slot(&header, 4.into(), &authorities).is_none()); - // this our authority - assert!(aura.claim_slot(&header, 5.into(), &authorities).is_some()); - } - - #[test] - fn current_authority_should_claim_all_slots() { - let header = ParentchainHeaderBuilder::default().build(); - let authorities = default_authorities(); - let aura = get_default_aura().with_claim_strategy(SlotClaimStrategy::Always); - - assert!(aura.claim_slot(&header, 0.into(), &authorities).is_some()); - assert!(aura.claim_slot(&header, 1.into(), &authorities).is_some()); - // this our authority - assert!(aura.claim_slot(&header, 2.into(), &authorities).is_some()); - assert!(aura.claim_slot(&header, 3.into(), &authorities).is_some()); - } - - #[test] - fn on_slot_returns_block() { - let _ = env_logger::builder().is_test(true).try_init(); - - let onchain_mock = onchain_mock_with_default_authorities_and_header(); - let mut aura = get_aura(onchain_mock, Default::default()); - - let slot_info = now_slot_with_default_header(0.into()); - - assert!( - SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false).is_some() - ); - } - - #[test] - fn on_slot_returns_no_block_if_slot_time_exceeded_for_multi_worker() { - let _ = env_logger::builder().is_test(true).try_init(); - - let onchain_mock = onchain_mock_with_default_authorities_and_header(); - let mut aura = get_aura_outdated(onchain_mock, Default::default()); - let slot_info = now_slot_with_default_header(0.into()); - - assert!( - SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false).is_none() - ); - } - - #[test] - fn on_slot_returns_block_if_slot_time_exceeded_for_single_worker() { - let _ = env_logger::builder().is_test(true).try_init(); - - let onchain_mock = onchain_mock_with_default_authorities_and_header(); - let mut aura = get_aura_outdated(onchain_mock, Default::default()); - let slot_info = now_slot_with_default_header(0.into()); - - assert!(SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), true).is_some()); - } - - #[test] - fn on_slot_for_multiple_shards_returns_blocks() { - let _ = env_logger::builder().is_test(true).try_init(); - - let onchain_mock = onchain_mock_with_default_authorities_and_header(); - let mut aura = get_aura(onchain_mock, Default::default()); - - let slot_info = now_slot_with_default_header(0.into()); - - let result = PerShardSlotWorkerScheduler::on_slot( - &mut aura, - slot_info, - vec![Default::default(), Default::default()], - false, - ); - - assert_eq!(result.len(), 2); - } - - #[test] - fn on_slot_with_nano_second_remaining_duration_does_not_panic() { - let _ = env_logger::builder().is_test(true).try_init(); - - let mut aura = get_default_aura(); - - let nano_dur = Duration::from_nanos(999); - let now = duration_now(); - - let slot_info = SlotInfo { - slot: 0.into(), - timestamp: now, - duration: nano_dur, - ends_at: now + nano_dur, - last_imported_integritee_parentchain_head: ParentchainHeaderBuilder::default().build(), - maybe_last_imported_target_a_parentchain_head: None, - maybe_last_imported_target_b_parentchain_head: None, - }; - - let result = PerShardSlotWorkerScheduler::on_slot( - &mut aura, - slot_info, - vec![Default::default(), Default::default()], - false, - ); - - assert_eq!(result.len(), 0); - } - - #[test] - fn on_slot_triggers_parentchain_block_import_if_slot_is_claimed() { - let _ = env_logger::builder().is_test(true).try_init(); - let latest_parentchain_header = ParentchainHeaderBuilder::default().with_number(84).build(); - let parentchain_block_import_trigger = - create_import_trigger_with_header(latest_parentchain_header.clone()); - let authorities = default_authorities(); - - let mut aura = get_aura( - onchain_mock(&latest_parentchain_header, authorities), - parentchain_block_import_trigger.clone(), - ); - - let slot_info = now_slot(0.into(), &latest_parentchain_header); - - let result = - SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false).unwrap(); - - assert_eq!( - result.block.block.block_data().layer_one_head, - latest_parentchain_header.hash() - ); - assert!(parentchain_block_import_trigger.has_import_been_called()); - } - - #[test] - fn on_slot_does_not_trigger_parentchain_block_import_if_slot_is_not_claimed() { - let _ = env_logger::builder().is_test(true).try_init(); - let latest_parentchain_header = ParentchainHeaderBuilder::default().with_number(84).build(); - let parentchain_block_import_trigger = - create_import_trigger_with_header(latest_parentchain_header.clone()); - let authorities = default_authorities(); - - let mut aura = get_aura( - onchain_mock(&latest_parentchain_header, authorities), - parentchain_block_import_trigger.clone(), - ); - - let slot_info = now_slot(2.into(), &latest_parentchain_header); - - let result = SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false); - - assert!(result.is_none()); - assert!(!parentchain_block_import_trigger.has_import_been_called()); - } - - #[test] - fn on_slot_claims_slot_if_latest_parentchain_header_in_queue_contains_correspondent_validateer_set( - ) { - let _ = env_logger::builder().is_test(true).try_init(); - let already_imported_parentchain_header = - ParentchainHeaderBuilder::default().with_number(84).build(); - let latest_parentchain_header = ParentchainHeaderBuilder::default().with_number(85).build(); - let parentchain_block_import_trigger = - create_import_trigger_with_header(latest_parentchain_header.clone()); - let validateer_set_one = create_validateer_set_from_publics(vec![ - Keyring::Alice.public(), - Keyring::Bob.public(), - ]); - let validateer_set_two = create_validateer_set_from_publics(vec![ - Keyring::Alice.public(), - Keyring::Bob.public(), - Keyring::Charlie.public(), - ]); - let onchain_mock = OnchainMock::default() - .add_validateer_set(&already_imported_parentchain_header, Some(validateer_set_one)) - .add_validateer_set(&latest_parentchain_header, Some(validateer_set_two)); - - let mut aura = get_aura(onchain_mock, parentchain_block_import_trigger.clone()); - - let slot_info = now_slot(3.into(), &already_imported_parentchain_header); - - let result = - SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false).unwrap(); - - assert_eq!( - result.block.block.block_data().layer_one_head, - latest_parentchain_header.hash() - ); - assert!(parentchain_block_import_trigger.has_import_been_called()); - } - - #[test] - fn on_slot_does_not_claim_slot_if_latest_parentchain_header_in_queue_contains_correspondent_validateer_set( - ) { - let _ = env_logger::builder().is_test(true).try_init(); - let already_imported_parentchain_header = - ParentchainHeaderBuilder::default().with_number(84).build(); - let latest_parentchain_header = ParentchainHeaderBuilder::default().with_number(85).build(); - let parentchain_block_import_trigger = - create_import_trigger_with_header(latest_parentchain_header.clone()); - let validateer_set_one = create_validateer_set_from_publics(vec![ - Keyring::Alice.public(), - Keyring::Bob.public(), - ]); - let validateer_set_two = create_validateer_set_from_publics(vec![ - Keyring::Alice.public(), - Keyring::Bob.public(), - Keyring::Charlie.public(), - ]); - let onchain_mock = OnchainMock::default() - .add_validateer_set(&already_imported_parentchain_header, Some(validateer_set_one)) - .add_validateer_set(&latest_parentchain_header, Some(validateer_set_two)); - - let mut aura = get_aura(onchain_mock, parentchain_block_import_trigger.clone()); - - // If the validateer set one (instead of the latest one) is looked up, the slot will be claimed. But it should not, as the latest one should be used. - let slot_info = now_slot(2.into(), &already_imported_parentchain_header); - let result = SimpleSlotWorker::on_slot(&mut aura, slot_info, Default::default(), false); - - assert!(result.is_none()); - assert!(!parentchain_block_import_trigger.has_import_been_called()); - } - - #[test] - fn proposing_remaining_duration_works() { - let slot_info = now_slot_with_default_header(0.into()); - - // hard to compare actual numbers but we can at least ensure that the general concept works - assert!( - proposing_remaining_duration(&slot_info, duration_now()) - < SLOT_DURATION.mul_f32(BLOCK_PROPOSAL_SLOT_PORTION + 0.01) - ); - } - - #[test] - fn proposing_remaining_duration_works_for_now_before_slot_timestamp() { - let slot_info = now_slot_with_default_header(0.into()); - - assert!( - proposing_remaining_duration(&slot_info, Duration::from_millis(0)) - < SLOT_DURATION.mul_f32(BLOCK_PROPOSAL_SLOT_PORTION + 0.01) - ); - } - - #[test] - fn proposing_remaining_duration_returns_default_if_now_after_slot() { - let slot_info = now_slot_with_default_header(0.into()); - - assert_eq!( - proposing_remaining_duration(&slot_info, duration_now() + SLOT_DURATION), - Default::default() - ); - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/proposer_factory.rs b/bitacross-worker/sidechain/consensus/aura/src/proposer_factory.rs deleted file mode 100644 index 61fa21557f..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/proposer_factory.rs +++ /dev/null @@ -1,131 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::slot_proposer::{ExternalitiesFor, SlotProposer}; -use codec::Encode; -use finality_grandpa::BlockNumberOps; -use ita_stf::{Getter, TrustedCallSigned}; -use itp_ocall_api::EnclaveMetricsOCallApi; -use itp_sgx_externalities::{SgxExternalitiesTrait, StateHash}; -use itp_stf_executor::traits::StateUpdateProposer; -use itp_top_pool_author::traits::AuthorApi; -use itp_types::H256; -use its_block_composer::ComposeBlock; -use its_consensus_common::{Environment, Error as ConsensusError}; -use its_primitives::traits::{ - Block as SidechainBlockTrait, Header as HeaderTrait, ShardIdentifierFor, - SignedBlock as SignedSidechainBlockTrait, -}; -use its_state::{SidechainState, SidechainSystemExt}; -use sp_runtime::{ - traits::{Block, NumberFor}, - MultiSignature, -}; -use std::{marker::PhantomData, sync::Arc}; - -///! `ProposerFactory` instance containing all the data to create the `SlotProposer` for the -/// next `Slot`. -pub struct ProposerFactory< - ParentchainBlock: Block, - TopPoolAuthor, - StfExecutor, - BlockComposer, - MetricsApi, -> { - top_pool_author: Arc, - stf_executor: Arc, - block_composer: Arc, - metrics_api: Arc, - _phantom: PhantomData, -} - -impl - ProposerFactory -{ - pub fn new( - top_pool_executor: Arc, - stf_executor: Arc, - block_composer: Arc, - metrics_api: Arc, - ) -> Self { - Self { - top_pool_author: top_pool_executor, - stf_executor, - block_composer, - metrics_api, - _phantom: Default::default(), - } - } -} - -impl< - ParentchainBlock: Block, - SignedSidechainBlock, - TopPoolAuthor, - StfExecutor, - BlockComposer, - MetricsApi, - > Environment - for ProposerFactory -where - NumberFor: BlockNumberOps, - SignedSidechainBlock: SignedSidechainBlockTrait - + 'static, - SignedSidechainBlock::Block: SidechainBlockTrait, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - TopPoolAuthor: - AuthorApi + Send + Sync + 'static, - StfExecutor: StateUpdateProposer + Send + Sync + 'static, - ExternalitiesFor: - SgxExternalitiesTrait + SidechainState + SidechainSystemExt + StateHash, - as SgxExternalitiesTrait>::SgxExternalitiesType: Encode, - BlockComposer: ComposeBlock< - ExternalitiesFor, - ParentchainBlock, - SignedSidechainBlock = SignedSidechainBlock, - > + Send - + Sync - + 'static, - MetricsApi: EnclaveMetricsOCallApi, -{ - type Proposer = SlotProposer< - ParentchainBlock, - SignedSidechainBlock, - TopPoolAuthor, - StfExecutor, - BlockComposer, - MetricsApi, - >; - type Error = ConsensusError; - - fn init( - &mut self, - parent_header: ParentchainBlock::Header, - shard: ShardIdentifierFor, - ) -> Result { - Ok(SlotProposer { - top_pool_author: self.top_pool_author.clone(), - stf_executor: self.stf_executor.clone(), - block_composer: self.block_composer.clone(), - parentchain_header: parent_header, - shard, - metrics_api: self.metrics_api.clone(), - _phantom: PhantomData, - }) - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/slot_proposer.rs b/bitacross-worker/sidechain/consensus/aura/src/slot_proposer.rs deleted file mode 100644 index 2baea76519..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/slot_proposer.rs +++ /dev/null @@ -1,206 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use codec::Encode; -use finality_grandpa::BlockNumberOps; -use ita_stf::{Getter, TrustedCallSigned}; -use itp_enclave_metrics::EnclaveMetric; -use itp_ocall_api::EnclaveMetricsOCallApi; -use itp_sgx_externalities::{SgxExternalitiesTrait, StateHash}; -use itp_stf_executor::traits::StateUpdateProposer; -use itp_time_utils::now_as_millis; -use itp_top_pool_author::traits::AuthorApi; -use itp_types::H256; -use its_block_composer::ComposeBlock; -use its_consensus_common::{Error as ConsensusError, Proposal, Proposer}; -use its_primitives::traits::{ - Block as SidechainBlockTrait, Header as HeaderTrait, ShardIdentifierFor, - SignedBlock as SignedSidechainBlockTrait, -}; -use its_state::{SidechainState, SidechainSystemExt}; -use log::*; -use sp_runtime::{ - traits::{Block, NumberFor}, - MultiSignature, -}; -use std::{marker::PhantomData, string::ToString, sync::Arc, time::Duration, vec::Vec}; - -pub type ExternalitiesFor = >::Externalities; -///! `SlotProposer` instance that has access to everything needed to propose a sidechain block. -pub struct SlotProposer< - ParentchainBlock: Block, - SignedSidechainBlock: SignedSidechainBlockTrait, - TopPoolAuthor, - StfExecutor, - BlockComposer, - MetricsApi, -> { - pub(crate) top_pool_author: Arc, - pub(crate) stf_executor: Arc, - pub(crate) block_composer: Arc, - pub(crate) parentchain_header: ParentchainBlock::Header, - pub(crate) shard: ShardIdentifierFor, - pub(crate) metrics_api: Arc, - pub(crate) _phantom: PhantomData, -} - -impl< - ParentchainBlock, - SignedSidechainBlock, - TopPoolAuthor, - BlockComposer, - StfExecutor, - MetricsApi, - > Proposer - for SlotProposer< - ParentchainBlock, - SignedSidechainBlock, - TopPoolAuthor, - StfExecutor, - BlockComposer, - MetricsApi, - > where - ParentchainBlock: Block, - NumberFor: BlockNumberOps, - SignedSidechainBlock: SignedSidechainBlockTrait - + 'static, - SignedSidechainBlock::Block: SidechainBlockTrait, - <::Block as SidechainBlockTrait>::HeaderType: - HeaderTrait, - StfExecutor: StateUpdateProposer, - ExternalitiesFor: - SgxExternalitiesTrait + SidechainState + SidechainSystemExt + StateHash, - as SgxExternalitiesTrait>::SgxExternalitiesType: Encode, - TopPoolAuthor: - AuthorApi + Send + Sync + 'static, - BlockComposer: ComposeBlock< - ExternalitiesFor, - ParentchainBlock, - SignedSidechainBlock = SignedSidechainBlock, - > + Send - + Sync - + 'static, - MetricsApi: EnclaveMetricsOCallApi, -{ - /// Proposes a new sidechain block. - /// - /// This includes the following steps: - /// 1) Retrieve all trusted calls from the top pool. - /// 2) Calculate a new state that will be proposed in the sidechain block. - /// 3) Compose the sidechain block and the parentchain confirmation. - fn propose( - &self, - max_duration: Duration, - ) -> Result, ConsensusError> { - let mut started = std::time::Instant::now(); - let latest_parentchain_header = &self.parentchain_header; - - // 1) Retrieve trusted calls from top pool. - let trusted_calls = self.top_pool_author.get_pending_trusted_calls(self.shard); - - if !trusted_calls.is_empty() { - debug!("Got following trusted calls from pool: {:?}", trusted_calls); - } - - if let Err(e) = self - .metrics_api - .update_metric(EnclaveMetric::SidechainSlotPrepareTime(started.elapsed())) - { - warn!("Failed to update metric for sidechain slot prepare time: {:?}", e); - }; - - started = std::time::Instant::now(); - // 2) Execute trusted calls. - let batch_execution_result = self - .stf_executor - .propose_state_update( - &trusted_calls, - latest_parentchain_header, - &self.shard, - max_duration, - |mut sidechain_db| { - sidechain_db.reset_events(); - sidechain_db - .set_block_number(&sidechain_db.get_block_number().map_or(1, |n| n + 1)); - sidechain_db.set_timestamp(&now_as_millis()); - sidechain_db.set_parentchain_block_number(latest_parentchain_header); - sidechain_db - }, - ) - .map_err(|e| ConsensusError::Other(e.to_string().into()))?; - - let parentchain_extrinsics = batch_execution_result.get_extrinsic_callbacks(); - - let executed_operation_hashes: Vec<_> = - batch_execution_result.get_executed_operation_hashes().to_vec(); - let number_executed_transactions = executed_operation_hashes.len(); - - // store the rpc response value to top pool - let rpc_responses_values = batch_execution_result.get_connection_updates(); - self.top_pool_author.update_connection_state(rpc_responses_values); - - // Remove all not successfully executed operations from the top pool. - let failed_operations = batch_execution_result.get_failed_operations(); - self.top_pool_author.remove_calls_from_pool( - self.shard, - failed_operations - .into_iter() - .map(|e| { - let is_success = e.is_success(); - (e.trusted_operation_or_hash, is_success) - }) - .collect(), - ); - - if let Err(e) = self - .metrics_api - .update_metric(EnclaveMetric::SidechainSlotStfExecutionTime(started.elapsed())) - { - warn!("Failed to update metric for sidechain slot stf execution time: {:?}", e); - }; - - started = std::time::Instant::now(); - - // 3) Compose sidechain block. - let sidechain_block = self - .block_composer - .compose_block( - latest_parentchain_header, - executed_operation_hashes, - self.shard, - batch_execution_result.state_hash_before_execution, - &batch_execution_result.state_after_execution, - ) - .map_err(|e| ConsensusError::Other(e.to_string().into()))?; - - if let Err(e) = self - .metrics_api - .update_metric(EnclaveMetric::SidechainSlotBlockCompositionTime(started.elapsed())) - { - warn!("Failed to update metric for sidechain slot block composition time: {:?}", e); - }; - - info!( - "Queue/Timeslot/Transactions: {:?};{}ms;{}", - trusted_calls.len(), - max_duration.as_millis(), - number_executed_transactions - ); - - Ok(Proposal { block: sidechain_block, parentchain_effects: parentchain_extrinsics }) - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs b/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs deleted file mode 100644 index be23004cf0..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/block_importer_tests.rs +++ /dev/null @@ -1,318 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - block_importer::BlockImporter, - test::{fixtures::validateer, mocks::peer_updater_mock::PeerUpdaterMock}, - ShardIdentifierFor, -}; -use codec::Encode; -use core::assert_matches::assert_matches; -use itc_parentchain_block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; -use itc_parentchain_test::{ParentchainBlockBuilder, ParentchainHeaderBuilder}; -use itp_sgx_crypto::{aes::Aes, mocks::KeyRepositoryMock, StateCrypto}; -use itp_sgx_externalities::SgxExternalitiesDiffType; -use itp_stf_state_handler::handle_state::HandleState; -use itp_test::mock::{ - handle_state_mock::HandleStateMock, - onchain_mock::OnchainMock, - stf_mock::{GetterMock, TrustedCallSignedMock}, -}; -use itp_time_utils::{duration_now, now_as_millis}; -use itp_top_pool_author::mocks::AuthorApiMock; -use itp_types::{Block as ParentchainBlock, Header as ParentchainHeader, H256}; -use its_consensus_common::{BlockImport, Error as ConsensusError}; -use its_primitives::{ - traits::{SignBlock, SignedBlock}, - types::SignedBlock as SignedSidechainBlock, -}; -use its_state::StateUpdate; -use its_test::{ - sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}, - sidechain_block_data_builder::SidechainBlockDataBuilder, - sidechain_header_builder::SidechainHeaderBuilder, -}; -use sp_core::{blake2_256, ed25519::Pair}; -use sp_keyring::ed25519::Keyring; -use sp_runtime::generic::SignedBlock as SignedParentchainBlock; -use std::sync::Arc; - -type TestTopPoolAuthor = AuthorApiMock; -type TestParentchainBlockImportTrigger = - TriggerParentchainBlockImportMock>; -type TestStateKeyRepo = KeyRepositoryMock; -type TestBlockImporter = BlockImporter< - Pair, - ParentchainBlock, - SignedSidechainBlock, - OnchainMock, - HandleStateMock, - TestStateKeyRepo, - TestTopPoolAuthor, - TestParentchainBlockImportTrigger, - PeerUpdaterMock, - TrustedCallSignedMock, - GetterMock, ->; - -fn state_key() -> Aes { - Aes::new([3u8; 16], [0u8; 16]) -} - -fn shard() -> ShardIdentifierFor { - blake2_256(&[1, 2, 3, 4, 5, 6]).into() -} - -fn default_authority() -> Pair { - Keyring::Alice.pair() -} - -fn test_fixtures( - parentchain_header: &ParentchainHeader, - parentchain_block_import_trigger: Arc, -) -> (TestBlockImporter, Arc, Arc) { - let state_handler = Arc::new(HandleStateMock::from_shard(shard()).unwrap()); - let top_pool_author = Arc::new(TestTopPoolAuthor::default()); - let ocall_api = Arc::new( - OnchainMock::default() - .add_validateer_set(parentchain_header, Some(vec![Keyring::Alice.public().into()])), - ); - let state_key_repository = Arc::new(TestStateKeyRepo::new(state_key())); - - let peer_updater_mock = Arc::new(PeerUpdaterMock {}); - - let block_importer = TestBlockImporter::new( - state_handler.clone(), - state_key_repository, - top_pool_author.clone(), - parentchain_block_import_trigger, - ocall_api, - peer_updater_mock, - ); - - (block_importer, state_handler, top_pool_author) -} - -fn test_fixtures_with_default_import_trigger( - parentchain_header: &ParentchainHeader, -) -> (TestBlockImporter, Arc, Arc) { - test_fixtures(parentchain_header, Arc::new(TestParentchainBlockImportTrigger::default())) -} - -fn empty_encrypted_state_update(state_handler: &HandleStateMock) -> Vec { - let (_, apriori_state_hash) = state_handler.load_cloned(&shard()).unwrap(); - let empty_state_diff = SgxExternalitiesDiffType::default(); - let mut state_update = - StateUpdate::new(apriori_state_hash, apriori_state_hash, empty_state_diff).encode(); - state_key().encrypt(&mut state_update).unwrap(); - state_update -} - -fn signed_block( - parentchain_header: &ParentchainHeader, - state_handler: &HandleStateMock, - signer: Pair, -) -> SignedSidechainBlock { - let state_update = empty_encrypted_state_update(state_handler); - - let header = SidechainHeaderBuilder::default() - .with_parent_hash(H256::default()) - .with_shard(shard()) - .build(); - - let block_data = SidechainBlockDataBuilder::default() - .with_timestamp(now_as_millis()) - .with_layer_one_head(parentchain_header.hash()) - .with_signer(signer.clone()) - .with_payload(state_update) - .build(); - - SidechainBlockBuilder::default() - .with_header(header) - .with_block_data(block_data) - .with_signer(signer) - .build_signed() -} - -fn default_authority_signed_block( - parentchain_header: &ParentchainHeader, - state_handler: &HandleStateMock, -) -> SignedSidechainBlock { - signed_block(parentchain_header, state_handler, default_authority()) -} - -#[test] -fn simple_block_import_works() { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let (block_importer, state_handler, _) = - test_fixtures_with_default_import_trigger(&parentchain_header); - let signed_sidechain_block = - default_authority_signed_block(&parentchain_header, state_handler.as_ref()); - - block_importer - .import_block(signed_sidechain_block, &parentchain_header) - .unwrap(); -} - -#[test] -fn block_import_with_invalid_signature_fails() { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let (block_importer, state_handler, _) = - test_fixtures_with_default_import_trigger(&parentchain_header); - - let state_update = empty_encrypted_state_update(state_handler.as_ref()); - - let header = SidechainHeaderBuilder::default() - .with_parent_hash(H256::default()) - .with_shard(shard()) - .build(); - - let block_data = SidechainBlockDataBuilder::default() - .with_timestamp(duration_now().as_millis() as u64) - .with_layer_one_head(parentchain_header.hash()) - .with_signer(Keyring::Charlie.pair()) - .with_payload(state_update) - .build(); - - let block = SidechainBlockBuilder::default() - .with_signer(Keyring::Charlie.pair()) - .with_header(header) - .with_block_data(block_data) - .build(); - - // Bob signs the block, but Charlie is set as the author -> invalid signature. - let invalid_signature_block: SignedSidechainBlock = block.sign_block(&Keyring::Bob.pair()); - - assert!(!invalid_signature_block.verify_signature()); - assert!(block_importer - .import_block(invalid_signature_block, &parentchain_header) - .is_err()); -} - -#[test] -fn block_import_with_invalid_parentchain_block_fails() { - let parentchain_header_invalid = ParentchainHeaderBuilder::default().with_number(2).build(); - let parentchain_header = ParentchainHeaderBuilder::default().with_number(10).build(); - let (block_importer, state_handler, _) = - test_fixtures_with_default_import_trigger(&parentchain_header); - - let signed_sidechain_block = - default_authority_signed_block(&parentchain_header_invalid, state_handler.as_ref()); - - assert!(block_importer - .import_block(signed_sidechain_block, &parentchain_header) - .is_err()); -} - -#[test] -fn cleanup_removes_tops_from_pool() { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let (block_importer, state_handler, top_pool_author) = - test_fixtures_with_default_import_trigger(&parentchain_header); - let signed_sidechain_block = - default_authority_signed_block(&parentchain_header, state_handler.as_ref()); - let bob_signed_sidechain_block = - signed_block(&parentchain_header, state_handler.as_ref(), Keyring::Bob.pair()); - - block_importer.cleanup(&signed_sidechain_block).unwrap(); - block_importer.cleanup(&bob_signed_sidechain_block).unwrap(); - - assert_eq!(2, *top_pool_author.remove_attempts.read().unwrap()); -} - -#[test] -fn sidechain_block_import_triggers_parentchain_block_import() { - let previous_parentchain_header = ParentchainHeaderBuilder::default().with_number(4).build(); - let latest_parentchain_header = ParentchainHeaderBuilder::default() - .with_number(5) - .with_parent_hash(previous_parentchain_header.hash()) - .build(); - - let latest_parentchain_block = ParentchainBlockBuilder::default() - .with_header(latest_parentchain_header.clone()) - .build_signed(); - - let parentchain_block_import_trigger = Arc::new( - TestParentchainBlockImportTrigger::default() - .with_latest_imported(Some(latest_parentchain_block)), - ); - let (block_importer, state_handler, _) = - test_fixtures(&latest_parentchain_header, parentchain_block_import_trigger.clone()); - - let signed_sidechain_block = - default_authority_signed_block(&latest_parentchain_header, state_handler.as_ref()); - - block_importer - .import_block(signed_sidechain_block, &previous_parentchain_header) - .unwrap(); - - assert!(parentchain_block_import_trigger.has_import_been_called()); -} - -#[test] -fn peek_parentchain_block_finds_block_in_queue() { - let previous_parentchain_header = ParentchainHeaderBuilder::default().with_number(4).build(); - let latest_parentchain_header = ParentchainHeaderBuilder::default() - .with_number(5) - .with_parent_hash(previous_parentchain_header.hash()) - .build(); - - let latest_parentchain_block = ParentchainBlockBuilder::default() - .with_header(latest_parentchain_header.clone()) - .build_signed(); - - let parentchain_block_import_trigger = Arc::new( - TestParentchainBlockImportTrigger::default() - .with_latest_imported(Some(latest_parentchain_block)), - ); - - let (block_importer, state_handler, _) = - test_fixtures(&latest_parentchain_header, parentchain_block_import_trigger); - - let signed_sidechain_block = - default_authority_signed_block(&latest_parentchain_header, state_handler.as_ref()); - - let peeked_header = block_importer - .peek_parentchain_header(&signed_sidechain_block.block, &previous_parentchain_header) - .unwrap(); - - assert_eq!(peeked_header, latest_parentchain_header); -} - -#[test] -fn peek_parentchain_block_returns_error_if_no_corresponding_block_can_be_found() { - let previous_parentchain_header = ParentchainHeaderBuilder::default().with_number(1).build(); - let latest_parentchain_header = ParentchainHeaderBuilder::default() - .with_number(2) - .with_parent_hash(previous_parentchain_header.hash()) - .build(); - - let parentchain_block_import_trigger = Arc::new( - TestParentchainBlockImportTrigger::default(), // Parentchain block import queue is empty, so nothing will be found when peeked. - ); - - let (block_importer, state_handler, _) = - test_fixtures(&latest_parentchain_header, parentchain_block_import_trigger); - - let signed_sidechain_block = - default_authority_signed_block(&latest_parentchain_header, state_handler.as_ref()); - - let peek_result = block_importer - .peek_parentchain_header(&signed_sidechain_block.block, &previous_parentchain_header); - - assert_matches!(peek_result, Err(ConsensusError::Other(_))); -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs b/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs deleted file mode 100644 index de85405549..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/mod.rs +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod types; - -use itp_types::{AccountId, Enclave}; -use std::time::Duration; - -pub const SLOT_DURATION: Duration = Duration::from_millis(300); - -pub fn validateer(account: AccountId) -> Enclave { - Enclave::default() -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/types.rs b/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/types.rs deleted file mode 100644 index b5eae68582..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/fixtures/types.rs +++ /dev/null @@ -1,48 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{test::mocks::environment_mock::EnvironmentMock, Aura}; -use itc_parentchain_block_import_dispatcher::trigger_parentchain_block_import_mock::TriggerParentchainBlockImportMock; -use itp_test::mock::{handle_state_mock::HandleStateMock, onchain_mock::OnchainMock}; -use itp_types::Block as ParentchainBlock; -use its_primitives::{ - traits::{ - Block as SidechainBlockTrait, Header as SidechainHeaderTrait, - SignedBlock as SignedBlockTrait, - }, - types::block::SignedBlock as SignedSidechainBlock, -}; -use lc_scheduled_enclave::ScheduledEnclaveMock; -use sp_runtime::{app_crypto::ed25519, generic::SignedBlock}; - -type AuthorityPair = ed25519::Pair; - -pub type ShardIdentifierFor = - <<::Block as SidechainBlockTrait>::HeaderType as SidechainHeaderTrait>::ShardIdentifier; - -pub type TestAura = Aura< - AuthorityPair, - ParentchainBlock, - SignedSidechainBlock, - E, - OnchainMock, - TriggerParentchainBlockImportMock>, - TriggerParentchainBlockImportMock>, - TriggerParentchainBlockImportMock>, - ScheduledEnclaveMock, - HandleStateMock, ->; diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/environment_mock.rs b/bitacross-worker/sidechain/consensus/aura/src/test/mocks/environment_mock.rs deleted file mode 100644 index 58f98d3687..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/environment_mock.rs +++ /dev/null @@ -1,58 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - test::{ - fixtures::types::ShardIdentifierFor, - mocks::proposer_mock::{DefaultProposerMock, OutdatedBlockProposerMock}, - }, - ConsensusError, -}; -use itp_types::{Block as ParentchainBlock, Header}; -use its_consensus_common::Environment; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; - -/// Mock proposer environment. -pub struct EnvironmentMock; - -impl Environment for EnvironmentMock { - type Proposer = DefaultProposerMock; - type Error = ConsensusError; - - fn init( - &mut self, - header: Header, - _: ShardIdentifierFor, - ) -> Result { - Ok(DefaultProposerMock { parentchain_header: header }) - } -} - -pub struct OutdatedBlockEnvironmentMock; - -impl Environment for OutdatedBlockEnvironmentMock { - type Proposer = OutdatedBlockProposerMock; - type Error = ConsensusError; - - fn init( - &mut self, - header: Header, - _: ShardIdentifierFor, - ) -> Result { - Ok(OutdatedBlockProposerMock { parentchain_header: header }) - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/mod.rs b/bitacross-worker/sidechain/consensus/aura/src/test/mocks/mod.rs deleted file mode 100644 index f5c7248d2f..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod environment_mock; -pub mod peer_updater_mock; -pub mod proposer_mock; diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/peer_updater_mock.rs b/bitacross-worker/sidechain/consensus/aura/src/test/mocks/peer_updater_mock.rs deleted file mode 100644 index b474e825c1..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/peer_updater_mock.rs +++ /dev/null @@ -1,7 +0,0 @@ -use itc_peer_top_broadcaster::PeerUpdater; - -pub struct PeerUpdaterMock {} - -impl PeerUpdater for PeerUpdaterMock { - fn update(&self, _peers: Vec) {} -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/proposer_mock.rs b/bitacross-worker/sidechain/consensus/aura/src/test/mocks/proposer_mock.rs deleted file mode 100644 index 00f78298c3..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/mocks/proposer_mock.rs +++ /dev/null @@ -1,73 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::ConsensusError; -use itp_time_utils::now_as_millis; -use itp_types::{Block as ParentchainBlock, Header}; -use its_consensus_common::{Proposal, Proposer}; -use its_primitives::types::block::SignedBlock as SignedSidechainBlock; -use its_test::{ - sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}, - sidechain_block_data_builder::SidechainBlockDataBuilder, -}; -use std::time::Duration; - -pub struct DefaultProposerMock { - pub(crate) parentchain_header: Header, -} - -impl Proposer for DefaultProposerMock { - fn propose( - &self, - _max_duration: Duration, - ) -> Result, ConsensusError> { - Ok(Proposal { - block: { - let block_data = SidechainBlockDataBuilder::random() - .with_layer_one_head(self.parentchain_header.hash()) - .build(); - SidechainBlockBuilder::random().with_block_data(block_data).build_signed() - }, - - parentchain_effects: Default::default(), - }) - } -} - -pub struct OutdatedBlockProposerMock { - pub(crate) parentchain_header: Header, -} - -impl Proposer for OutdatedBlockProposerMock { - fn propose( - &self, - _max_duration: Duration, - ) -> Result, ConsensusError> { - let past = now_as_millis() - 1000; - Ok(Proposal { - block: { - let block_data = SidechainBlockDataBuilder::random() - .with_layer_one_head(self.parentchain_header.hash()) - .with_timestamp(past) - .build(); - SidechainBlockBuilder::random().with_block_data(block_data).build_signed() - }, - - parentchain_effects: Default::default(), - }) - } -} diff --git a/bitacross-worker/sidechain/consensus/aura/src/test/mod.rs b/bitacross-worker/sidechain/consensus/aura/src/test/mod.rs deleted file mode 100644 index 7c40ba019d..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/test/mod.rs +++ /dev/null @@ -1,20 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -mod block_importer_tests; -pub mod fixtures; -pub mod mocks; diff --git a/bitacross-worker/sidechain/consensus/aura/src/verifier.rs b/bitacross-worker/sidechain/consensus/aura/src/verifier.rs deleted file mode 100644 index 121cef58eb..0000000000 --- a/bitacross-worker/sidechain/consensus/aura/src/verifier.rs +++ /dev/null @@ -1,90 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{authorities, EnclaveOnChainOCallApi, ShardIdentifierFor}; -use core::marker::PhantomData; -use its_block_verification::verify_sidechain_block; -use its_consensus_common::{Error as ConsensusError, Verifier}; -use its_primitives::{ - traits::{Block as SidechainBlockTrait, SignedBlock as SignedSidechainBlockTrait}, - types::block::BlockHash, -}; -use its_validateer_fetch::ValidateerFetch; -use sp_core::crypto::UncheckedFrom; -use sp_runtime::{app_crypto::Pair, traits::Block as ParentchainBlockTrait}; -use std::{fmt::Debug, time::Duration}; - -#[derive(Default)] -pub struct AuraVerifier -where - SignedSidechainBlock: SignedSidechainBlockTrait + 'static, - SignedSidechainBlock::Block: SidechainBlockTrait, -{ - slot_duration: Duration, - last_sidechain_block: Option, - _phantom: PhantomData<(AuthorityPair, ParentchainBlock, Context)>, -} - -impl - AuraVerifier -where - SignedSidechainBlock: SignedSidechainBlockTrait + 'static, - SignedSidechainBlock::Block: SidechainBlockTrait, -{ - pub fn new( - slot_duration: Duration, - last_sidechain_block: Option, - ) -> Self { - Self { slot_duration, last_sidechain_block, _phantom: Default::default() } - } -} - -impl - Verifier - for AuraVerifier -where - AuthorityPair: Pair, - AuthorityPair::Public: Debug + UncheckedFrom<[u8; 32]>, - // todo: Relax hash trait bound, but this needs a change to some other parts in the code. - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait + 'static, - SignedSidechainBlock::Block: SidechainBlockTrait, - Context: ValidateerFetch + EnclaveOnChainOCallApi + Send + Sync, -{ - type BlockImportParams = SignedSidechainBlock; - - type Context = Context; - - fn verify( - &self, - signed_block: SignedSidechainBlock, - parentchain_header: &ParentchainBlock::Header, - _shard: ShardIdentifierFor, - ctx: &Self::Context, - ) -> Result { - let authorities = - authorities::<_, AuthorityPair, ParentchainBlock::Header>(ctx, parentchain_header)?; - - Ok(verify_sidechain_block::( - signed_block, - self.slot_duration, - &self.last_sidechain_block, - parentchain_header, - &authorities, - )?) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/Cargo.toml b/bitacross-worker/sidechain/consensus/common/Cargo.toml deleted file mode 100644 index 6408dd9c08..0000000000 --- a/bitacross-worker/sidechain/consensus/common/Cargo.toml +++ /dev/null @@ -1,85 +0,0 @@ -[package] -name = "its-consensus-common" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false } -log = { version = "0.4", default-features = false } -thiserror = { version = "1.0.26", optional = true } - -# local deps -itc-parentchain-light-client = { path = "../../../core/parentchain/light-client", default-features = false } -itp-enclave-metrics = { path = "../../../core-primitives/enclave-metrics", default-features = false } -itp-extrinsics-factory = { path = "../../../core-primitives/extrinsics-factory", default-features = false } -itp-import-queue = { path = "../../../core-primitives/import-queue", default-features = false } -itp-node-api-metadata = { path = "../../../core-primitives/node-api/metadata", default-features = false } -itp-node-api-metadata-provider = { path = "../../../core-primitives/node-api/metadata-provider", default-features = false } -itp-ocall-api = { path = "../../../core-primitives/ocall-api", default-features = false } -itp-settings = { path = "../../../core-primitives/settings" } -itp-sgx-crypto = { path = "../../../core-primitives/sgx/crypto", default-features = false } -itp-types = { path = "../../../core-primitives/types", default-features = false } -its-block-verification = { path = "../../block-verification", optional = true, default-features = false } -its-primitives = { path = "../../primitives", default-features = false } -its-state = { path = "../../state", default-features = false } - -# sgx deps -sgx_tstd = { optional = true, git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master" } -sgx_types = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "master" } -thiserror-sgx = { package = "thiserror", optional = true, git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3" } - -# substrate deps -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[dev-dependencies] -# local -itc-parentchain-test = { path = "../../../core/parentchain/test" } -itp-sgx-externalities = { default-features = false, path = "../../../core-primitives/substrate-sgx/externalities" } -itp-test = { path = "../../../core-primitives/test" } -its-test = { path = "../../test" } -fork-tree = { path = "../../fork-tree", default-features = false } - -# substrate -sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[features] -default = ["std"] -std = [ - "codec/std", - "log/std", - "thiserror", - # local - "itc-parentchain-light-client/std", - "itp-import-queue/std", - "itp-enclave-metrics/std", - "itp-extrinsics-factory/std", - "itp-node-api-metadata/std", - "itp-node-api-metadata-provider/std", - "itp-ocall-api/std", - "itp-sgx-crypto/std", - "itp-sgx-externalities/std", - "itp-types/std", - "its-primitives/std", - "its-block-verification/std", - "its-state/std", - "fork-tree/std", - # substrate - "sp-runtime/std", -] -sgx = [ - "sgx_tstd", - "thiserror-sgx", - # local - "itc-parentchain-light-client/sgx", - "itp-import-queue/sgx", - "itp-enclave-metrics/sgx", - "itp-extrinsics-factory/sgx", - "itp-node-api-metadata-provider/sgx", - "itp-sgx-crypto/sgx", - "itp-sgx-externalities/sgx", - "its-state/sgx", - "fork-tree/sgx", - # scs - "its-block-verification/sgx", -] diff --git a/bitacross-worker/sidechain/consensus/common/src/block_import.rs b/bitacross-worker/sidechain/consensus/common/src/block_import.rs deleted file mode 100644 index 826b1e456b..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/block_import.rs +++ /dev/null @@ -1,201 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Abstraction around block import - -use crate::{Error, Verifier}; -use codec::Decode; -use itp_enclave_metrics::EnclaveMetric; -use itp_ocall_api::{EnclaveMetricsOCallApi, EnclaveSidechainOCallApi}; -use itp_sgx_crypto::StateCrypto; -use its_primitives::traits::{ - Block as SidechainBlockTrait, BlockData, Header as HeaderTrait, ShardIdentifierFor, - SignedBlock as SignedSidechainBlockTrait, -}; -use its_state::{LastBlockExt, SidechainState}; -use log::*; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::{time::Instant, vec::Vec}; - -pub trait BlockImport -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, -{ - /// The verifier for of the respective consensus instance. - type Verifier: Verifier< - ParentchainBlock, - SignedSidechainBlock, - BlockImportParams = SignedSidechainBlock, - Context = Self::Context, - >; - - /// Context needed to derive verifier relevant data. - type SidechainState: SidechainState + LastBlockExt; - - /// Provides the cryptographic functions for our the state encryption. - type StateCrypto: StateCrypto; - - /// Context needed to derive verifier relevant data. - type Context: EnclaveSidechainOCallApi + EnclaveMetricsOCallApi; - - /// Get a verifier instance. - fn verifier( - &self, - maybe_last_sidechain_block: Option, - ) -> Self::Verifier; - - /// Apply a state update by providing a mutating function. - fn apply_state_update( - &self, - shard: &ShardIdentifierFor, - mutating_function: F, - ) -> Result<(), Error> - where - F: FnOnce(Self::SidechainState) -> Result; - - /// Verify a sidechain block that is to be imported. - fn verify_import( - &self, - shard: &ShardIdentifierFor, - verifying_function: F, - ) -> Result - where - F: FnOnce(&Self::SidechainState) -> Result; - - /// Key that is used for state encryption. - fn state_key(&self) -> Result; - - /// Getter for the context. - fn get_context(&self) -> &Self::Context; - - /// Import parentchain blocks up to and including the one we see in the sidechain block that - /// is scheduled for import. - /// - /// Returns the latest header. If no block was imported with the trigger, - /// we return `last_imported_parentchain_header`. - fn import_parentchain_block( - &self, - sidechain_block: &SignedSidechainBlock::Block, - last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> Result; - - /// Peek the parentchain import queue for the block that is associated with a given sidechain. - /// Does not perform the import or mutate the queue. - /// - /// Warning: Be aware that peeking the parentchain block means that it is not verified (that happens upon import). - fn peek_parentchain_header( - &self, - sidechain_block: &SignedSidechainBlock::Block, - last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> Result; - /// Cleanup task after import is done. - fn cleanup(&self, signed_sidechain_block: &SignedSidechainBlock) -> Result<(), Error>; - - /// Import a sidechain block and mutate state by `apply_state_update`. - fn import_block( - &self, - signed_sidechain_block: SignedSidechainBlock, - parentchain_header: &ParentchainBlock::Header, - ) -> Result { - let start_time = Instant::now(); - - let sidechain_block = signed_sidechain_block.block().clone(); - let shard = sidechain_block.header().shard_id(); - let block_number = signed_sidechain_block.block().header().block_number(); - - debug!( - "Attempting to import sidechain block (number: {}, hash: {:?}, parentchain hash: {:?})", - block_number, - signed_sidechain_block.block().hash(), - signed_sidechain_block.block().block_data().layer_one_head() - ); - - let peeked_parentchain_header = - self.peek_parentchain_header(&sidechain_block, parentchain_header) - .unwrap_or_else(|e| { - warn!("Could not peek parentchain block, returning latest parentchain block ({:?})", e); - parentchain_header.clone() - }); - - let block_import_params = self.verify_import(&shard, |state| { - let verifier = self.verifier(state.get_last_block()); - verifier.verify( - signed_sidechain_block.clone(), - &peeked_parentchain_header, - shard, - self.get_context(), - ) - })?; - - let latest_parentchain_header = - self.import_parentchain_block(&sidechain_block, parentchain_header)?; - - let state_key = self.state_key()?; - - let state_update_start_time = Instant::now(); - self.apply_state_update(&shard, |mut state| { - let encrypted_state_diff = - block_import_params.block().block_data().encrypted_state_diff(); - - info!( - "Applying state diff for block {} of size {} bytes", - block_number, - encrypted_state_diff.len() - ); - - let update = state_update_from_encrypted(encrypted_state_diff, state_key)?; - - state.apply_state_update(&update).map_err(|e| Error::Other(e.into()))?; - - state.set_last_block(block_import_params.block()); - - Ok(state) - })?; - info!( - "Applying state update from block {} took {} ms", - block_number, - state_update_start_time.elapsed().as_millis() - ); - - self.cleanup(&signed_sidechain_block)?; - - // Store block in storage. - self.get_context().store_sidechain_blocks(vec![signed_sidechain_block])?; - - let import_duration = start_time.elapsed(); - info!("Importing block {} took {} ms", block_number, import_duration.as_millis()); - if let Err(e) = self - .get_context() - .update_metric(EnclaveMetric::SidechainBlockImportTime(import_duration)) - { - warn!("Failed to update metric for sidechain block import: {:?}", e); - }; - - Ok(latest_parentchain_header) - } -} - -fn state_update_from_encrypted( - encrypted: &[u8], - key: Key, -) -> Result { - let mut payload: Vec = encrypted.to_vec(); - key.decrypt(&mut payload).map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - Ok(Decode::decode(&mut payload.as_slice())?) -} diff --git a/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs b/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs deleted file mode 100644 index ee2b5f3ede..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/block_import_confirmation_handler.rs +++ /dev/null @@ -1,130 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, Result}; -use itc_parentchain_light_client::{ - concurrent_access::ValidatorAccess, BlockNumberOps, ExtrinsicSender, NumberFor, -}; -use itp_extrinsics_factory::CreateExtrinsics; -use itp_node_api_metadata::{pallet_teebag::TeebagCallIndexes, NodeMetadataTrait}; -use itp_node_api_metadata_provider::AccessNodeMetadata; -use itp_settings::worker::BLOCK_NUMBER_FINALIZATION_DIFF; -use itp_types::{OpaqueCall, ShardIdentifier}; -use its_primitives::traits::Header as HeaderTrait; -use log::*; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::{marker::PhantomData, sync::Arc}; - -/// Trait to confirm a sidechain block import. -pub trait ConfirmBlockImport { - fn confirm_import(&self, header: &SidechainHeader, shard: &ShardIdentifier) -> Result<()>; -} - -/// Creates and sends a sidechain block import confirmation extrsinic to the parentchain. -pub struct BlockImportConfirmationHandler< - ParentchainBlock, - SidechainHeader, - NodeMetadataRepository, - ExtrinsicsFactory, - ValidatorAccessor, -> { - metadata_repository: Arc, - extrinsics_factory: Arc, - validator_accessor: Arc, - _phantom: PhantomData<(ParentchainBlock, SidechainHeader)>, -} - -impl< - ParentchainBlock, - SidechainHeader, - NodeMetadataRepository, - ExtrinsicsFactory, - ValidatorAccessor, - > - BlockImportConfirmationHandler< - ParentchainBlock, - SidechainHeader, - NodeMetadataRepository, - ExtrinsicsFactory, - ValidatorAccessor, - > -{ - pub fn new( - metadata_repository: Arc, - extrinsics_factory: Arc, - validator_accessor: Arc, - ) -> Self { - Self { - metadata_repository, - extrinsics_factory, - validator_accessor, - _phantom: Default::default(), - } - } -} - -impl< - ParentchainBlock, - SidechainHeader, - NodeMetadataRepository, - ExtrinsicsFactory, - ValidatorAccessor, - > ConfirmBlockImport - for BlockImportConfirmationHandler< - ParentchainBlock, - SidechainHeader, - NodeMetadataRepository, - ExtrinsicsFactory, - ValidatorAccessor, - > where - ParentchainBlock: ParentchainBlockTrait, - NumberFor: BlockNumberOps, - SidechainHeader: HeaderTrait, - NodeMetadataRepository: AccessNodeMetadata, - NodeMetadataRepository::MetadataType: NodeMetadataTrait, - ExtrinsicsFactory: CreateExtrinsics, - ValidatorAccessor: ValidatorAccess + Send + Sync + 'static, -{ - fn confirm_import(&self, header: &SidechainHeader, shard: &ShardIdentifier) -> Result<()> { - let call = self - .metadata_repository - .get_from_metadata(|m| m.sidechain_block_imported_call_indexes()) - .map_err(|e| Error::Other(e.into()))? - .map_err(|e| Error::Other(format!("{:?}", e).into()))?; - - if header.block_number() == header.next_finalization_block_number() { - let opaque_call = OpaqueCall::from_tuple(&( - call, - shard, - header.block_number(), - header.next_finalization_block_number() + BLOCK_NUMBER_FINALIZATION_DIFF, - header.hash(), - )); - - let xts = self - .extrinsics_factory - .create_extrinsics(&[opaque_call], None) - .map_err(|e| Error::Other(e.into()))?; - - debug!("Sending sidechain block import confirmation extrinsic.."); - self.validator_accessor - .execute_mut_on_validator(|v| v.send_extrinsics(xts)) - .map_err(|e| Error::Other(e.into()))?; - } - Ok(()) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/block_import_queue_worker.rs b/bitacross-worker/sidechain/consensus/common/src/block_import_queue_worker.rs deleted file mode 100644 index fc7d9a23ef..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/block_import_queue_worker.rs +++ /dev/null @@ -1,120 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{Error, Result, SyncBlockFromPeer}; -use core::marker::PhantomData; -use itp_import_queue::PopFromQueue; -use its_primitives::traits::{Block as BlockTrait, SignedBlock as SignedSidechainBlockTrait}; -use log::debug; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::{sync::Arc, time::Instant}; - -/// Trait to trigger working the sidechain block import queue. -pub trait ProcessBlockImportQueue { - /// Pop sidechain blocks from the import queue and import them until queue is empty. - fn process_queue( - &self, - current_parentchain_header: &ParentchainBlockHeader, - ) -> Result; -} - -pub struct BlockImportQueueWorker< - ParentchainBlock, - SignedSidechainBlock, - BlockImportQueue, - PeerBlockSyncer, -> { - block_import_queue: Arc, - peer_block_syncer: Arc, - _phantom: PhantomData<(ParentchainBlock, SignedSidechainBlock)>, -} - -impl - BlockImportQueueWorker -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, - SignedSidechainBlock::Block: BlockTrait, - BlockImportQueue: PopFromQueue, - PeerBlockSyncer: SyncBlockFromPeer, -{ - pub fn new( - block_import_queue: Arc, - peer_block_syncer: Arc, - ) -> Self { - BlockImportQueueWorker { - block_import_queue, - peer_block_syncer, - _phantom: Default::default(), - } - } - - fn record_timings(start_time: Instant, number_of_imported_blocks: usize) { - let elapsed_time_millis = start_time.elapsed().as_millis(); - let time_millis_per_block = - (elapsed_time_millis as f64 / number_of_imported_blocks as f64).ceil(); - debug!( - "Imported {} blocks in {} ms (average of {} ms per block)", - number_of_imported_blocks, elapsed_time_millis, time_millis_per_block - ); - } -} - -impl - ProcessBlockImportQueue - for BlockImportQueueWorker< - ParentchainBlock, - SignedSidechainBlock, - BlockImportQueue, - PeerBlockSyncer, - > where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, - SignedSidechainBlock::Block: BlockTrait, - BlockImportQueue: PopFromQueue, - PeerBlockSyncer: SyncBlockFromPeer, -{ - fn process_queue( - &self, - current_parentchain_header: &ParentchainBlock::Header, - ) -> Result { - let mut latest_imported_parentchain_header = current_parentchain_header.clone(); - let mut number_of_imported_blocks = 0usize; - let start_time = Instant::now(); - - loop { - match self.block_import_queue.pop_front() { - Ok(maybe_block) => match maybe_block { - Some(block) => { - latest_imported_parentchain_header = self - .peer_block_syncer - .sync_block(block, &latest_imported_parentchain_header)?; - number_of_imported_blocks += 1; - }, - None => { - Self::record_timings(start_time, number_of_imported_blocks); - return Ok(latest_imported_parentchain_header) - }, - }, - Err(e) => { - Self::record_timings(start_time, number_of_imported_blocks); - return Err(Error::FailedToPopBlockImportQueue(e)) - }, - } - } - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/block_production_suspension.rs b/bitacross-worker/sidechain/consensus/common/src/block_production_suspension.rs deleted file mode 100644 index ae664925da..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/block_production_suspension.rs +++ /dev/null @@ -1,112 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Mechanisms to (temporarily) suspend the production of sidechain blocks. - -#[cfg(feature = "sgx")] -use std::sync::SgxRwLock as RwLock; - -#[cfg(feature = "std")] -use std::sync::RwLock; - -use crate::error::{Error, Result}; -use log::*; - -/// Trait to suspend the production of sidechain blocks. -pub trait SuspendBlockProduction { - /// Suspend any sidechain block production. - fn suspend_for_sync(&self) -> Result<()>; - - /// Resume block sidechain block production. - fn resume(&self) -> Result<()>; -} - -/// Trait to query if sidechain block production is suspended. -pub trait IsBlockProductionSuspended { - fn is_suspended(&self) -> Result; - - fn is_sync_ongoing(&self) -> Result; -} - -/// Implementation for suspending and resuming sidechain block production. -#[derive(Default)] -pub struct BlockProductionSuspender { - is_suspended: RwLock, - sync_is_ongoing: RwLock, -} - -impl BlockProductionSuspender { - pub fn new(is_suspended: bool) -> Self { - BlockProductionSuspender { - is_suspended: RwLock::new(is_suspended), - sync_is_ongoing: RwLock::new(false), - } - } -} - -impl SuspendBlockProduction for BlockProductionSuspender { - fn suspend_for_sync(&self) -> Result<()> { - let mut suspended_lock = self.is_suspended.write().map_err(|_| Error::LockPoisoning)?; - *suspended_lock = true; - - let mut sync_is_ongoing_lock = - self.sync_is_ongoing.write().map_err(|_| Error::LockPoisoning)?; - *sync_is_ongoing_lock = true; - - info!("Suspend sidechain block production"); - Ok(()) - } - - fn resume(&self) -> Result<()> { - let mut suspended_lock = self.is_suspended.write().map_err(|_| Error::LockPoisoning)?; - *suspended_lock = false; - info!("Resume sidechain block production"); - Ok(()) - } -} - -impl IsBlockProductionSuspended for BlockProductionSuspender { - fn is_suspended(&self) -> Result { - Ok(*self.is_suspended.read().map_err(|_| Error::LockPoisoning)?) - } - - fn is_sync_ongoing(&self) -> Result { - Ok(*self.sync_is_ongoing.read().map_err(|_| Error::LockPoisoning)?) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn initial_production_is_not_suspended() { - let block_production_suspender = BlockProductionSuspender::default(); - assert!(!block_production_suspender.is_suspended().unwrap()); - } - - #[test] - fn suspending_production_works() { - let block_production_suspender = BlockProductionSuspender::default(); - - block_production_suspender.suspend_for_sync().unwrap(); - assert!(block_production_suspender.is_suspended().unwrap()); - - block_production_suspender.resume().unwrap(); - assert!(!block_production_suspender.is_suspended().unwrap()); - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/error.rs b/bitacross-worker/sidechain/consensus/common/src/error.rs deleted file mode 100644 index f6ba8b6fd0..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/error.rs +++ /dev/null @@ -1,99 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Error types in sidechain consensus - -use itp_types::BlockHash as ParentchainBlockHash; -use its_block_verification::error::Error as VerificationError; -use its_primitives::types::{block::BlockHash as SidechainBlockHash, BlockNumber}; -use sgx_types::sgx_status_t; -use std::{ - boxed::Box, - error, - string::{String, ToString}, - vec::Vec, -}; - -pub type Result = std::result::Result; - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub use thiserror_sgx as thiserror; - -#[derive(Debug, thiserror::Error)] -#[non_exhaustive] -pub enum Error { - #[error("SGX error, status: {0}")] - Sgx(sgx_status_t), - #[error("Unable to create block proposal.")] - CannotPropose, - #[error("Encountered poisoned lock")] - LockPoisoning, - #[error("Message sender {0} is not a valid authority")] - InvalidAuthority(String), - #[error("Could not get authorities: {0:?}.")] - CouldNotGetAuthorities(String), - #[error("Chain lookup failed: {0}")] - ChainLookup(String), - #[error("Failed to sign using key: {0:?}. Reason: {1}")] - CannotSign(Vec, String), - #[error("Bad parentchain block (Hash={0}). Reason: {1}")] - BadParentchainBlock(ParentchainBlockHash, String), - #[error("Bad sidechain block (Hash={0}). Reason: {1}")] - BadSidechainBlock(SidechainBlockHash, String), - #[error("Could not import new block due to {2}. (Last imported by number: {0:?})")] - BlockAncestryMismatch(BlockNumber, SidechainBlockHash, String), - #[error("Could not import new block. Expected first block, but found {0}. {1:?}")] - InvalidFirstBlock(BlockNumber, String), - #[error("Could not import block (number: {0}). A block with this number is already imported (current state block number: {1})")] - BlockAlreadyImported(BlockNumber, BlockNumber), - #[error("Failed to pop from block import queue: {0}")] - FailedToPopBlockImportQueue(#[from] itp_import_queue::error::Error), - #[error("Verification Error: {0}")] - VerificationError(its_block_verification::error::Error), - #[error(transparent)] - Other(#[from] Box), -} - -impl core::convert::From for Error { - fn from(e: std::io::Error) -> Self { - Self::Other(e.into()) - } -} - -impl core::convert::From for Error { - fn from(e: codec::Error) -> Self { - Self::Other(e.to_string().into()) - } -} - -impl From for Error { - fn from(sgx_status: sgx_status_t) -> Self { - Self::Sgx(sgx_status) - } -} - -impl From for Error { - fn from(e: VerificationError) -> Self { - match e { - VerificationError::BlockAncestryMismatch(a, b, c) => - Error::BlockAncestryMismatch(a, b, c), - VerificationError::InvalidFirstBlock(a, b) => Error::InvalidFirstBlock(a, b), - VerificationError::BlockAlreadyImported(a, b) => Error::BlockAlreadyImported(a, b), - _ => Error::VerificationError(e), - } - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/header_db.rs b/bitacross-worker/sidechain/consensus/common/src/header_db.rs deleted file mode 100644 index f15acd5028..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/header_db.rs +++ /dev/null @@ -1,44 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use itp_types::H256; -use its_primitives::traits::Header as HeaderT; -use std::{collections::HashMap, convert::From, hash::Hash as HashT}; - -/// Normally implemented on the `client` in substrate. -/// Is a trait which can offer methods for interfacing with a block Database. -pub trait HeaderDbTrait { - type Header: HeaderT; - /// Retrieves Header for the corresponding block hash. - fn header(&self, hash: &H256) -> Option; -} - -/// A mocked Header Database which allows you to take a Block Hash and Query a Block Header. -pub struct HeaderDb(pub HashMap); - -impl HeaderDbTrait for HeaderDb -where - // TODO: the H256 trait bounds are needed because: #1203 - Hash: PartialEq + HashT + Into + From + core::cmp::Eq + Clone, - Header: HeaderT + Clone, -{ - type Header = Header; - - fn header(&self, hash: &H256) -> Option { - let header = self.0.get(&Hash::from(*hash))?; - Some(header.clone()) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/is_descendant_of_builder.rs b/bitacross-worker/sidechain/consensus/common/src/is_descendant_of_builder.rs deleted file mode 100644 index 5e13c6f69a..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/is_descendant_of_builder.rs +++ /dev/null @@ -1,133 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate::header_db::HeaderDbTrait; -use core::{hash::Hash as HashT, marker::PhantomData}; -use itp_types::H256; -use its_primitives::traits::Header as HeaderT; - -pub struct IsDescendantOfBuilder(PhantomData<(Hash, HeaderDb, Error)>); - -impl<'a, Hash, HeaderDb, Error> IsDescendantOfBuilder -where - Error: From<()>, - Hash: PartialEq + HashT + Default + Into + From + Clone, - HeaderDb: HeaderDbTrait, -{ - /// Builds the `is_descendant_of` closure for the fork-tree - /// used when adding and removing nodes from the tree. - pub fn build_is_descendant_of( - current: Option<(&'a Hash, &'a Hash)>, - header_db: &'a HeaderDb, - ) -> impl Fn(&Hash, &Hash) -> Result + 'a { - move |base, head| { - // If the base is equal to the proposed head, then the head is for sure not a descendant of the base. - if base == head { - return Ok(false) - } - - let mut head = head; - if let Some((current_hash, current_parent_hash)) = current { - // If the current hash is equal to the base, then it will not be a descendant of base. - if current_hash == base { - return Ok(false) - } - - // If the current hash is the head and the parent is the base, then we know that - // this current hash is the descendant of the parent. Otherwise we can set the - // head to the parent and find the lowest common ancestor between `head` - // and `base` in the tree. - if current_hash == head { - if current_parent_hash == base { - return Ok(true) - } else { - head = current_parent_hash; - } - } - } - - let ancestor = - >::find_lowest_common_ancestor( - head, base, header_db, - )?; - Ok(ancestor == *base) - } - } -} - -pub struct LowestCommonAncestorFinder(PhantomData<(Hash, HeaderDb)>); - -impl LowestCommonAncestorFinder -where - Hash: PartialEq + Default + Into + From + Clone, - HeaderDb: HeaderDbTrait, -{ - /// Used by the `build_is_descendant_of` to find the LCA of two nodes in the fork-tree. - fn find_lowest_common_ancestor(a: &Hash, b: &Hash, header_db: &HeaderDb) -> Result { - let header_1 = header_db.header(&a.clone().into()).ok_or(())?; - let header_2 = header_db.header(&b.clone().into()).ok_or(())?; - let mut blocknum_1 = header_1.block_number(); - let mut blocknum_2 = header_2.block_number(); - let mut parent_1 = Hash::from(header_1.parent_hash()); - let mut parent_2 = Hash::from(header_2.parent_hash()); - - if *a == parent_2 { - // Then a is the common ancestor of b and it means it is itself the ancestor - return Ok(parent_2) - } - - if *b == parent_1 { - // Then b is the common ancestor of a and it means it is itself the ancestor - return Ok(parent_1) - } - - while blocknum_1 > blocknum_2 { - // This means block 1 is further down in the tree than block 2 - let new_parent = header_db.header(&parent_1.clone().into()).ok_or(())?; - - if new_parent.block_number() >= blocknum_2 { - blocknum_1 = new_parent.block_number(); - parent_1 = Hash::from(new_parent.parent_hash()); - } else { - break - } - } - - while blocknum_2 > blocknum_1 { - // This means block 2 is further down in the tree than block 1 - let new_parent = header_db.header(&parent_2.clone().into()).ok_or(())?; - - if new_parent.block_number() >= blocknum_1 { - blocknum_2 = new_parent.block_number(); - parent_2 = Hash::from(new_parent.parent_hash()); - } else { - break - } - } - - // At this point will be at equal height - while parent_1 != parent_2 { - // go up on both nodes - let new_header_1 = header_db.header(&parent_1.into()).ok_or(())?; - let new_header_2 = header_db.header(&parent_2.into()).ok_or(())?; - parent_1 = Hash::from(new_header_1.parent_hash()); - parent_2 = Hash::from(new_header_2.parent_hash()); - } - - // Return any Parent node Hash as in worst case scenario it is the root which is shared amongst all - Ok(parent_1) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/lib.rs b/bitacross-worker/sidechain/consensus/common/src/lib.rs deleted file mode 100644 index adb91d9ec8..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/lib.rs +++ /dev/null @@ -1,114 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Common stuff that could be shared across multiple consensus engines - -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(test, feature(assert_matches))] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -use its_primitives::traits::{ShardIdentifierFor, SignedBlock as SignedSidechainBlockTrait}; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::{time::Duration, vec::Vec}; - -mod block_import; -mod block_import_confirmation_handler; -mod block_import_queue_worker; -mod error; -mod header_db; -mod peer_block_sync; - -// The feature flag will be removed once we use the module outside of tests. -#[cfg(test)] -mod is_descendant_of_builder; - -#[cfg(test)] -mod test; - -pub use block_import::*; -pub use block_import_confirmation_handler::*; -pub use block_import_queue_worker::*; -pub use error::*; -use itp_types::parentchain::ParentchainCall; -pub use peer_block_sync::*; - -pub trait Verifier: Send + Sync -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, -{ - /// Contains all the relevant data needed for block import - type BlockImportParams; - - /// Context used to derive slot relevant data - type Context; - - /// Verify the given data and return the `BlockImportParams` if successful - fn verify( - &self, - block: SignedSidechainBlock, - parentchain_header: &ParentchainBlock::Header, - shard: ShardIdentifierFor, - ctx: &Self::Context, - ) -> Result; -} - -/// Environment for a Consensus instance. -/// -/// Creates proposer instance. -pub trait Environment< - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, -> -{ - /// The proposer type this creates. - type Proposer: Proposer + Send; - /// Error which can occur upon creation. - type Error: From + std::fmt::Debug + 'static; - - /// Initialize the proposal logic on top of a specific header. - fn init( - &mut self, - parent_header: ParentchainBlock::Header, - shard: ShardIdentifierFor, - ) -> std::result::Result; -} - -pub trait Proposer< - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, -> -{ - fn propose(&self, max_duration: Duration) -> Result>; -} - -/// A proposal that is created by a [`Proposer`]. -pub struct Proposal { - /// The sidechain block that was build. - pub block: SignedSidechainBlock, - /// Parentchain state transitions triggered by sidechain state transitions. - /// - /// Any sidechain stf that invokes a parentchain stf must not commit its state change - /// before the parentchain effect has been finalized. - pub parentchain_effects: Vec, -} diff --git a/bitacross-worker/sidechain/consensus/common/src/peer_block_sync.rs b/bitacross-worker/sidechain/consensus/common/src/peer_block_sync.rs deleted file mode 100644 index 945c1c014e..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/peer_block_sync.rs +++ /dev/null @@ -1,320 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{BlockImport, ConfirmBlockImport, Error, Result}; -use core::marker::PhantomData; -use itp_ocall_api::{EnclaveMetricsOCallApi, EnclaveSidechainOCallApi}; -use itp_types::H256; -use its_primitives::{ - traits::{ - Block as BlockTrait, Header as HeaderTrait, ShardIdentifierFor, - SignedBlock as SignedSidechainBlockTrait, - }, - types::BlockHash, -}; -use log::*; -use sp_runtime::traits::{Block as ParentchainBlockTrait, Header as ParentchainHeaderTrait}; -use std::{sync::Arc, vec::Vec}; - -/// Trait for syncing sidechain blocks from a peer validateer. -/// -/// This entails importing blocks and detecting if we're out of date with our blocks, in which -/// case we fetch the missing blocks from a peer. -pub trait SyncBlockFromPeer -where - ParentchainHeader: ParentchainHeaderTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, -{ - fn sync_block( - &self, - sidechain_block: SignedSidechainBlock, - last_imported_parentchain_header: &ParentchainHeader, - ) -> Result; -} - -/// Sidechain peer block sync implementation. -pub struct PeerBlockSync< - ParentchainBlock, - SignedSidechainBlock, - BlockImporter, - OCallApi, - ImportConfirmationHandler, -> { - importer: Arc, - ocall_api: Arc, - import_confirmation_handler: Arc, - _phantom: PhantomData<(ParentchainBlock, SignedSidechainBlock)>, -} - -impl< - ParentchainBlock, - SignedSidechainBlock, - BlockImporter, - OCallApi, - ImportConfirmationHandler, - > - PeerBlockSync< - ParentchainBlock, - SignedSidechainBlock, - BlockImporter, - OCallApi, - ImportConfirmationHandler, - > where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, - <::Block as BlockTrait>::HeaderType: - HeaderTrait, - BlockImporter: BlockImport, - OCallApi: EnclaveSidechainOCallApi + EnclaveMetricsOCallApi, - ImportConfirmationHandler: ConfirmBlockImport< - <::Block as BlockTrait>::HeaderType, - >, -{ - pub fn new( - importer: Arc, - sidechain_ocall_api: Arc, - import_confirmation_handler: Arc, - ) -> Self { - PeerBlockSync { - importer, - ocall_api: sidechain_ocall_api, - import_confirmation_handler, - _phantom: Default::default(), - } - } - - fn fetch_and_import_blocks_from_peer( - &self, - last_imported_sidechain_block_hash: BlockHash, - import_until_block_hash: BlockHash, - current_parentchain_header: &ParentchainBlock::Header, - shard_identifier: ShardIdentifierFor, - ) -> Result { - info!( - "Initiating fetch blocks from peer, last imported block hash: {:?}, until block hash: {:?}", - last_imported_sidechain_block_hash, import_until_block_hash - ); - - let blocks_to_import: Vec = - self.ocall_api.fetch_sidechain_blocks_from_peer( - last_imported_sidechain_block_hash, - Some(import_until_block_hash), - shard_identifier, - )?; - - info!("Fetched {} blocks from peer to import", blocks_to_import.len()); - - let mut latest_imported_parentchain_header = current_parentchain_header.clone(); - - for block_to_import in blocks_to_import { - let block_number = block_to_import.block().header().block_number(); - - latest_imported_parentchain_header = match self - .importer - .import_block(block_to_import, &latest_imported_parentchain_header) - { - Err(e) => { - error!("Failed to import sidechain block that was fetched from peer: {:?}", e); - return Err(e) - }, - Ok(h) => { - info!( - "Successfully imported peer fetched sidechain block (number: {})", - block_number - ); - h - }, - }; - } - - Ok(latest_imported_parentchain_header) - } -} - -impl - SyncBlockFromPeer - for PeerBlockSync -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, - <::Block as BlockTrait>::HeaderType: - HeaderTrait, - BlockImporter: BlockImport, - OCallApi: EnclaveSidechainOCallApi + EnclaveMetricsOCallApi, - ImportConfirmationHandler: ConfirmBlockImport<<::Block as BlockTrait>::HeaderType>, -{ - fn sync_block( - &self, - sidechain_block: SignedSidechainBlock, - current_parentchain_header: &ParentchainBlock::Header, - ) -> Result { - let shard_identifier = sidechain_block.block().header().shard_id(); - let sidechain_block_number = sidechain_block.block().header().block_number(); - let sidechain_block_hash = sidechain_block.hash(); - - // Attempt to import the block - in case we encounter an ancestry error, we go into - // peer fetching mode to fetch sidechain blocks from a peer and import those first. - match self.importer.import_block(sidechain_block.clone(), current_parentchain_header) { - Err(e) => match e { - Error::BlockAncestryMismatch(_block_number, block_hash, _) => { - warn!("Got ancestry mismatch error upon block import. Attempting to fetch missing blocks from peer"); - let updated_parentchain_header = self.fetch_and_import_blocks_from_peer( - block_hash, - sidechain_block_hash, - current_parentchain_header, - shard_identifier, - )?; - - self.importer.import_block(sidechain_block, &updated_parentchain_header) - }, - Error::InvalidFirstBlock(block_number, _) => { - warn!("Got invalid first block error upon block import (expected first block, but got block with number {}). \ - Attempting to fetch missing blocks from peer", block_number); - let updated_parentchain_header = self.fetch_and_import_blocks_from_peer( - Default::default(), // This is the parent hash of the first block. So we import everything. - sidechain_block_hash, - current_parentchain_header, - shard_identifier, - )?; - - self.importer.import_block(sidechain_block, &updated_parentchain_header) - }, - Error::BlockAlreadyImported(to_import_block_number, last_known_block_number) => { - warn!("Sidechain block from queue (number: {}) was already imported (current block number: {}). Block will be ignored.", - to_import_block_number, last_known_block_number); - Ok(current_parentchain_header.clone()) - }, - _ => Err(e), - }, - Ok(latest_parentchain_header) => { - info!("Successfully imported broadcast sidechain block (number: {}), based on parentchain block {:?}", - sidechain_block_number, latest_parentchain_header.number()); - - // We confirm the successful block import. Only in this case, not when we're in - // on-boarding and importing blocks that were fetched from a peer. - if let Err(e) = self.import_confirmation_handler.confirm_import(sidechain_block.block().header(), &shard_identifier) { - error!("Failed to confirm sidechain block import: {:?}", e); - } - - Ok(latest_parentchain_header) - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::test::mocks::{ - block_importer_mock::BlockImportMock, confirm_block_import_mock::ConfirmBlockImportMock, - }; - use core::assert_matches::assert_matches; - use itc_parentchain_test::ParentchainHeaderBuilder; - use itp_test::mock::sidechain_ocall_api_mock::SidechainOCallApiMock; - use itp_types::Block as ParentchainBlock; - use its_primitives::types::block::SignedBlock as SignedSidechainBlock; - use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; - - type TestBlockImport = BlockImportMock; - type TestOCallApi = SidechainOCallApiMock; - type TestPeerBlockSync = PeerBlockSync< - ParentchainBlock, - SignedSidechainBlock, - TestBlockImport, - TestOCallApi, - ConfirmBlockImportMock, - >; - - #[test] - fn if_block_import_is_successful_no_peer_fetching_happens() { - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let signed_sidechain_block = SidechainBlockBuilder::default().build_signed(); - - let block_importer_mock = Arc::new( - BlockImportMock::::default() - .with_import_result_once(Ok(parentchain_header.clone())), - ); - - let sidechain_ocall_api = - Arc::new(SidechainOCallApiMock::::default()); - - let peer_syncer = - create_peer_syncer(block_importer_mock.clone(), sidechain_ocall_api.clone()); - - peer_syncer.sync_block(signed_sidechain_block, &parentchain_header).unwrap(); - - assert_eq!(1, block_importer_mock.get_imported_blocks().len()); - assert_eq!(0, sidechain_ocall_api.number_of_fetch_calls()); - } - - #[test] - fn error_is_propagated_if_import_returns_error_other_than_ancestry_mismatch() { - let block_importer_mock = Arc::new( - BlockImportMock::::default() - .with_import_result_once(Err(Error::InvalidAuthority("auth".to_string()))), - ); - - let sidechain_ocall_api = - Arc::new(SidechainOCallApiMock::::default()); - - let peer_syncer = - create_peer_syncer(block_importer_mock.clone(), sidechain_ocall_api.clone()); - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let signed_sidechain_block = SidechainBlockBuilder::default().build_signed(); - - let sync_result = peer_syncer.sync_block(signed_sidechain_block, &parentchain_header); - - assert_matches!(sync_result, Err(Error::InvalidAuthority(_))); - assert_eq!(1, block_importer_mock.get_imported_blocks().len()); - assert_eq!(0, sidechain_ocall_api.number_of_fetch_calls()); - } - - #[test] - fn blocks_are_fetched_from_peer_if_initial_import_yields_ancestry_mismatch() { - let block_importer_mock = - Arc::new(BlockImportMock::::default().with_import_result_once( - Err(Error::BlockAncestryMismatch(1, H256::random(), "".to_string())), - )); - - let sidechain_ocall_api = Arc::new( - SidechainOCallApiMock::::default().with_peer_fetch_blocks(vec![ - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - ]), - ); - - let peer_syncer = - create_peer_syncer(block_importer_mock.clone(), sidechain_ocall_api.clone()); - - let parentchain_header = ParentchainHeaderBuilder::default().build(); - let signed_sidechain_block = SidechainBlockBuilder::default().build_signed(); - - peer_syncer.sync_block(signed_sidechain_block, &parentchain_header).unwrap(); - - assert_eq!(4, block_importer_mock.get_imported_blocks().len()); - assert_eq!(1, sidechain_ocall_api.number_of_fetch_calls()); - } - - fn create_peer_syncer( - block_importer: Arc, - ocall_api: Arc, - ) -> TestPeerBlockSync { - let import_confirmation_handler = Arc::new(ConfirmBlockImportMock {}); - TestPeerBlockSync::new(block_importer, ocall_api, import_confirmation_handler) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_import_queue_worker_mock.rs b/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_import_queue_worker_mock.rs deleted file mode 100644 index fb2b0d8bcc..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_import_queue_worker_mock.rs +++ /dev/null @@ -1,263 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate::{header_db::HeaderDb, is_descendant_of_builder::IsDescendantOfBuilder}; -use core::marker::PhantomData; -use fork_tree::ForkTree; -use itp_types::H256; -use its_primitives::{ - traits::{Block as BlockT, Header as HeaderT}, - types::{header::SidechainHeader as Header, Block}, -}; -use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; -use std::collections::VecDeque; - -#[derive(Default)] -pub struct BlockQueueBuilder { - queue: VecDeque, - _phantom_data: PhantomData, -} - -impl BlockQueueBuilder -where - Builder: SidechainBlockBuilderTrait + Default, - B: BlockT + From, -{ - fn new() -> Self { - Self { queue: VecDeque::new(), _phantom_data: PhantomData::default() } - } - - /// Allows definining a mock queue based and assumes that a genesis block - /// will need to be appended to the queue as the first item. - /// Returns: BuiltQueue - fn build_queue(self, f: impl FnOnce(VecDeque) -> VecDeque) -> VecDeque { - f(self.queue) - } - - fn add_genesis_block_to_queue(self) -> Self { - let mut self_mut = self; - let genesis_header = Header { - block_number: 0, - parent_hash: H256::from_slice(&[0; 32]), - ..Default::default() - }; - let block: B = Builder::default().with_header(genesis_header).build().into(); - self_mut.queue.push_back(block); - self_mut - } -} - -pub trait BlockQueueHeaderBuild { - type QueueHeader; - /// Helper trait to build a Header for a BlockQueue. - fn build_queue_header(block_number: BlockNumber, parent_hash: Hash) -> Self::QueueHeader; -} - -pub struct BlockQueueHeaderBuilder(PhantomData<(BlockNumber, Hash)>); - -impl BlockQueueHeaderBuild - for BlockQueueHeaderBuilder -where - BlockNumber: Into, - Hash: Into, -{ - type QueueHeader = Header; - /// Helper trait to build a Header for a BlockQueue. - fn build_queue_header(block_number: BlockNumber, parent_hash: Hash) -> Self::QueueHeader { - Header { - block_number: block_number.into(), - parent_hash: parent_hash.into(), - block_data_hash: H256::random(), - ..Default::default() - } - } -} - -#[derive(Debug)] -pub enum TestError { - Error, -} - -impl From<()> for TestError { - fn from(_a: ()) -> Self { - TestError::Error - } -} - -impl std::fmt::Display for TestError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "TestError") - } -} - -impl std::error::Error for TestError {} - -#[cfg(test)] -mod tests { - use super::*; - - fn fork_tree_from_header_queue(queue: VecDeque) -> ForkTree - where - B: BlockT, - { - // Store all block_headers in db - let db = HeaderDb::( - queue.iter().map(|block| (block.hash(), *block.header())).collect(), - ); - - // Import into forktree - let is_descendant_of = - , TestError>>::build_is_descendant_of(None, &db); - let mut tree = >::new(); - queue.iter().for_each(|block| { - let _ = tree - .import(block.header().hash(), block.header().block_number(), (), &is_descendant_of) - .unwrap(); - }); - tree - } - - #[test] - fn process_sequential_queue_no_forks() { - // Construct a queue which is sequential with 5 members all with distinct block numbers and parents - let mut queue = >::new() - .add_genesis_block_to_queue() - .build_queue(|mut queue| { - for i in 1..5 { - let parent_header = queue.back().unwrap().header(); - let header = >::build_queue_header( - i, - parent_header.hash(), - ); - queue.push_back(SidechainBlockBuilder::default().with_header(header).build()); - } - queue - }); - - // queue -> [0, 1, 2, 3, 4] - assert_eq!(queue.len(), 5); - - let mut tree = fork_tree_from_header_queue::(queue.clone()); - - // We have a tree which looks like this. H0 is the only root. - // - // H0 - H1 - H2 - H3 - H4 - // - - // We see that the only root of this tree is so far H0 - assert_eq!(tree.roots_hash_and_number(), vec![(&queue.front().unwrap().header.hash(), &0)]); - - // Now finalize H0 and so the new Root should be H1 - tree.finalize_root(&queue.front().unwrap().header.hash()).unwrap(); - let _ = queue.pop_front(); - assert_eq!(tree.roots_hash_and_number(), vec![(&queue.front().unwrap().header.hash(), &1)]); - } - - #[test] - fn process_sequential_queue_with_forks() { - // Construct a queue which is sequential and every odd member has 2 block numbers which are the same - let mut queue = >::new() - .add_genesis_block_to_queue() - .build_queue(|mut queue| { - for i in 1..8 { - let parent_header = queue.back().unwrap().header(); - if i % 2 == 0 && i != 1 { - // 1 is not even want all odds to have 2 of the same block_number - let header = >::build_queue_header( - i, - parent_header.hash(), - ); - queue.push_back( - SidechainBlockBuilder::default().with_header(header).build(), - ); - } else { - // build a Queue with 2 headers which are of the same block_number - let headers = vec![ - >::build_queue_header( - i, - parent_header.hash(), - ), - >::build_queue_header( - i, - parent_header.hash(), - ), - ]; - headers.iter().for_each(|header| { - queue.push_back( - SidechainBlockBuilder::default().with_header(*header).build(), - ); - }); - } - } - queue - }); - - // queue -> [0, 1, 1, 2, 3, 3, 4, 5, 5, 6, 7, 7] - assert_eq!(queue.len(), 12); - - let mut tree = fork_tree_from_header_queue::(queue.clone()); - - // We have a tree which looks like the following - // - (H5, B3).. - // / - // - (H3, B2) - // / \ - // - (H1, B1) - (H4, B3).. - // / - // / - // (H0, B0) - // \ - // \ - // - (H2, B1).. - // - // - - // H0 is the first root - assert_eq!(tree.roots_hash_and_number(), vec![(&queue.front().unwrap().header.hash(), &0)]); - - // Now if we finalize H0 we should see 2 roots H1 and H2 - tree.finalize_root(&queue.front().unwrap().header.hash()).unwrap(); - let _ = queue.pop_front(); - assert_eq!( - tree.roots_hash_and_number(), - vec![(&queue[1].header.hash(), &1), (&queue[0].header.hash(), &1)] - ); - - // If we finalize (H1, B1) then we should see one roots (H3, B2) - let _ = queue.pop_front(); // remove (H1, B1) - tree.finalize_root(&queue.front().unwrap().header.hash()).unwrap(); - let _ = queue.pop_front(); // remove (H2, B1) - assert_eq!(tree.roots_hash_and_number(), vec![(&queue[0].header.hash(), &2)]); - - // If we finalize (H3, B2) we should see two roots (H4, B3), (H5, B3) - tree.finalize_root(&queue.front().unwrap().header.hash()).unwrap(); - let _ = queue.pop_front(); // remove (H3, B2) - assert_eq!( - tree.roots_hash_and_number(), - vec![(&queue[1].header.hash(), &3), (&queue[0].header.hash(), &3)] - ); - } - - #[test] - fn process_non_sequential_queue_without_forks() { - // TODO - } - - #[test] - fn process_non_sequential_queue_with_forks() { - // TODO - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_importer_mock.rs b/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_importer_mock.rs deleted file mode 100644 index 9d6060561f..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mocks/block_importer_mock.rs +++ /dev/null @@ -1,168 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{test::mocks::verifier_mock::VerifierMock, BlockImport, Error, Result}; -use core::marker::PhantomData; -use itp_ocall_api::EnclaveMetricsOCallApi; -use itp_sgx_crypto::aes::Aes; -use itp_sgx_externalities::SgxExternalities; -use itp_test::mock::onchain_mock::OnchainMock; -use itp_types::H256; -use its_primitives::traits::{ShardIdentifierFor, SignedBlock as SignedSidechainBlockTrait}; -use sp_core::Pair; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::{collections::VecDeque, sync::RwLock}; - -/// Block importer mock. -pub struct BlockImportMock -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait::Public> + 'static, -{ - import_result: RwLock>>, - imported_blocks: RwLock>, - _phantom: PhantomData<(ParentchainBlock, SignedSidechainBlock)>, -} - -impl BlockImportMock -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait::Public> + 'static, -{ - pub fn with_import_result_once(self, result: Result) -> Self { - let mut imported_results_lock = self.import_result.write().unwrap(); - imported_results_lock.push_back(result); - std::mem::drop(imported_results_lock); - self - } - - #[allow(unused)] - pub fn with_import_result_sequence( - self, - mut results: VecDeque>, - ) -> Self { - let mut imported_results_lock = self.import_result.write().unwrap(); - imported_results_lock.append(&mut results); - std::mem::drop(imported_results_lock); - self - } - - pub fn get_imported_blocks(&self) -> Vec { - (*self.imported_blocks.read().unwrap()).clone() - } -} - -impl Default - for BlockImportMock -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait::Public> + 'static, -{ - fn default() -> Self { - BlockImportMock { - import_result: RwLock::default(), - imported_blocks: RwLock::default(), - _phantom: Default::default(), - } - } -} - -impl BlockImport - for BlockImportMock -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait::Public> + 'static, -{ - type Verifier = - VerifierMock; - type SidechainState = SgxExternalities; - type StateCrypto = Aes; - type Context = OnchainMock; - - fn verifier( - &self, - _maybe_last_sidechain_block: Option, - ) -> Self::Verifier { - todo!() - } - - fn apply_state_update( - &self, - _shard: &ShardIdentifierFor, - _mutating_function: F, - ) -> Result<()> - where - F: FnOnce(Self::SidechainState) -> Result, - { - todo!() - } - - fn verify_import( - &self, - _shard: &ShardIdentifierFor, - _verifying_function: F, - ) -> core::result::Result - where - F: FnOnce(&Self::SidechainState) -> core::result::Result, - { - todo!() - } - - fn state_key(&self) -> Result { - todo!() - } - - fn get_context(&self) -> &Self::Context { - todo!() - } - - fn import_parentchain_block( - &self, - _sidechain_block: &SignedSidechainBlock::Block, - _last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> Result { - todo!() - } - - fn peek_parentchain_header( - &self, - _sidechain_block: &SignedSidechainBlock::Block, - _last_imported_parentchain_header: &ParentchainBlock::Header, - ) -> core::result::Result { - todo!() - } - - fn cleanup(&self, _signed_sidechain_block: &SignedSidechainBlock) -> Result<()> { - todo!() - } - - fn import_block( - &self, - signed_sidechain_block: SignedSidechainBlock, - parentchain_header: &ParentchainBlock::Header, - ) -> Result { - let mut imported_blocks_lock = self.imported_blocks.write().unwrap(); - imported_blocks_lock.push(signed_sidechain_block); - - let mut imported_results_lock = self.import_result.write().unwrap(); - imported_results_lock.pop_front().unwrap_or(Ok(parentchain_header.clone())) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mocks/confirm_block_import_mock.rs b/bitacross-worker/sidechain/consensus/common/src/test/mocks/confirm_block_import_mock.rs deleted file mode 100644 index a810da2f3b..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mocks/confirm_block_import_mock.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, ConfirmBlockImport}; -use itp_types::ShardIdentifier; -use its_primitives::types::header::SidechainHeader; - -/// Mock implementation of the `ConfirmBlockImport` trait. -pub struct ConfirmBlockImportMock; - -impl ConfirmBlockImport for ConfirmBlockImportMock { - fn confirm_import(&self, _header: &SidechainHeader, _shard: &ShardIdentifier) -> Result<()> { - Ok(()) - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mocks/mod.rs b/bitacross-worker/sidechain/consensus/common/src/test/mocks/mod.rs deleted file mode 100644 index 1408ce9402..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mocks/mod.rs +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod block_import_queue_worker_mock; -pub mod block_importer_mock; -pub mod confirm_block_import_mock; -pub mod verifier_mock; diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mocks/verifier_mock.rs b/bitacross-worker/sidechain/consensus/common/src/test/mocks/verifier_mock.rs deleted file mode 100644 index e6d8cbeb0e..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mocks/verifier_mock.rs +++ /dev/null @@ -1,62 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{Result, ShardIdentifierFor, Verifier}; -use itp_types::H256; -use its_primitives::traits::SignedBlock as SignedSidechainBlockTrait; -use sp_core::Pair; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::marker::PhantomData; - -/// Verifier mock implementation. -pub struct VerifierMock< - ParentchainBlock, - SignedSidechainBlock, - BlockImportParameters, - VerifierContext, -> { - _phantom: PhantomData<( - ParentchainBlock, - SignedSidechainBlock, - BlockImportParameters, - VerifierContext, - )>, -} - -impl - Verifier - for VerifierMock -where - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: - SignedSidechainBlockTrait::Public> + 'static, - BlockImportParameters: Send + Sync, - VerifierContext: Send + Sync, -{ - type BlockImportParams = BlockImportParameters; - type Context = VerifierContext; - - fn verify( - &self, - _block: SignedSidechainBlock, - _parentchain_header: &ParentchainBlock::Header, - _shard: ShardIdentifierFor, - _ctx: &Self::Context, - ) -> Result { - todo!() - } -} diff --git a/bitacross-worker/sidechain/consensus/common/src/test/mod.rs b/bitacross-worker/sidechain/consensus/common/src/test/mod.rs deleted file mode 100644 index 43e6cb274d..0000000000 --- a/bitacross-worker/sidechain/consensus/common/src/test/mod.rs +++ /dev/null @@ -1,18 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod mocks; diff --git a/bitacross-worker/sidechain/consensus/slots/Cargo.toml b/bitacross-worker/sidechain/consensus/slots/Cargo.toml deleted file mode 100644 index 41070d0af0..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/Cargo.toml +++ /dev/null @@ -1,78 +0,0 @@ -[package] -name = "its-consensus-slots" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -derive_more = "0.99.16" -lazy_static = { version = "1.1.0", features = ["spin_no_std"] } -log = { version = "0.4", default-features = false } - -# local deps -itp-types = { path = "../../../core-primitives/types", default-features = false } -its-block-verification = { path = "../../block-verification", default-features = false } -its-primitives = { path = "../../primitives", default-features = false } - -# only for slot-stream -futures-timer = { version = "3.0", optional = true } - -# sgx deps -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true, features = ["untrusted_time"] } - -# substrate deps -sp-consensus-slots = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# local deps -itp-settings = { path = "../../../core-primitives/settings" } -itp-time-utils = { path = "../../../core-primitives/time-utils", default-features = false } -its-consensus-common = { path = "../common", default-features = false } - -# litentry -hex = { version = "0.4", default-features = false } -itp-sgx-externalities = { path = "../../../core-primitives/substrate-sgx/externalities", default-features = false } -itp-stf-state-handler = { path = "../../../core-primitives/stf-state-handler", default-features = false } -its-state = { path = "../../state", default-features = false } -lc-scheduled-enclave = { path = "../../../litentry/core/scheduled-enclave", default-features = false } - - -[dev-dependencies] -itc-parentchain-test = { path = "../../../core/parentchain/test" } -its-test = { path = "../../test" } -sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -itp-test = { path = "../../../core-primitives/test" } -tokio = { version = "1.6.1", features = ["full"] } - -[features] -default = ["std"] -std = [ - "codec/std", - "log/std", - # only for slot-stream - "futures-timer", - # substrate - "sp-consensus-slots/std", - "sp-runtime/std", - # local - "itp-time-utils/std", - "itp-types/std", - "its-primitives/std", - "its-block-verification/std", - "its-consensus-common/std", - "itp-stf-state-handler/std", - "itp-sgx-externalities/std", - "its-state/std", - "lc-scheduled-enclave/std", -] -sgx = [ - "itp-time-utils/sgx", - "its-consensus-common/sgx", - "sgx_tstd", - "itp-stf-state-handler/sgx", - "itp-sgx-externalities/sgx", - "its-state/sgx", - "lc-scheduled-enclave/sgx", -] diff --git a/bitacross-worker/sidechain/consensus/slots/src/lib.rs b/bitacross-worker/sidechain/consensus/slots/src/lib.rs deleted file mode 100644 index 370abc7702..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/src/lib.rs +++ /dev/null @@ -1,575 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Slots functionality for the integritee-sidechain. -//! -//! Some consensus algorithms have a concept of *slots*, which are intervals in -//! time during which certain events can and/or must occur. This crate -//! provides generic functionality for slots. - -#![cfg_attr(not(feature = "std"), no_std)] -#![cfg_attr(test, feature(assert_matches))] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -use codec::Encode; -use core::str::FromStr; -use derive_more::From; -use itp_sgx_externalities::SgxExternalities; -use itp_stf_state_handler::handle_state::HandleState; -use itp_time_utils::{duration_difference, duration_now}; - -use its_consensus_common::{Error as ConsensusError, Proposer}; -use its_primitives::traits::{ - Block as SidechainBlockTrait, Header as HeaderTrait, ShardIdentifierFor, - SignedBlock as SignedSidechainBlockTrait, -}; -use its_state::SidechainSystemExt; -use lc_scheduled_enclave::ScheduledEnclaveUpdater; -use log::*; -pub use slots::*; -use sp_runtime::traits::{Block as ParentchainBlockTrait, Header as ParentchainHeaderTrait}; -use std::{fmt::Debug, sync::Arc, time::Duration, vec::Vec}; - -#[cfg(feature = "std")] -mod slot_stream; -mod slots; - -#[cfg(feature = "sgx")] -use std::sync::SgxRwLock as RwLock; - -#[cfg(feature = "std")] -use std::sync::RwLock; - -#[cfg(test)] -mod mocks; - -#[cfg(test)] -mod per_shard_slot_worker_tests; - -use itp_types::parentchain::ParentchainCall; -#[cfg(feature = "std")] -pub use slot_stream::*; -pub use slots::*; - -/// The result of [`SlotWorker::on_slot`]. -#[derive(Debug, Clone, Encode, From)] -pub struct SlotResult { - /// The result of a slot operation. - pub block: SignedSidechainBlock, - /// Parentchain state transitions triggered by sidechain state transitions. - /// - /// Any sidechain stf that invokes a parentchain stf must not commit its state change - /// before the parentchain effect has been finalized. - pub parentchain_effects: Vec, -} - -pub struct FailSlotOnDemand { - // we need to keep a internal counter because node's slot number is a function of slot_beginning_timestamp and SLOT_DURATION - current_slot: RwLock, - fail_at_slot: u64, - mode: FailSlotMode, -} - -impl FailSlotOnDemand { - pub fn new(fail_at_slot: u64, mode: FailSlotMode) -> Self { - Self { current_slot: Default::default(), fail_at_slot, mode } - } - - pub fn next_slot(&self) { - let mut current_slot_lock = self.current_slot.write().unwrap(); - *current_slot_lock += 1; - } - - pub fn check_before_on_slot(&self) -> bool { - let current_slot = self.current_slot.read().unwrap(); - *current_slot == self.fail_at_slot && matches!(&self.mode, FailSlotMode::BeforeOnSlot) - } - - pub fn check_after_on_slot(&self) -> bool { - let current_slot = self.current_slot.read().unwrap(); - *current_slot == self.fail_at_slot && matches!(&self.mode, FailSlotMode::AfterOnSlot) - } -} - -#[derive(Clone, Debug, PartialEq)] -pub enum FailSlotMode { - BeforeOnSlot, - AfterOnSlot, -} - -impl FromStr for FailSlotMode { - type Err = &'static str; - - fn from_str(s: &str) -> Result { - match s { - "BeforeOnSlot" => Ok(FailSlotMode::BeforeOnSlot), - "AfterOnSlot" => Ok(FailSlotMode::AfterOnSlot), - _ => Err("no match"), - } - } -} - -/// A worker that should be invoked at every new slot for a specific shard. -/// -/// The implementation should not make any assumptions of the slot being bound to the time or -/// similar. The only valid assumption is that the slot number is always increasing. -pub trait SlotWorker { - /// Output generated after a slot - type Output: SignedSidechainBlockTrait + Send + 'static; - - /// Called when a new slot is triggered. - /// - /// Returns a [`SlotResult`] iff a block was successfully built in - /// the slot. Otherwise `None` is returned. - fn on_slot( - &mut self, - slot_info: SlotInfo, - shard: ShardIdentifierFor, - is_single_worker: bool, - ) -> Option>; -} - -/// A slot worker scheduler that should be invoked at every new slot. -/// -/// It manages the timeslots of individual per shard `SlotWorker`s. It gives each shard an equal -/// amount of time to produce it's result, equally distributing leftover time from a previous shard's -/// slot share to all subsequent slots. -pub trait PerShardSlotWorkerScheduler { - /// Output generated after a slot - type Output: Send + 'static; - - /// The shard type 'PerShardWorker's operate on. - type ShardIdentifier: Send + 'static + Debug + Clone; - - /// Called when a new slot is triggered. - /// - /// Returns a [`SlotResult`] iff a block was successfully built in - /// the slot. Otherwise `None` is returned. - fn on_slot( - &mut self, - slot_info: SlotInfo, - shard: Vec, - is_single_worker: bool, - ) -> Self::Output; -} - -/// A skeleton implementation for `SlotWorker` which tries to claim a slot at -/// its beginning and tries to produce a block if successfully claimed, timing -/// out if block production takes too long. -pub trait SimpleSlotWorker { - /// The type of proposer to use to build blocks. - type Proposer: Proposer; - - /// Data associated with a slot claim. - type Claim: Send + 'static; - - /// Epoch data necessary for authoring. - type EpochData: Send + 'static; - - /// Output generated after a slot - type Output: SignedSidechainBlockTrait + Send + 'static; - - /// Scheduled enclave context for authoring - type ScheduledEnclave: ScheduledEnclaveUpdater; - - /// State handler context for authoring - type StateHandler: HandleState; - - /// Get scheduled enclave - fn get_scheduled_enclave(&mut self) -> Arc; - - /// Get state handler for query and mutation - fn get_state_handler(&mut self) -> Arc; - - /// Returns the epoch data necessary for authoring. For time-dependent epochs, - /// use the provided slot number as a canonical source of time. - fn epoch_data( - &self, - header: &ParentchainBlock::Header, - slot: Slot, - ) -> Result; - - /// Returns the number of authorities given the epoch data. - /// None indicate that the authorities information is incomplete. - fn authorities_len(&self, epoch_data: &Self::EpochData) -> Option; - - /// Tries to claim the given slot, returning an object with claim data if successful. - fn claim_slot( - &self, - header: &ParentchainBlock::Header, - slot: Slot, - epoch_data: &Self::EpochData, - ) -> Option; - - /// Creates the proposer for the current slot - fn proposer( - &mut self, - header: ParentchainBlock::Header, - shard: ShardIdentifierFor, - ) -> Result; - - /// Remaining duration for proposing. - fn proposing_remaining_duration(&self, slot_info: &SlotInfo) -> Duration; - - /// Trigger the import of the given parentchain block. - /// - /// Returns the header of the latest imported block. In case no block was imported with this trigger, - /// None is returned. - fn import_integritee_parentchain_blocks_until( - &self, - last_imported_parentchain_header: &::Hash, - ) -> Result, ConsensusError>; - - fn import_target_a_parentchain_blocks_until( - &self, - last_imported_parentchain_header: &::Hash, - ) -> Result, ConsensusError>; - - fn import_target_b_parentchain_blocks_until( - &self, - last_imported_parentchain_header: &::Hash, - ) -> Result, ConsensusError>; - - /// Peek the parentchain import queue for the latest block in queue. - /// Does not perform the import or mutate the queue. - fn peek_latest_integritee_parentchain_header( - &self, - ) -> Result, ConsensusError>; - - fn peek_latest_target_a_parentchain_header( - &self, - ) -> Result, ConsensusError>; - - fn peek_latest_target_b_parentchain_header( - &self, - ) -> Result, ConsensusError>; - - /// Implements [`SlotWorker::on_slot`]. This is an adaption from - /// substrate's sc-consensus-slots implementation. There, the slot worker handles all the - /// scheduling itself. Unfortunately, we can't use the same principle in the enclave due to some - /// futures-primitives not being available in sgx, e.g. `Delay` in our case. Hence, before - /// reimplementing the those things ourselves, we take a simplified approach and simply call - /// this function from the outside at each slot. - fn on_slot( - &mut self, - slot_info: SlotInfo, - shard: ShardIdentifierFor, - is_single_worker: bool, - ) -> Option> { - let (_timestamp, slot) = (slot_info.timestamp, slot_info.slot); - let remaining_duration = self.proposing_remaining_duration(&slot_info); - - if remaining_duration == Duration::default() { - debug!("Skipping proposal slot {} since there's no time left to propose", *slot,); - - return None - } - - let latest_integritee_parentchain_header = - match self.peek_latest_integritee_parentchain_header() { - Ok(Some(peeked_header)) => peeked_header, - Ok(None) => slot_info.last_imported_integritee_parentchain_head.clone(), - Err(e) => { - warn!("Failed to peek latest Integritee parentchain block header: {:?}", e); - return None - }, - }; - trace!( - "on_slot: a priori latest Integritee block number: {:?}", - latest_integritee_parentchain_header.number() - ); - // fixme: we need proper error handling here. we just assume there is no target_a if there is an error here, which is very brittle - let maybe_latest_target_a_parentchain_header = - match self.peek_latest_target_a_parentchain_header() { - Ok(Some(peeked_header)) => Some(peeked_header), - Ok(None) => slot_info.maybe_last_imported_target_a_parentchain_head.clone(), - Err(e) => { - debug!("Failed to peek latest target_a_parentchain block header: {:?}", e); - None - }, - }; - trace!( - "on_slot: a priori latest TargetA block number: {:?}", - maybe_latest_target_a_parentchain_header.clone().map(|h| *h.number()) - ); - - let maybe_latest_target_b_parentchain_header = - match self.peek_latest_target_b_parentchain_header() { - Ok(Some(peeked_header)) => Some(peeked_header), - Ok(None) => slot_info.maybe_last_imported_target_b_parentchain_head.clone(), - Err(e) => { - debug!("Failed to peek latest target_a_parentchain block header: {:?}", e); - None - }, - }; - trace!( - "on_slot: a priori latest TargetB block number: {:?}", - maybe_latest_target_b_parentchain_header.clone().map(|h| *h.number()) - ); - - let epoch_data = match self.epoch_data(&latest_integritee_parentchain_header, slot) { - Ok(epoch_data) => epoch_data, - Err(e) => { - warn!( - "Unable to fetch epoch data at block {:?}: {:?}", - latest_integritee_parentchain_header.hash(), - e, - ); - - return None - }, - }; - - let authorities_len = self.authorities_len(&epoch_data); - - if !authorities_len.map(|a| a > 0).unwrap_or(false) { - debug!("Skipping proposal slot. Authorities len {:?}", authorities_len); - } - - // Return early if MRENCLAVE doesn't match - it implies that the enclave should be updated - let scheduled_enclave = self.get_scheduled_enclave(); - let state_handler = self.get_state_handler(); - // TODO: is this always consistent? Reference: `propose_state_update` in slot_proposer.rs - let (state, _) = state_handler.load_cloned(&shard.into()).ok()?; - let next_sidechain_number = state.get_block_number().map_or(1, |n| n + 1); - - if !scheduled_enclave.is_mrenclave_matching(next_sidechain_number) { - warn!( - "Skipping sidechain block {} due to mismatch MRENCLAVE, current: {:?}, expect: {:?}", - next_sidechain_number, - scheduled_enclave.get_current_mrenclave().map(hex::encode), - scheduled_enclave.get_expected_mrenclave(next_sidechain_number).map(hex::encode), - ); - if let Ok(false) = scheduled_enclave.is_block_production_paused() { - let _ = scheduled_enclave.set_block_production_paused(true); - info!("Pause sidechain block production"); - } - return None - } else { - // TODO: this block production pause/unpause is not strictly needed but I add it here as placeholder. - // Maybe we should add a field to describe the reason for pausing/unpausing, as - // it's possible that we want to manually/focibly pause the sidechain - if let Ok(true) = scheduled_enclave.is_block_production_paused() { - info!("Resume sidechain block production"); - let _ = scheduled_enclave.set_block_production_paused(false); - } - } - - // TODO: about the shard migration and state migration - // - the shard migration(copy-over) is done manually by the subcommand "migrate-shard". - // - the state migration is done via conditionally calling on_runtime_upgrade() by comparing - // the current runtime version and LastRuntimeUpgrade, see `stf_sgx.rs`. - // It means we need to bump the runtime version for the new enclave if we want the state - // migration to be executed. - - let _claim = self.claim_slot(&latest_integritee_parentchain_header, slot, &epoch_data)?; - - // Import the peeked parentchain header(s). - let last_imported_integritee_header = match self.import_integritee_parentchain_blocks_until( - &latest_integritee_parentchain_header.hash(), - ) { - Ok(h) => h, - Err(e) => { - debug!( - "Failed to import Integritee blocks until nr{:?}: {:?}", - latest_integritee_parentchain_header.number(), - e - ); - None - }, - }; - trace!( - "on_slot: a posteriori latest Integritee block number: {:?}", - last_imported_integritee_header.clone().map(|h| *h.number()) - ); - - let maybe_last_imported_target_a_header = - if let Some(ref header) = maybe_latest_target_a_parentchain_header { - match self.import_target_a_parentchain_blocks_until(&header.hash()) { - Ok(Some(h)) => Some(h), - Ok(None) => None, - Err(e) => { - debug!( - "Failed to import TargetA blocks until nr{:?}: {:?}", - header.number(), - e - ); - None - }, - } - } else { - None - }; - trace!( - "on_slot: a posteriori latest TargetA block number: {:?}", - maybe_last_imported_target_a_header.map(|h| *h.number()) - ); - - let maybe_last_imported_target_b_header = - if let Some(ref header) = maybe_latest_target_b_parentchain_header { - match self.import_target_b_parentchain_blocks_until(&header.hash()) { - Ok(Some(h)) => Some(h), - Ok(None) => None, - Err(e) => { - debug!( - "Failed to import TargetB blocks until nr{:?}: {:?}", - header.number(), - e - ); - None - }, - } - } else { - None - }; - - trace!( - "on_slot: a posteriori latest TargetB block number: {:?}", - maybe_last_imported_target_b_header.map(|h| *h.number()) - ); - - let proposer = match self.proposer(latest_integritee_parentchain_header.clone(), shard) { - Ok(p) => p, - Err(e) => { - warn!("Could not create proposer: {:?}", e); - return None - }, - }; - - let proposing = match proposer.propose(remaining_duration) { - Ok(p) => p, - Err(e) => { - warn!("Could not propose: {:?}", e); - return None - }, - }; - - if is_single_worker { - warn!("Running as single worker, skipping timestamp within slot check") - } else if !timestamp_within_slot(&slot_info, &proposing.block) { - warn!( - "⌛️ Discarding proposal for slot {}, block number {}; block production took too long", - *slot, proposing.block.block().header().block_number(), - ); - - return None - } - - if last_imported_integritee_header.is_some() { - println!( - "Syncing Parentchains: Integritee: {:?} TargetA: {:?}, TargetB: {:?}, Sidechain: {:?}", - latest_integritee_parentchain_header.number(), - maybe_latest_target_a_parentchain_header.map(|h| *h.number()), - maybe_latest_target_b_parentchain_header.map(|h| *h.number()), - proposing.block.block().header().block_number() - ); - } - - info!("Proposing sidechain block (number: {}, hash: {}) based on integritee parentchain block (number: {:?}, hash: {:?})", - proposing.block.block().header().block_number(), proposing.block.hash(), - latest_integritee_parentchain_header.number(), latest_integritee_parentchain_header.hash() - ); - - Some(SlotResult { - block: proposing.block, - parentchain_effects: proposing.parentchain_effects, - }) - } -} - -impl + Send> - SlotWorker for T -{ - type Output = T::Output; - - fn on_slot( - &mut self, - slot_info: SlotInfo, - shard: ShardIdentifierFor, - is_single_worker: bool, - ) -> Option> { - SimpleSlotWorker::on_slot(self, slot_info, shard, is_single_worker) - } -} - -impl> - PerShardSlotWorkerScheduler for T -{ - type Output = Vec>; - - type ShardIdentifier = ShardIdentifierFor; - - fn on_slot( - &mut self, - slot_info: SlotInfo, - shards: Vec, - is_single_worker: bool, - ) -> Self::Output { - let mut remaining_shards = shards.len(); - let mut slot_results = Vec::with_capacity(remaining_shards); - - for shard in shards.into_iter() { - let now = duration_now(); // It's important we have a common `now` for all following computations. - let shard_remaining_duration = duration_difference(now, slot_info.ends_at) - .and_then(|time| time.checked_div(remaining_shards as u32)) - .unwrap_or_default(); - - // important to check against millis here. We had the corner-case in production - // setup where `shard_remaining_duration` contained only nanos. - if shard_remaining_duration.as_millis() == u128::default() { - info!("⌛️ Could not produce blocks for all shards; block production took too long",); - - return slot_results - } - - let shard_slot_ends_at = now + shard_remaining_duration; - let shard_slot = SlotInfo::new( - slot_info.slot, - now, - shard_remaining_duration, - shard_slot_ends_at, - slot_info.last_imported_integritee_parentchain_head.clone(), - slot_info.maybe_last_imported_target_a_parentchain_head.clone(), - slot_info.maybe_last_imported_target_b_parentchain_head.clone(), - ); - - match SimpleSlotWorker::on_slot(self, shard_slot.clone(), shard, is_single_worker) { - Some(res) => { - slot_results.push(res); - debug!( - "on_slot: produced block for slot: {:?} in shard {:?}", - shard_slot, shard - ) - }, - None => info!( - "Did not propose a block for slot {} in shard {:?}", - *slot_info.slot, shard - ), - } - - remaining_shards -= 1; - } - - slot_results - } -} diff --git a/bitacross-worker/sidechain/consensus/slots/src/mocks.rs b/bitacross-worker/sidechain/consensus/slots/src/mocks.rs deleted file mode 100644 index 72d6f9fee8..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/src/mocks.rs +++ /dev/null @@ -1,149 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{slots::Slot, SimpleSlotWorker, SlotInfo, SlotResult}; -pub use itp_test::mock::handle_state_mock::HandleStateMock; -use its_consensus_common::{Proposal, Proposer, Result}; -use its_primitives::{traits::ShardIdentifierFor, types::SignedBlock as SignedSidechainBlock}; -use lc_scheduled_enclave::ScheduledEnclaveMock; -use sp_runtime::traits::{Block as ParentchainBlockTrait, Header as ParentchainHeaderTrait}; -use std::{marker::PhantomData, sync::Arc, thread, time::Duration}; - -#[derive(Default)] -pub(crate) struct ProposerMock { - _phantom: PhantomData, -} - -impl Proposer for ProposerMock -where - B: ParentchainBlockTrait, -{ - fn propose(&self, _max_duration: Duration) -> Result> { - todo!() - } -} - -#[derive(Default)] -pub(crate) struct SimpleSlotWorkerMock -where - B: ParentchainBlockTrait, -{ - pub slot_infos: Vec>, - pub slot_time_spent: Option, -} - -impl SimpleSlotWorker for SimpleSlotWorkerMock -where - B: ParentchainBlockTrait, -{ - type Proposer = ProposerMock; - - type Claim = u64; - - type EpochData = u64; - - type Output = SignedSidechainBlock; - - type ScheduledEnclave = ScheduledEnclaveMock; - - type StateHandler = HandleStateMock; - - fn get_scheduled_enclave(&mut self) -> Arc { - todo!() - } - - fn get_state_handler(&mut self) -> Arc { - todo!() - } - - fn epoch_data(&self, _header: &B::Header, _slot: Slot) -> Result { - todo!() - } - - fn authorities_len(&self, _epoch_data: &Self::EpochData) -> Option { - todo!() - } - - fn claim_slot( - &self, - _header: &B::Header, - _slot: Slot, - _epoch_data: &Self::EpochData, - ) -> Option { - todo!() - } - - fn proposer( - &mut self, - _header: B::Header, - _shard: ShardIdentifierFor, - ) -> Result { - todo!() - } - - fn proposing_remaining_duration(&self, _slot_info: &SlotInfo) -> Duration { - todo!() - } - - fn import_integritee_parentchain_blocks_until( - &self, - _last_imported_parentchain_header: &::Hash, - ) -> Result> { - todo!() - } - - fn peek_latest_integritee_parentchain_header(&self) -> Result> { - todo!() - } - - fn import_target_a_parentchain_blocks_until( - &self, - _last_imported_parentchain_header: &::Hash, - ) -> Result> { - todo!() - } - - fn peek_latest_target_a_parentchain_header(&self) -> Result> { - todo!() - } - - fn import_target_b_parentchain_blocks_until( - &self, - _last_imported_parentchain_header: &::Hash, - ) -> Result> { - todo!() - } - - fn peek_latest_target_b_parentchain_header(&self) -> Result> { - todo!() - } - - fn on_slot( - &mut self, - slot_info: SlotInfo, - _shard: ShardIdentifierFor, - _is_single_worker: bool, - ) -> Option> { - self.slot_infos.push(slot_info); - - if let Some(sleep_duration) = self.slot_time_spent { - thread::sleep(sleep_duration); - } - - None - } -} diff --git a/bitacross-worker/sidechain/consensus/slots/src/per_shard_slot_worker_tests.rs b/bitacross-worker/sidechain/consensus/slots/src/per_shard_slot_worker_tests.rs deleted file mode 100644 index b9bcaf92f0..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/src/per_shard_slot_worker_tests.rs +++ /dev/null @@ -1,100 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{mocks::SimpleSlotWorkerMock, PerShardSlotWorkerScheduler, SlotInfo}; -use itc_parentchain_test::ParentchainHeaderBuilder; -use itp_settings::sidechain::SLOT_DURATION; -use itp_time_utils::duration_now; -use itp_types::{Block as ParentchainBlock, ShardIdentifier}; -use its_block_verification::slot::slot_from_timestamp_and_duration; - -type TestSlotWorker = SimpleSlotWorkerMock; - -#[test] -fn slot_timings_are_correct_with_multiple_shards() { - let slot_info = slot_info_from_now(); - let mut slot_worker = - TestSlotWorker { slot_infos: Vec::new(), slot_time_spent: Some(SLOT_DURATION / 10) }; - - let shards = - vec![ShardIdentifier::default(), ShardIdentifier::default(), ShardIdentifier::default()]; - - let _slot_results = PerShardSlotWorkerScheduler::on_slot( - &mut slot_worker, - slot_info.clone(), - shards.clone(), - false, - ); - - assert_eq!(slot_worker.slot_infos.len(), shards.len()); - - // end-time of the first shard slot should not exceed timestamp + 1/(n_shards) of the total slot duration - let first_shard_slot_end_time = slot_worker.slot_infos.first().unwrap().ends_at.as_millis(); - let expected_upper_bound = (slot_info.timestamp.as_millis() - + SLOT_DURATION.as_millis().checked_div(shards.len() as u128).unwrap()) - + 2u128; - assert!( - first_shard_slot_end_time <= expected_upper_bound, - "First shard end time, expected: {}, actual: {}", - expected_upper_bound, - first_shard_slot_end_time - ); - - // none of the shard slot end times should exceed the global slot end time - for shard_slot_info in slot_worker.slot_infos { - assert!( - shard_slot_info.ends_at.as_millis() <= slot_info.ends_at.as_millis(), - "shard slot info ends at: {} ms, total slot info ends at: {} ms", - shard_slot_info.ends_at.as_millis(), - slot_info.ends_at.as_millis() - ); - } -} - -#[test] -fn if_shard_takes_up_all_slot_time_subsequent_shards_are_not_served() { - let slot_info = slot_info_from_now(); - let mut slot_worker = - TestSlotWorker { slot_infos: Vec::new(), slot_time_spent: Some(SLOT_DURATION) }; - - let shards = - vec![ShardIdentifier::default(), ShardIdentifier::default(), ShardIdentifier::default()]; - - let _slot_results = PerShardSlotWorkerScheduler::on_slot( - &mut slot_worker, - slot_info.clone(), - shards.clone(), - false, - ); - - assert_eq!(1, slot_worker.slot_infos.len()); -} - -fn slot_info_from_now() -> SlotInfo { - let timestamp_now = duration_now(); - let slot = slot_from_timestamp_and_duration(timestamp_now, SLOT_DURATION); - let slot_ends_at = timestamp_now + SLOT_DURATION; - SlotInfo::new( - slot, - timestamp_now, - SLOT_DURATION, - slot_ends_at, - ParentchainHeaderBuilder::default().build(), - None, - None, - ) -} diff --git a/bitacross-worker/sidechain/consensus/slots/src/slot_stream.rs b/bitacross-worker/sidechain/consensus/slots/src/slot_stream.rs deleted file mode 100644 index 1c738419bf..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/src/slot_stream.rs +++ /dev/null @@ -1,116 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Slots functionality for Substrate. -//! -//! Some consensus algorithms have a concept of *slots*, which are intervals in -//! time during which certain events can and/or must occur. This crate -//! provides generic functionality for slots. - -use crate::time_until_next_slot; -use futures_timer::Delay; -use std::time::Duration; - -/// Executes given `task` repeatedly when the next slot becomes available. -pub async fn start_slot_worker(task: F, slot_duration: Duration) -where - F: Fn(), -{ - let mut slot_stream = SlotStream::new(slot_duration); - - loop { - slot_stream.next_slot().await; - task(); - } -} - -/// Stream to calculate the slot schedule with. -pub struct SlotStream { - slot_duration: Duration, - inner_delay: Option, -} - -impl SlotStream { - pub fn new(slot_duration: Duration) -> Self { - SlotStream { slot_duration, inner_delay: None } - } -} - -impl SlotStream { - /// Waits for the duration of `inner_delay`. - /// Upon timeout, `inner_delay` is reset according to the time left until next slot. - pub async fn next_slot(&mut self) { - self.inner_delay = match self.inner_delay.take() { - None => { - // Delay is not initialized in this case, - // so we have to initialize with the time until the next slot. - let wait_dur = time_until_next_slot(self.slot_duration); - Some(Delay::new(wait_dur)) - }, - Some(d) => Some(d), - }; - - if let Some(inner_delay) = self.inner_delay.take() { - inner_delay.await; - } - - let ends_in = time_until_next_slot(self.slot_duration); - - // Re-schedule delay for next slot. - self.inner_delay = Some(Delay::new(ends_in)); - } -} - -#[cfg(test)] -mod tests { - use super::*; - use std::{thread, time::Instant}; - - const SLOT_DURATION: Duration = Duration::from_millis(300); - const SLOT_TOLERANCE: Duration = Duration::from_millis(10); - - #[tokio::test] - async fn short_task_execution_does_not_influence_next_slot() { - let mut slot_stream = SlotStream::new(SLOT_DURATION); - - slot_stream.next_slot().await; - let now = Instant::now(); - // Task execution is shorter than slot duration. - thread::sleep(Duration::from_millis(200)); - slot_stream.next_slot().await; - - let elapsed = now.elapsed(); - assert!(elapsed >= SLOT_DURATION - SLOT_TOLERANCE); - assert!(elapsed <= SLOT_DURATION + SLOT_TOLERANCE); - } - - #[tokio::test] - async fn long_task_execution_does_not_cause_drift() { - let mut slot_stream = SlotStream::new(SLOT_DURATION); - - slot_stream.next_slot().await; - let now = Instant::now(); - // Task execution is longer than slot duration. - thread::sleep(Duration::from_millis(500)); - slot_stream.next_slot().await; - slot_stream.next_slot().await; - - let elapsed = now.elapsed(); - assert!(elapsed >= 2 * SLOT_DURATION - SLOT_TOLERANCE); - assert!(elapsed <= 2 * SLOT_DURATION + SLOT_TOLERANCE); - } -} diff --git a/bitacross-worker/sidechain/consensus/slots/src/slots.rs b/bitacross-worker/sidechain/consensus/slots/src/slots.rs deleted file mode 100644 index 7f8a910a97..0000000000 --- a/bitacross-worker/sidechain/consensus/slots/src/slots.rs +++ /dev/null @@ -1,421 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Utility stream for yielding slots in a loop. -//! -//! This is used instead of `futures_timer::Interval` because it was unreliable. - -use itp_time_utils::duration_now; -use its_block_verification::slot::slot_from_timestamp_and_duration; -use its_consensus_common::Error as ConsensusError; -use its_primitives::traits::{ - Block as SidechainBlockTrait, BlockData, SignedBlock as SignedSidechainBlockTrait, -}; -use lazy_static::lazy_static; -use log::warn; -use sp_runtime::traits::Block as ParentchainBlockTrait; -use std::time::Duration; - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use std::sync::SgxRwLock as RwLock; - -#[cfg(all(feature = "std", not(feature = "sgx")))] -use std::sync::RwLock; - -pub use sp_consensus_slots::Slot; - -/// Returns the duration until the next slot from now. -pub fn time_until_next_slot(slot_duration: Duration) -> Duration { - let now = duration_now().as_millis(); - - if slot_duration.as_millis() == u128::default() { - log::warn!("[Slots]: slot_duration.as_millis() is 0"); - return Default::default() - } - - let next_slot = (now + slot_duration.as_millis()) / slot_duration.as_millis(); - let remaining_millis = next_slot * slot_duration.as_millis() - now; - Duration::from_millis(remaining_millis as u64) -} - -/// Information about a slot. -#[derive(Debug, Clone)] -pub struct SlotInfo { - /// The slot number as found in the inherent data. - pub slot: Slot, - /// Current timestamp as found in the inherent data. - pub timestamp: Duration, - /// Slot duration. - pub duration: Duration, - /// The time at which the slot ends. - pub ends_at: Duration, - /// Last imported parentchain header, potentially outdated. - pub last_imported_integritee_parentchain_head: ParentchainBlock::Header, - /// Last imported parentchain header, potentially outdated. - pub maybe_last_imported_target_a_parentchain_head: Option, - /// Last imported parentchain header, potentially outdated. - pub maybe_last_imported_target_b_parentchain_head: Option, -} - -impl SlotInfo { - /// Create a new [`SlotInfo`]. - /// - /// `ends_at` is calculated using `now` and `time_until_next_slot`. - pub fn new( - slot: Slot, - timestamp: Duration, - duration: Duration, - ends_at: Duration, - last_imported_integritee_parentchain_head: ParentchainBlock::Header, - maybe_last_imported_target_a_parentchain_head: Option, - maybe_last_imported_target_b_parentchain_head: Option, - ) -> Self { - Self { - slot, - timestamp, - duration, - ends_at, - last_imported_integritee_parentchain_head, - maybe_last_imported_target_a_parentchain_head, - maybe_last_imported_target_b_parentchain_head, - } - } - - pub fn duration_remaining(&self) -> Option { - let duration_now = duration_now(); - if self.ends_at <= duration_now { - return None - } - Some(self.ends_at - duration_now) - } -} - -/// The time at which the slot ends. -/// -/// !! Slot duration needs to be the 'global' slot duration that is used for the sidechain. -/// Do not use this with 'custom' slot durations, as used e.g. for the shard slots. -pub fn slot_ends_at(slot: Slot, slot_duration: Duration) -> Duration { - Duration::from_millis(*slot.saturating_add(1u64) * (slot_duration.as_millis() as u64)) -} - -#[allow(dead_code)] -pub(crate) fn timestamp_within_slot< - ParentchainBlock: ParentchainBlockTrait, - SignedSidechainBlock: SignedSidechainBlockTrait, ->( - slot: &SlotInfo, - proposal: &SignedSidechainBlock, -) -> bool { - let proposal_stamp = proposal.block().block_data().timestamp(); - - let is_within_slot = slot.timestamp.as_millis() as u64 <= proposal_stamp - && slot.ends_at.as_millis() as u64 >= proposal_stamp; - - if !is_within_slot { - warn!( - "Proposed block slot time: {} ms, slot start: {} ms , slot end: {} ms", - proposal_stamp, - slot.timestamp.as_millis(), - slot.ends_at.as_millis() - ); - } - - is_within_slot -} - -pub fn yield_next_slot( - timestamp: Duration, - duration: Duration, - integritee_header: ParentchainBlock::Header, - maybe_target_a_header: Option, - maybe_target_b_header: Option, - last_slot_getter: &mut SlotGetter, -) -> Result>, ConsensusError> -where - SlotGetter: LastSlotTrait, - ParentchainBlock: ParentchainBlockTrait, -{ - if duration == Default::default() { - return Err(ConsensusError::Other("Tried to yield next slot with 0 duration".into())) - } - - let last_slot = last_slot_getter.get_last_slot()?; - let slot = slot_from_timestamp_and_duration(timestamp, duration); - - if slot <= last_slot { - return Ok(None) - } - - last_slot_getter.set_last_slot(slot)?; - - let slot_ends_time = slot_ends_at(slot, duration); - Ok(Some(SlotInfo::new( - slot, - timestamp, - duration, - slot_ends_time, - integritee_header, - maybe_target_a_header, - maybe_target_b_header, - ))) -} - -pub trait LastSlotTrait { - fn get_last_slot(&self) -> Result; - fn set_last_slot(&mut self, slot: Slot) -> Result<(), ConsensusError>; -} - -pub struct LastSlot; - -lazy_static! { - static ref LAST_SLOT: RwLock = Default::default(); -} - -impl LastSlotTrait for LastSlot { - fn get_last_slot(&self) -> Result { - Ok(*LAST_SLOT.read().map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))?) - } - - fn set_last_slot(&mut self, slot: Slot) -> Result<(), ConsensusError> { - *LAST_SLOT - .write() - .map_err(|e| ConsensusError::Other(format!("{:?}", e).into()))? = slot; - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use core::assert_matches::assert_matches; - use itc_parentchain_test::ParentchainHeaderBuilder; - use itp_types::Block as ParentchainBlock; - use its_primitives::{ - traits::{Block as BlockT, SignBlock}, - types::block::{Block, SignedBlock}, - }; - use its_test::{ - sidechain_block_data_builder::SidechainBlockDataBuilder, - sidechain_header_builder::SidechainHeaderBuilder, - }; - use sp_keyring::ed25519::Keyring; - use std::{fmt::Debug, thread, time::SystemTime}; - - const SLOT_DURATION: Duration = Duration::from_millis(1000); - const ALLOWED_THRESHOLD: Duration = Duration::from_millis(1); - - fn test_block_with_time_stamp(timestamp: u64) -> SignedBlock { - let header = SidechainHeaderBuilder::default().build(); - - let block_data = SidechainBlockDataBuilder::default().with_timestamp(timestamp).build(); - - Block::new(header, block_data).sign_block(&Keyring::Alice.pair()) - } - - fn slot(slot: u64) -> SlotInfo { - SlotInfo { - slot: slot.into(), - timestamp: duration_now(), - duration: SLOT_DURATION, - ends_at: duration_now() + SLOT_DURATION, - last_imported_integritee_parentchain_head: ParentchainHeaderBuilder::default().build(), - maybe_last_imported_target_a_parentchain_head: None, - maybe_last_imported_target_b_parentchain_head: None, - } - } - - fn timestamp_in_the_future(later: Duration) -> u64 { - let moment = SystemTime::now() + later; - let dur = moment.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| { - panic!("Current time {:?} is before unix epoch. Something is wrong: {:?}", moment, e) - }); - dur.as_millis() as u64 - } - - fn timestamp_in_the_past(earlier: Duration) -> u64 { - let moment = SystemTime::now() - earlier; - let dur = moment.duration_since(SystemTime::UNIX_EPOCH).unwrap_or_else(|e| { - panic!("Current time {:?} is before unix epoch. Something is wrong: {:?}", moment, e) - }); - dur.as_millis() as u64 - } - - fn assert_consensus_other_err(result: Result, msg: &str) { - assert_matches!(result.unwrap_err(), ConsensusError::Other( - m, - ) if m.to_string() == msg) - } - - #[test] - fn time_until_next_slot_returns_default_on_nano_duration() { - // prevent panic: https://github.com/integritee-network/worker/issues/439 - assert_eq!(time_until_next_slot(Duration::from_nanos(999)), Default::default()) - } - - #[test] - fn slot_info_ends_at_does_not_change_after_second_calculation() { - let timestamp = duration_now(); - let pc_header = ParentchainHeaderBuilder::default().build(); - let slot: Slot = 1000.into(); - - let slot_end_time = slot_ends_at(slot, SLOT_DURATION); - let slot_one: SlotInfo = SlotInfo::new( - slot, - timestamp, - SLOT_DURATION, - slot_end_time, - pc_header.clone(), - None, - None, - ); - thread::sleep(Duration::from_millis(200)); - let slot_two: SlotInfo = - SlotInfo::new(slot, timestamp, SLOT_DURATION, slot_end_time, pc_header, None, None); - - let difference_of_ends_at = - (slot_one.ends_at.as_millis()).abs_diff(slot_two.ends_at.as_millis()); - - assert!( - difference_of_ends_at < ALLOWED_THRESHOLD.as_millis(), - "Diff in ends at timestamp: {} ms, tolerance: {} ms", - difference_of_ends_at, - ALLOWED_THRESHOLD.as_millis() - ); - } - - #[test] - fn duration_remaing_returns_none_if_ends_at_is_in_the_past() { - let slot: SlotInfo = SlotInfo { - slot: 1.into(), - timestamp: duration_now() - Duration::from_secs(5), - duration: SLOT_DURATION, - ends_at: duration_now() + SLOT_DURATION - Duration::from_secs(5), - last_imported_integritee_parentchain_head: ParentchainHeaderBuilder::default().build(), - maybe_last_imported_target_a_parentchain_head: None, - maybe_last_imported_target_b_parentchain_head: None, - }; - assert!(slot.duration_remaining().is_none()); - } - - #[test] - fn duration_remaining_returns_some_if_ends_at_is_in_the_future() { - let slot: SlotInfo = SlotInfo { - slot: 1.into(), - timestamp: duration_now() - Duration::from_secs(5), - duration: SLOT_DURATION, - ends_at: duration_now() + Duration::from_secs(60), - last_imported_integritee_parentchain_head: ParentchainHeaderBuilder::default().build(), - maybe_last_imported_target_a_parentchain_head: None, - maybe_last_imported_target_b_parentchain_head: None, - }; - let maybe_duration_remaining = slot.duration_remaining(); - assert!(maybe_duration_remaining.is_some()); - assert!(maybe_duration_remaining.unwrap() > Duration::from_secs(30)); - } - - #[test] - fn slot_info_ends_at_does_is_correct_even_if_delay_is_more_than_slot_duration() { - let timestamp = duration_now(); - let pc_header = ParentchainHeaderBuilder::default().build(); - let slot: Slot = 1000.into(); - let slot_end_time = slot_ends_at(slot, SLOT_DURATION); - - thread::sleep(SLOT_DURATION * 2); - let slot: SlotInfo = - SlotInfo::new(slot, timestamp, SLOT_DURATION, slot_end_time, pc_header, None, None); - - assert!(slot.ends_at < duration_now()); - } - - #[test] - fn timestamp_within_slot_returns_true_for_correct_timestamp() { - let slot = slot(1); - let time_stamp_in_slot = timestamp_in_the_future(SLOT_DURATION / 2); - - let block = test_block_with_time_stamp(time_stamp_in_slot); - - assert!(timestamp_within_slot(&slot, &block)); - } - - #[test] - fn timestamp_within_slot_returns_false_if_timestamp_after_slot() { - let slot = slot(1); - let time_stamp_after_slot = - timestamp_in_the_future(SLOT_DURATION + Duration::from_millis(10)); - - let block_too_late = test_block_with_time_stamp(time_stamp_after_slot); - - assert!(!timestamp_within_slot(&slot, &block_too_late)); - } - - #[test] - fn timestamp_within_slot_returns_false_if_timestamp_before_slot() { - let slot = slot(1); - let time_stamp_before_slot = timestamp_in_the_past(Duration::from_millis(10)); - - let block_too_early = test_block_with_time_stamp(time_stamp_before_slot); - - assert!(!timestamp_within_slot(&slot, &block_too_early)); - } - - #[test] - fn yield_next_slot_returns_none_when_slot_equals_last_slot() { - let _lock = - LastSlot.set_last_slot(slot_from_timestamp_and_duration(duration_now(), SLOT_DURATION)); - assert!(yield_next_slot::<_, ParentchainBlock>( - duration_now(), - SLOT_DURATION, - ParentchainHeaderBuilder::default().build(), - None, - None, - &mut LastSlot, - ) - .unwrap() - .is_none()) - } - - #[test] - fn yield_next_slot_returns_next_slot() { - let _lock = - LastSlot.set_last_slot(slot_from_timestamp_and_duration(duration_now(), SLOT_DURATION)); - assert!(yield_next_slot::<_, ParentchainBlock>( - duration_now() + SLOT_DURATION, - SLOT_DURATION, - ParentchainHeaderBuilder::default().build(), - None, - None, - &mut LastSlot - ) - .unwrap() - .is_some()) - } - - #[test] - fn yield_next_slot_returns_err_on_0_duration() { - assert_consensus_other_err( - yield_next_slot::<_, ParentchainBlock>( - duration_now(), - Default::default(), - ParentchainHeaderBuilder::default().build(), - None, - None, - &mut LastSlot, - ), - "Tried to yield next slot with 0 duration", - ) - } -} diff --git a/bitacross-worker/sidechain/fork-tree/Cargo.toml b/bitacross-worker/sidechain/fork-tree/Cargo.toml deleted file mode 100644 index 6b9c4fc561..0000000000 --- a/bitacross-worker/sidechain/fork-tree/Cargo.toml +++ /dev/null @@ -1,27 +0,0 @@ -[package] -name = "fork-tree" -version = "3.0.0" -authors = ["Parity Technologies "] -edition = "2021" -license = "Apache-2.0" -homepage = "https://substrate.io" -repository = "https://github.com/paritytech/substrate/" -description = "Utility library for managing tree-like ordered data with logic for pruning the tree while finalizing nodes." -documentation = "https://docs.rs/fork-tree" -readme = "README.md" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.2.2", features = ["derive"], default-features = false } - -# sgx deps -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } - -[features] -default = ["std"] -std = [ - "codec/std", -] -sgx = [ - # teaclave - "sgx_tstd", -] diff --git a/bitacross-worker/sidechain/fork-tree/src/lib.rs b/bitacross-worker/sidechain/fork-tree/src/lib.rs deleted file mode 100644 index 0af11b653b..0000000000 --- a/bitacross-worker/sidechain/fork-tree/src/lib.rs +++ /dev/null @@ -1,1552 +0,0 @@ -// This file is part of Substrate. - -// Copyright (C) 2019-2022 Parity Technologies (UK) Ltd. -// SPDX-License-Identifier: Apache-2.0 - -// 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. - -//! Utility library for managing tree-like ordered data with logic for pruning -//! the tree while finalizing nodes. - -#![cfg_attr(not(feature = "std"), no_std)] -#![warn(missing_docs)] - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use std::vec::Vec; - -use codec::{Decode, Encode}; -use core::cmp::Reverse; - -/// Error occurred when iterating with the tree. -#[derive(Clone, Debug, PartialEq)] -pub enum Error { - /// Adding duplicate node to tree. - Duplicate, - /// Finalizing descendent of tree node without finalizing ancestor(s). - UnfinalizedAncestor, - /// Imported or finalized node that is an ancestor of previously finalized node. - Revert, - /// Error throw by client when checking for node ancestry. - Client(E), -} - -impl std::fmt::Display for Error { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - let message = match *self { - Error::Duplicate => "Hash already exists in Tree".into(), - Error::UnfinalizedAncestor => "Finalized descendent of Tree node without finalizing its ancestor(s) first".into(), - Error::Revert => "Tried to import or finalize node that is an ancestor of a previously finalized node".into(), - Error::Client(ref err) => format!("Client error: {}", err), - }; - write!(f, "{}", message) - } -} - -impl std::error::Error for Error { - fn cause(&self) -> Option<&dyn std::error::Error> { - None - } -} - -impl From for Error { - fn from(err: E) -> Error { - Error::Client(err) - } -} - -/// Result of finalizing a node (that could be a part of the tree or not). -#[derive(Debug, PartialEq)] -pub enum FinalizationResult { - /// The tree has changed, optionally return the value associated with the finalized node. - Changed(Option), - /// The tree has not changed. - Unchanged, -} - -/// Filtering action. -#[derive(Debug, PartialEq)] -pub enum FilterAction { - /// Remove the node and its subtree. - Remove, - /// Maintain the node. - KeepNode, - /// Maintain the node and its subtree. - KeepTree, -} - -/// A tree data structure that stores several nodes across multiple branches. -/// -/// Top-level branches are called roots. The tree has functionality for -/// finalizing nodes, which means that that node is traversed, and all competing -/// branches are pruned. It also guarantees that nodes in the tree are finalized -/// in order. Each node is uniquely identified by its hash but can be ordered by -/// its number. In order to build the tree an external function must be provided -/// when interacting with the tree to establish a node's ancestry. -#[derive(Clone, Debug, Decode, Encode, PartialEq)] -pub struct ForkTree { - roots: Vec>, - best_finalized_number: Option, -} - -impl Default for ForkTree { - fn default() -> ForkTree { - ForkTree { roots: Vec::new(), best_finalized_number: None } - } -} - -impl ForkTree -where - H: PartialEq, - N: Ord, -{ - /// Create a new empty tree. - pub fn new() -> ForkTree { - ForkTree { roots: Vec::new(), best_finalized_number: None } - } - - /// Rebalance the tree, i.e. sort child nodes by max branch depth (decreasing). - /// - /// Most operations in the tree are performed with depth-first search - /// starting from the leftmost node at every level, since this tree is meant - /// to be used in a blockchain context, a good heuristic is that the node - /// we'll be looking for at any point will likely be in one of the deepest chains - /// (i.e. the longest ones). - pub fn rebalance(&mut self) { - self.roots.sort_by_key(|n| Reverse(n.max_depth())); - let mut stack: Vec<_> = self.roots.iter_mut().collect(); - while let Some(node) = stack.pop() { - node.children.sort_by_key(|n| Reverse(n.max_depth())); - stack.extend(node.children.iter_mut()); - } - } - - /// Import a new node into the tree. The given function `is_descendent_of` - /// should return `true` if the second hash (target) is a descendent of the - /// first hash (base). This method assumes that nodes in the same branch are - /// imported in order. - /// - /// Returns `true` if the imported node is a root. - // WARNING: some users of this method (i.e. consensus epoch changes tree) currently silently - // rely on a **post-order DFS** traversal. If we are using instead a top-down traversal method - // then the `is_descendent_of` closure, when used after a warp-sync, may end up querying the - // backend for a block (the one corresponding to the root) that is not present and thus will - // return a wrong result. - pub fn import( - &mut self, - hash: H, - number: N, - data: V, - is_descendent_of: &F, - ) -> Result> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - H: std::fmt::Debug, - { - if let Some(ref best_finalized_number) = self.best_finalized_number { - if number <= *best_finalized_number { - return Err(Error::Revert) - } - } - - let (children, is_root) = - match self.find_node_where_mut(&hash, &number, is_descendent_of, &|_| true)? { - Some(parent) => (&mut parent.children, false), - None => (&mut self.roots, true), - }; - - if children.iter().any(|elem| elem.hash == hash) { - return Err(Error::Duplicate) - } - - children.push(Node { data, hash, number, children: Default::default() }); - - if children.len() == 1 { - // Rebalance may be required only if we've extended the branch depth. - self.rebalance(); - } - - Ok(is_root) - } - - /// Iterates over the existing roots in the tree. - pub fn roots(&self) -> impl Iterator { - self.roots.iter().map(|node| (&node.hash, &node.number, &node.data)) - } - - /// Iterates over the roots and gives just the hash and block number - pub fn roots_hash_and_number(&self) -> Vec<(&H, &N)> { - self.roots.iter().map(|node| (&node.hash, &node.number)).collect::>() - } - - fn node_iter(&self) -> impl Iterator> { - // we need to reverse the order of roots to maintain the expected - // ordering since the iterator uses a stack to track state. - ForkTreeIterator { stack: self.roots.iter().rev().collect() } - } - - /// Iterates the nodes in the tree in pre-order. - pub fn iter(&self) -> impl Iterator { - self.node_iter().map(|node| (&node.hash, &node.number, &node.data)) - } - - /// Map fork tree into values of new types. - /// - /// Tree traversal technique (e.g. BFS vs DFS) is left as not specified and - /// may be subject to change in the future. In other words, your predicates - /// should not rely on the observed traversal technique currently in use. - pub fn map(self, f: &mut F) -> ForkTree - where - F: FnMut(&H, &N, V) -> VT, - { - let mut queue: Vec<_> = - self.roots.into_iter().rev().map(|node| (usize::MAX, node)).collect(); - let mut next_queue = Vec::new(); - let mut output = Vec::new(); - - while !queue.is_empty() { - for (parent_index, node) in queue.drain(..) { - let new_data = f(&node.hash, &node.number, node.data); - let new_node = Node { - hash: node.hash, - number: node.number, - data: new_data, - children: Vec::with_capacity(node.children.len()), - }; - - let node_id = output.len(); - output.push((parent_index, new_node)); - - for child in node.children.into_iter().rev() { - next_queue.push((node_id, child)); - } - } - - std::mem::swap(&mut queue, &mut next_queue); - } - - let mut roots = Vec::new(); - while let Some((parent_index, new_node)) = output.pop() { - if parent_index == usize::MAX { - roots.push(new_node); - } else { - output[parent_index].1.children.push(new_node); - } - } - - ForkTree { roots, best_finalized_number: self.best_finalized_number } - } - - /// Find a node in the tree that is the deepest ancestor of the given - /// block hash and which passes the given predicate. The given function - /// `is_descendent_of` should return `true` if the second hash (target) - /// is a descendent of the first hash (base). - pub fn find_node_where( - &self, - hash: &H, - number: &N, - is_descendent_of: &F, - predicate: &P, - ) -> Result>, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - let maybe_path = self.find_node_index_where(hash, number, is_descendent_of, predicate)?; - Ok(maybe_path.map(|path| { - let children = - path.iter().take(path.len() - 1).fold(&self.roots, |curr, &i| &curr[i].children); - &children[path[path.len() - 1]] - })) - } - - /// Same as [`find_node_where`](ForkTree::find_node_where), but returns mutable reference. - pub fn find_node_where_mut( - &mut self, - hash: &H, - number: &N, - is_descendent_of: &F, - predicate: &P, - ) -> Result>, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - let maybe_path = self.find_node_index_where(hash, number, is_descendent_of, predicate)?; - Ok(maybe_path.map(|path| { - let children = path - .iter() - .take(path.len() - 1) - .fold(&mut self.roots, |curr, &i| &mut curr[i].children); - &mut children[path[path.len() - 1]] - })) - } - - /// Same as [`find_node_where`](ForkTree::find_node_where), but returns indices. - /// - /// The returned indices represent the full path to reach the matching node starting - /// from first to last, i.e. the earliest index in the traverse path goes first, and the final - /// index in the traverse path goes last. If a node is found that matches the predicate - /// the returned path should always contain at least one index, otherwise `None` is - /// returned. - // WARNING: some users of this method (i.e. consensus epoch changes tree) currently silently - // rely on a **post-order DFS** traversal. If we are using instead a top-down traversal method - // then the `is_descendent_of` closure, when used after a warp-sync, will end up querying the - // backend for a block (the one corresponding to the root) that is not present and thus will - // return a wrong result. - pub fn find_node_index_where( - &self, - hash: &H, - number: &N, - is_descendent_of: &F, - predicate: &P, - ) -> Result>, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - let mut stack = vec![]; - let mut root_idx = 0; - let mut found = false; - let mut is_descendent = false; - - while root_idx < self.roots.len() { - if *number <= self.roots[root_idx].number { - root_idx += 1; - continue - } - // The second element in the stack tuple tracks what is the **next** children - // index to search into. If we find an ancestor then we stop searching into - // alternative branches and we focus on the current path up to the root. - stack.push((&self.roots[root_idx], 0)); - while let Some((node, i)) = stack.pop() { - if i < node.children.len() && !is_descendent { - stack.push((node, i + 1)); - if node.children[i].number < *number { - stack.push((&node.children[i], 0)); - } - } else if is_descendent || is_descendent_of(&node.hash, hash)? { - is_descendent = true; - if predicate(&node.data) { - found = true; - break - } - } - } - - // If the element we are looking for is a descendent of the current root - // then we can stop the search. - if is_descendent { - break - } - root_idx += 1; - } - - Ok(if found { - // The path is the root index followed by the indices of all the children - // we were processing when we found the element (remember the stack - // contains the index of the **next** children to process). - let path: Vec<_> = - std::iter::once(root_idx).chain(stack.iter().map(|(_, i)| *i - 1)).collect(); - Some(path) - } else { - None - }) - } - - /// Prune the tree, removing all non-canonical nodes. We find the node in the - /// tree that is the deepest ancestor of the given hash and that passes the - /// given predicate. If such a node exists, we re-root the tree to this - /// node. Otherwise the tree remains unchanged. The given function - /// `is_descendent_of` should return `true` if the second hash (target) is a - /// descendent of the first hash (base). - /// - /// Returns all pruned node data. - pub fn prune( - &mut self, - hash: &H, - number: &N, - is_descendent_of: &F, - predicate: &P, - ) -> Result, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - let root_index = - match self.find_node_index_where(hash, number, is_descendent_of, predicate)? { - Some(idx) => idx, - None => return Ok(RemovedIterator { stack: Vec::new() }), - }; - - let mut old_roots = std::mem::take(&mut self.roots); - - let curr_children = root_index - .iter() - .take(root_index.len() - 1) - .fold(&mut old_roots, |curr, idx| &mut curr[*idx].children); - let mut root = curr_children.remove(root_index[root_index.len() - 1]); - - let mut removed = old_roots; - - // we found the deepest ancestor of the finalized block, so we prune - // out any children that don't include the finalized block. - let root_children = std::mem::take(&mut root.children); - let mut is_first = true; - - for child in root_children { - if is_first - && (child.number == *number && child.hash == *hash - || child.number < *number && is_descendent_of(&child.hash, hash)?) - { - root.children.push(child); - // assuming that the tree is well formed only one child should pass this - // requirement due to ancestry restrictions (i.e. they must be different forks). - is_first = false; - } else { - removed.push(child); - } - } - - self.roots = vec![root]; - self.rebalance(); - - Ok(RemovedIterator { stack: removed }) - } - - /// Finalize a root in the tree and return it, return `None` in case no root - /// with the given hash exists. All other roots are pruned, and the children - /// of the finalized node become the new roots. - pub fn finalize_root(&mut self, hash: &H) -> Option { - self.roots - .iter() - .position(|node| node.hash == *hash) - .map(|position| self.finalize_root_at(position)) - } - - /// Finalize root at given position. See `finalize_root` comment for details. - fn finalize_root_at(&mut self, position: usize) -> V { - let node = self.roots.swap_remove(position); - self.roots = node.children; - self.best_finalized_number = Some(node.number); - node.data - } - - /// Finalize a node in the tree. This method will make sure that the node - /// being finalized is either an existing root (and return its data), or a - /// node from a competing branch (not in the tree), tree pruning is done - /// accordingly. The given function `is_descendent_of` should return `true` - /// if the second hash (target) is a descendent of the first hash (base). - pub fn finalize( - &mut self, - hash: &H, - number: N, - is_descendent_of: &F, - ) -> Result, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - { - if let Some(ref best_finalized_number) = self.best_finalized_number { - if number <= *best_finalized_number { - return Err(Error::Revert) - } - } - - // check if one of the current roots is being finalized - if let Some(root) = self.finalize_root(hash) { - return Ok(FinalizationResult::Changed(Some(root))) - } - - // make sure we're not finalizing a descendent of any root - for root in self.roots.iter() { - if number > root.number && is_descendent_of(&root.hash, hash)? { - return Err(Error::UnfinalizedAncestor) - } - } - - // we finalized a block earlier than any existing root (or possibly - // another fork not part of the tree). make sure to only keep roots that - // are part of the finalized branch - let mut changed = false; - let roots = std::mem::take(&mut self.roots); - - for root in roots { - if root.number > number && is_descendent_of(hash, &root.hash)? { - self.roots.push(root); - } else { - changed = true; - } - } - - self.best_finalized_number = Some(number); - - if changed { - Ok(FinalizationResult::Changed(None)) - } else { - Ok(FinalizationResult::Unchanged) - } - } - - /// Finalize a node in the tree and all its ancestors. The given function - /// `is_descendent_of` should return `true` if the second hash (target) is - // a descendent of the first hash (base). - pub fn finalize_with_ancestors( - &mut self, - hash: &H, - number: N, - is_descendent_of: &F, - ) -> Result, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - { - if let Some(ref best_finalized_number) = self.best_finalized_number { - if number <= *best_finalized_number { - return Err(Error::Revert) - } - } - - // check if one of the current roots is being finalized - if let Some(root) = self.finalize_root(hash) { - return Ok(FinalizationResult::Changed(Some(root))) - } - - // we need to: - // 1) remove all roots that are not ancestors AND not descendants of finalized block; - // 2) if node is descendant - just leave it; - // 3) if node is ancestor - 'open it' - let mut changed = false; - let mut idx = 0; - while idx != self.roots.len() { - let (is_finalized, is_descendant, is_ancestor) = { - let root = &self.roots[idx]; - let is_finalized = root.hash == *hash; - let is_descendant = - !is_finalized && root.number > number && is_descendent_of(hash, &root.hash)?; - let is_ancestor = !is_finalized - && !is_descendant && root.number < number - && is_descendent_of(&root.hash, hash)?; - (is_finalized, is_descendant, is_ancestor) - }; - - // if we have met finalized root - open it and return - if is_finalized { - return Ok(FinalizationResult::Changed(Some(self.finalize_root_at(idx)))) - } - - // if node is descendant of finalized block - just leave it as is - if is_descendant { - idx += 1; - continue - } - - // if node is ancestor of finalized block - remove it and continue with children - if is_ancestor { - let root = self.roots.swap_remove(idx); - self.roots.extend(root.children); - changed = true; - continue - } - - // if node is neither ancestor, nor descendant of the finalized block - remove it - self.roots.swap_remove(idx); - changed = true; - } - - self.best_finalized_number = Some(number); - - if changed { - Ok(FinalizationResult::Changed(None)) - } else { - Ok(FinalizationResult::Unchanged) - } - } - - /// Checks if any node in the tree is finalized by either finalizing the - /// node itself or a node's descendent that's not in the tree, guaranteeing - /// that the node being finalized isn't a descendent of (or equal to) any of - /// the node's children. Returns `Some(true)` if the node being finalized is - /// a root, `Some(false)` if the node being finalized is not a root, and - /// `None` if no node in the tree is finalized. The given `predicate` is - /// checked on the prospective finalized root and must pass for finalization - /// to occur. The given function `is_descendent_of` should return `true` if - /// the second hash (target) is a descendent of the first hash (base). - pub fn finalizes_any_with_descendent_if( - &self, - hash: &H, - number: N, - is_descendent_of: &F, - predicate: P, - ) -> Result, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - if let Some(ref best_finalized_number) = self.best_finalized_number { - if number <= *best_finalized_number { - return Err(Error::Revert) - } - } - - // check if the given hash is equal or a descendent of any node in the - // tree, if we find a valid node that passes the predicate then we must - // ensure that we're not finalizing past any of its child nodes. - for node in self.node_iter() { - if predicate(&node.data) && (node.hash == *hash || is_descendent_of(&node.hash, hash)?) - { - for child in node.children.iter() { - if child.number <= number - && (child.hash == *hash || is_descendent_of(&child.hash, hash)?) - { - return Err(Error::UnfinalizedAncestor) - } - } - - return Ok(Some(self.roots.iter().any(|root| root.hash == node.hash))) - } - } - - Ok(None) - } - - /// Finalize a root in the tree by either finalizing the node itself or a - /// node's descendent that's not in the tree, guaranteeing that the node - /// being finalized isn't a descendent of (or equal to) any of the root's - /// children. The given `predicate` is checked on the prospective finalized - /// root and must pass for finalization to occur. The given function - /// `is_descendent_of` should return `true` if the second hash (target) is a - /// descendent of the first hash (base). - pub fn finalize_with_descendent_if( - &mut self, - hash: &H, - number: N, - is_descendent_of: &F, - predicate: P, - ) -> Result, Error> - where - E: std::error::Error, - F: Fn(&H, &H) -> Result, - P: Fn(&V) -> bool, - { - if let Some(ref best_finalized_number) = self.best_finalized_number { - if number <= *best_finalized_number { - return Err(Error::Revert) - } - } - - // check if the given hash is equal or a a descendent of any root, if we - // find a valid root that passes the predicate then we must ensure that - // we're not finalizing past any children node. - let mut position = None; - for (i, root) in self.roots.iter().enumerate() { - if predicate(&root.data) && (root.hash == *hash || is_descendent_of(&root.hash, hash)?) - { - for child in root.children.iter() { - if child.number <= number - && (child.hash == *hash || is_descendent_of(&child.hash, hash)?) - { - return Err(Error::UnfinalizedAncestor) - } - } - - position = Some(i); - break - } - } - - let node_data = position.map(|i| { - let node = self.roots.swap_remove(i); - self.roots = node.children; - self.best_finalized_number = Some(node.number); - node.data - }); - - // Retain only roots that are descendents of the finalized block (this - // happens if the node has been properly finalized) or that are - // ancestors (or equal) to the finalized block (in this case the node - // wasn't finalized earlier presumably because the predicate didn't - // pass). - let mut changed = false; - let roots = std::mem::take(&mut self.roots); - - for root in roots { - let retain = root.number > number && is_descendent_of(hash, &root.hash)? - || root.number == number && root.hash == *hash - || is_descendent_of(&root.hash, hash)?; - - if retain { - self.roots.push(root); - } else { - changed = true; - } - } - - self.best_finalized_number = Some(number); - - match (node_data, changed) { - (Some(data), _) => Ok(FinalizationResult::Changed(Some(data))), - (None, true) => Ok(FinalizationResult::Changed(None)), - (None, false) => Ok(FinalizationResult::Unchanged), - } - } - - /// Remove from the tree some nodes (and their subtrees) using a `filter` predicate. - /// - /// The `filter` is called over tree nodes and returns a filter action: - /// - `Remove` if the node and its subtree should be removed; - /// - `KeepNode` if we should maintain the node and keep processing the tree. - /// - `KeepTree` if we should maintain the node and its entire subtree. - /// - /// An iterator over all the pruned nodes is returned. - pub fn drain_filter(&mut self, filter: F) -> impl Iterator - where - F: Fn(&H, &N, &V) -> FilterAction, - { - let mut removed = vec![]; - let mut retained = Vec::new(); - - let mut queue: Vec<_> = std::mem::take(&mut self.roots) - .into_iter() - .rev() - .map(|node| (usize::MAX, node)) - .collect(); - let mut next_queue = Vec::new(); - - while !queue.is_empty() { - for (parent_idx, mut node) in queue.drain(..) { - match filter(&node.hash, &node.number, &node.data) { - FilterAction::KeepNode => { - let node_idx = retained.len(); - let children = std::mem::take(&mut node.children); - retained.push((parent_idx, node)); - for child in children.into_iter().rev() { - next_queue.push((node_idx, child)); - } - }, - FilterAction::KeepTree => { - retained.push((parent_idx, node)); - }, - FilterAction::Remove => { - removed.push(node); - }, - } - } - - std::mem::swap(&mut queue, &mut next_queue); - } - - while let Some((parent_idx, node)) = retained.pop() { - if parent_idx == usize::MAX { - self.roots.push(node); - } else { - retained[parent_idx].1.children.push(node); - } - } - - if !removed.is_empty() { - self.rebalance(); - } - RemovedIterator { stack: removed } - } -} - -// Workaround for: https://github.com/rust-lang/rust/issues/34537 -use node_implementation::Node; - -mod node_implementation { - use super::*; - - #[derive(Clone, Debug, Decode, Encode, PartialEq)] - pub struct Node { - pub hash: H, - pub number: N, - pub data: V, - pub children: Vec>, - } - - impl Node { - /// Finds the max depth among all branches descendent from this node. - pub fn max_depth(&self) -> usize { - let mut max: usize = 0; - let mut stack = vec![(self, 0)]; - while let Some((node, height)) = stack.pop() { - if height > max { - max = height; - } - node.children.iter().for_each(|n| stack.push((n, height + 1))); - } - max - } - } -} - -struct ForkTreeIterator<'a, H, N, V> { - stack: Vec<&'a Node>, -} - -impl<'a, H, N, V> Iterator for ForkTreeIterator<'a, H, N, V> { - type Item = &'a Node; - - fn next(&mut self) -> Option { - self.stack.pop().map(|node| { - // child nodes are stored ordered by max branch height (decreasing), - // we want to keep this ordering while iterating but since we're - // using a stack for iterator state we need to reverse it. - self.stack.extend(node.children.iter().rev()); - node - }) - } -} - -struct RemovedIterator { - stack: Vec>, -} - -impl Iterator for RemovedIterator { - type Item = (H, N, V); - - fn next(&mut self) -> Option { - self.stack.pop().map(|mut node| { - // child nodes are stored ordered by max branch height (decreasing), - // we want to keep this ordering while iterating but since we're - // using a stack for iterator state we need to reverse it. - let children = std::mem::take(&mut node.children); - - self.stack.extend(children.into_iter().rev()); - (node.hash, node.number, node.data) - }) - } -} - -#[cfg(test)] -mod test { - use crate::FilterAction; - - use super::{Error, FinalizationResult, ForkTree}; - - #[derive(Debug, PartialEq)] - struct TestError; - - impl std::fmt::Display for TestError { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "TestError") - } - } - - impl std::error::Error for TestError {} - - fn test_fork_tree<'a>( - ) -> (ForkTree<&'a str, u64, ()>, impl Fn(&&str, &&str) -> Result) { - let mut tree = ForkTree::new(); - - #[rustfmt::skip] - // - // - B - C - D - E - // / - // / - G - // / / - // A - F - H - I - // \ \ - // \ - L - M - N - // \ \ - // \ - O - // - J - K - // - // (where N is not a part of fork tree) - // - // NOTE: the tree will get automatically rebalance on import and won't be laid out like the - // diagram above. the children will be ordered by subtree depth and the longest branches - // will be on the leftmost side of the tree. - let is_descendent_of = |base: &&str, block: &&str| -> Result { - let letters = vec!["B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O"]; - match (*base, *block) { - ("A", b) => Ok(letters.into_iter().any(|n| n == b)), - ("B", b) => Ok(b == "C" || b == "D" || b == "E"), - ("C", b) => Ok(b == "D" || b == "E"), - ("D", b) => Ok(b == "E"), - ("E", _) => Ok(false), - ("F", b) => - Ok(b == "G" || b == "H" || b == "I" || b == "L" || b == "M" || b == "N" || b == "O"), - ("G", _) => Ok(false), - ("H", b) => Ok(b == "I" || b == "L" || b == "M" || b == "N" || b == "O"), - ("I", _) => Ok(false), - ("J", b) => Ok(b == "K"), - ("K", _) => Ok(false), - ("L", b) => Ok(b == "M" || b == "O" || b == "N"), - ("M", b) => Ok(b == "N"), - ("O", _) => Ok(false), - ("0", _) => Ok(true), - _ => Ok(false), - } - }; - - tree.import("A", 1, (), &is_descendent_of).unwrap(); - - tree.import("B", 2, (), &is_descendent_of).unwrap(); - tree.import("C", 3, (), &is_descendent_of).unwrap(); - tree.import("D", 4, (), &is_descendent_of).unwrap(); - tree.import("E", 5, (), &is_descendent_of).unwrap(); - - tree.import("F", 2, (), &is_descendent_of).unwrap(); - tree.import("G", 3, (), &is_descendent_of).unwrap(); - - tree.import("H", 3, (), &is_descendent_of).unwrap(); - tree.import("I", 4, (), &is_descendent_of).unwrap(); - tree.import("L", 4, (), &is_descendent_of).unwrap(); - tree.import("M", 5, (), &is_descendent_of).unwrap(); - tree.import("O", 5, (), &is_descendent_of).unwrap(); - - tree.import("J", 2, (), &is_descendent_of).unwrap(); - tree.import("K", 3, (), &is_descendent_of).unwrap(); - - (tree, is_descendent_of) - } - - #[test] - fn import_doesnt_revert() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - tree.finalize_root(&"A"); - - assert_eq!(tree.best_finalized_number, Some(1)); - - assert_eq!(tree.import("A", 1, (), &is_descendent_of), Err(Error::Revert)); - } - - #[test] - fn import_doesnt_add_duplicates() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - assert_eq!(tree.import("A", 1, (), &is_descendent_of), Err(Error::Duplicate)); - - assert_eq!(tree.import("I", 4, (), &is_descendent_of), Err(Error::Duplicate)); - - assert_eq!(tree.import("G", 3, (), &is_descendent_of), Err(Error::Duplicate)); - - assert_eq!(tree.import("K", 3, (), &is_descendent_of), Err(Error::Duplicate)); - } - - #[test] - fn finalize_root_works() { - let finalize_a = || { - let (mut tree, ..) = test_fork_tree(); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![("A", 1)]); - - // finalizing "A" opens up three possible forks - tree.finalize_root(&"A"); - - assert_eq!( - tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![("B", 2), ("F", 2), ("J", 2)], - ); - - tree - }; - - { - let mut tree = finalize_a(); - - // finalizing "B" will progress on its fork and remove any other competing forks - tree.finalize_root(&"B"); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![("C", 3)],); - - // all the other forks have been pruned - assert!(tree.roots.len() == 1); - } - - { - let mut tree = finalize_a(); - - // finalizing "J" will progress on its fork and remove any other competing forks - tree.finalize_root(&"J"); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![("K", 3)],); - - // all the other forks have been pruned - assert!(tree.roots.len() == 1); - } - } - - #[test] - fn finalize_works() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - let original_roots = tree.roots.clone(); - - // finalizing a block prior to any in the node doesn't change the tree - assert_eq!(tree.finalize(&"0", 0, &is_descendent_of), Ok(FinalizationResult::Unchanged)); - - assert_eq!(tree.roots, original_roots); - - // finalizing "A" opens up three possible forks - assert_eq!( - tree.finalize(&"A", 1, &is_descendent_of), - Ok(FinalizationResult::Changed(Some(()))), - ); - - assert_eq!( - tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![("B", 2), ("F", 2), ("J", 2)], - ); - - // finalizing anything lower than what we observed will fail - assert_eq!(tree.best_finalized_number, Some(1)); - - assert_eq!(tree.finalize(&"Z", 1, &is_descendent_of), Err(Error::Revert)); - - // trying to finalize a node without finalizing its ancestors first will fail - assert_eq!(tree.finalize(&"H", 3, &is_descendent_of), Err(Error::UnfinalizedAncestor)); - - // after finalizing "F" we can finalize "H" - assert_eq!( - tree.finalize(&"F", 2, &is_descendent_of), - Ok(FinalizationResult::Changed(Some(()))), - ); - - assert_eq!( - tree.finalize(&"H", 3, &is_descendent_of), - Ok(FinalizationResult::Changed(Some(()))), - ); - - assert_eq!( - tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![("L", 4), ("I", 4)], - ); - - // finalizing a node from another fork that isn't part of the tree clears the tree - assert_eq!( - tree.finalize(&"Z", 5, &is_descendent_of), - Ok(FinalizationResult::Changed(None)), - ); - - assert!(tree.roots.is_empty()); - } - - #[test] - fn finalize_with_ancestor_works() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - let original_roots = tree.roots.clone(); - - // finalizing a block prior to any in the node doesn't change the tree - assert_eq!( - tree.finalize_with_ancestors(&"0", 0, &is_descendent_of), - Ok(FinalizationResult::Unchanged), - ); - - assert_eq!(tree.roots, original_roots); - - // finalizing "A" opens up three possible forks - assert_eq!( - tree.finalize_with_ancestors(&"A", 1, &is_descendent_of), - Ok(FinalizationResult::Changed(Some(()))), - ); - - assert_eq!( - tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![("B", 2), ("F", 2), ("J", 2)], - ); - - // finalizing H: - // 1) removes roots that are not ancestors/descendants of H (B, J) - // 2) opens root that is ancestor of H (F -> G+H) - // 3) finalizes the just opened root H (H -> I + L) - assert_eq!( - tree.finalize_with_ancestors(&"H", 3, &is_descendent_of), - Ok(FinalizationResult::Changed(Some(()))), - ); - - assert_eq!( - tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![("L", 4), ("I", 4)], - ); - - assert_eq!(tree.best_finalized_number, Some(3)); - - // finalizing N (which is not a part of the tree): - // 1) removes roots that are not ancestors/descendants of N (I) - // 2) opens root that is ancestor of N (L -> M+O) - // 3) removes roots that are not ancestors/descendants of N (O) - // 4) opens root that is ancestor of N (M -> {}) - assert_eq!( - tree.finalize_with_ancestors(&"N", 6, &is_descendent_of), - Ok(FinalizationResult::Changed(None)), - ); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![],); - - assert_eq!(tree.best_finalized_number, Some(6)); - } - - #[test] - fn finalize_with_descendent_works() { - #[derive(Debug, PartialEq)] - struct Change { - effective: u64, - } - - let (mut tree, is_descendent_of) = { - let mut tree = ForkTree::new(); - - let is_descendent_of = |base: &&str, block: &&str| -> Result { - // A0 #1 - (B #2) - (C #5) - D #10 - E #15 - (F #100) - // \ - // - (G #100) - // - // A1 #1 - // - // Nodes B, C, F and G are not part of the tree. - match (*base, *block) { - ("A0", b) => Ok(b == "B" || b == "C" || b == "D" || b == "E" || b == "G"), - ("A1", _) => Ok(false), - ("C", b) => Ok(b == "D"), - ("D", b) => Ok(b == "E" || b == "F" || b == "G"), - ("E", b) => Ok(b == "F"), - _ => Ok(false), - } - }; - - let is_root = tree.import("A0", 1, Change { effective: 5 }, &is_descendent_of).unwrap(); - assert!(is_root); - let is_root = tree.import("A1", 1, Change { effective: 5 }, &is_descendent_of).unwrap(); - assert!(is_root); - let is_root = - tree.import("D", 10, Change { effective: 10 }, &is_descendent_of).unwrap(); - assert!(!is_root); - let is_root = - tree.import("E", 15, Change { effective: 50 }, &is_descendent_of).unwrap(); - assert!(!is_root); - - (tree, is_descendent_of) - }; - - assert_eq!( - tree.finalizes_any_with_descendent_if( - &"B", - 2, - &is_descendent_of, - |c| c.effective <= 2, - ), - Ok(None), - ); - - // finalizing "D" is not allowed since it is not a root. - assert_eq!( - tree.finalize_with_descendent_if(&"D", 10, &is_descendent_of, |c| c.effective <= 10), - Err(Error::UnfinalizedAncestor) - ); - - // finalizing "D" will finalize a block from the tree, but it can't be applied yet - // since it is not a root change. - assert_eq!( - tree.finalizes_any_with_descendent_if(&"D", 10, &is_descendent_of, |c| c.effective - == 10), - Ok(Some(false)), - ); - - // finalizing "E" is not allowed since there are not finalized anchestors. - assert_eq!( - tree.finalizes_any_with_descendent_if(&"E", 15, &is_descendent_of, |c| c.effective - == 10), - Err(Error::UnfinalizedAncestor) - ); - - // finalizing "B" doesn't finalize "A0" since the predicate doesn't pass, - // although it will clear out "A1" from the tree - assert_eq!( - tree.finalize_with_descendent_if(&"B", 2, &is_descendent_of, |c| c.effective <= 2), - Ok(FinalizationResult::Changed(None)), - ); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![("A0", 1)],); - - // finalizing "C" will finalize the node "A0" and prune it out of the tree - assert_eq!( - tree.finalizes_any_with_descendent_if( - &"C", - 5, - &is_descendent_of, - |c| c.effective <= 5, - ), - Ok(Some(true)), - ); - - assert_eq!( - tree.finalize_with_descendent_if(&"C", 5, &is_descendent_of, |c| c.effective <= 5), - Ok(FinalizationResult::Changed(Some(Change { effective: 5 }))), - ); - - assert_eq!(tree.roots().map(|(h, n, _)| (*h, *n)).collect::>(), vec![("D", 10)],); - - // finalizing "F" will fail since it would finalize past "E" without finalizing "D" first - assert_eq!( - tree.finalizes_any_with_descendent_if(&"F", 100, &is_descendent_of, |c| c.effective - <= 100,), - Err(Error::UnfinalizedAncestor), - ); - - // it will work with "G" though since it is not in the same branch as "E" - assert_eq!( - tree.finalizes_any_with_descendent_if(&"G", 100, &is_descendent_of, |c| c.effective - <= 100), - Ok(Some(true)), - ); - - assert_eq!( - tree.finalize_with_descendent_if(&"G", 100, &is_descendent_of, |c| c.effective <= 100), - Ok(FinalizationResult::Changed(Some(Change { effective: 10 }))), - ); - - // "E" will be pruned out - assert_eq!(tree.roots().count(), 0); - } - - #[test] - fn iter_iterates_in_preorder() { - let (tree, ..) = test_fork_tree(); - assert_eq!( - tree.iter().map(|(h, n, _)| (*h, *n)).collect::>(), - vec![ - ("A", 1), - ("B", 2), - ("C", 3), - ("D", 4), - ("E", 5), - ("F", 2), - ("H", 3), - ("L", 4), - ("M", 5), - ("O", 5), - ("I", 4), - ("G", 3), - ("J", 2), - ("K", 3), - ], - ); - } - - #[test] - fn minimizes_calls_to_is_descendent_of() { - use std::sync::atomic::{AtomicUsize, Ordering}; - - let n_is_descendent_of_calls = AtomicUsize::new(0); - - let is_descendent_of = |_: &&str, _: &&str| -> Result { - n_is_descendent_of_calls.fetch_add(1, Ordering::SeqCst); - Ok(true) - }; - - { - // Deep tree where we want to call `finalizes_any_with_descendent_if`. The - // search for the node should first check the predicate (which is cheaper) and - // only then call `is_descendent_of` - let mut tree = ForkTree::new(); - let letters = vec!["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"]; - - for (i, letter) in letters.iter().enumerate() { - tree.import::<_, TestError>(*letter, i, i, &|_, _| Ok(true)).unwrap(); - } - - // "L" is a descendent of "K", but the predicate will only pass for "K", - // therefore only one call to `is_descendent_of` should be made - assert_eq!( - tree.finalizes_any_with_descendent_if(&"L", 11, &is_descendent_of, |i| *i == 10,), - Ok(Some(false)), - ); - - assert_eq!(n_is_descendent_of_calls.load(Ordering::SeqCst), 1); - } - - n_is_descendent_of_calls.store(0, Ordering::SeqCst); - - { - // Multiple roots in the tree where we want to call `finalize_with_descendent_if`. - // The search for the root node should first check the predicate (which is cheaper) - // and only then call `is_descendent_of` - let mut tree = ForkTree::new(); - let letters = vec!["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K"]; - - for (i, letter) in letters.iter().enumerate() { - tree.import::<_, TestError>(*letter, i, i, &|_, _| Ok(false)).unwrap(); - } - - // "L" is a descendent of "K", but the predicate will only pass for "K", - // therefore only one call to `is_descendent_of` should be made - assert_eq!( - tree.finalize_with_descendent_if(&"L", 11, &is_descendent_of, |i| *i == 10,), - Ok(FinalizationResult::Changed(Some(10))), - ); - - assert_eq!(n_is_descendent_of_calls.load(Ordering::SeqCst), 1); - } - } - - #[test] - fn map_works() { - let (mut tree, _) = test_fork_tree(); - - // Extend the single root fork-tree to also excercise the roots order during map. - let is_descendent_of = |_: &&str, _: &&str| -> Result { Ok(false) }; - let is_root = tree.import("A1", 1, (), &is_descendent_of).unwrap(); - assert!(is_root); - let is_root = tree.import("A2", 1, (), &is_descendent_of).unwrap(); - assert!(is_root); - - let old_tree = tree.clone(); - let new_tree = tree.map(&mut |hash, _, _| hash.to_owned()); - - // Check content and order - assert!(new_tree.iter().all(|(hash, _, data)| hash == data)); - assert_eq!( - old_tree.iter().map(|(hash, _, _)| *hash).collect::>(), - new_tree.iter().map(|(hash, _, _)| *hash).collect::>(), - ); - } - - #[test] - fn prune_works() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - let removed = tree.prune(&"C", &3, &is_descendent_of, &|_| true).unwrap(); - - assert_eq!(tree.roots.iter().map(|node| node.hash).collect::>(), vec!["B"]); - - assert_eq!( - tree.iter().map(|(hash, _, _)| *hash).collect::>(), - vec!["B", "C", "D", "E"], - ); - - assert_eq!( - removed.map(|(hash, _, _)| hash).collect::>(), - vec!["A", "F", "H", "L", "M", "O", "I", "G", "J", "K"] - ); - - let removed = tree.prune(&"E", &5, &is_descendent_of, &|_| true).unwrap(); - - assert_eq!(tree.roots.iter().map(|node| node.hash).collect::>(), vec!["D"]); - - assert_eq!(tree.iter().map(|(hash, _, _)| *hash).collect::>(), vec!["D", "E"]); - - assert_eq!(removed.map(|(hash, _, _)| hash).collect::>(), vec!["B", "C"]); - } - - #[test] - fn find_node_backtracks_after_finding_highest_descending_node() { - let mut tree = ForkTree::new(); - - // A - B - // \ - // — C - // - let is_descendent_of = |base: &&str, block: &&str| -> Result { - match (*base, *block) { - ("A", b) => Ok(b == "B" || b == "C" || b == "D"), - ("B", b) | ("C", b) => Ok(b == "D"), - ("0", _) => Ok(true), - _ => Ok(false), - } - }; - - tree.import("A", 1, 1, &is_descendent_of).unwrap(); - tree.import("B", 2, 2, &is_descendent_of).unwrap(); - tree.import("C", 2, 4, &is_descendent_of).unwrap(); - - // when searching the tree we reach node `C`, but the - // predicate doesn't pass. we should backtrack to `B`, but not to `A`, - // since "B" fulfills the predicate. - let node = tree.find_node_where(&"D", &3, &is_descendent_of, &|data| *data < 3).unwrap(); - - assert_eq!(node.unwrap().hash, "B"); - } - - #[test] - fn rebalance_works() { - let (mut tree, _) = test_fork_tree(); - - // the tree is automatically rebalanced on import, therefore we should iterate in preorder - // exploring the longest forks first. check the ascii art above to understand the expected - // output below. - assert_eq!( - tree.iter().map(|(h, _, _)| *h).collect::>(), - vec!["A", "B", "C", "D", "E", "F", "H", "L", "M", "O", "I", "G", "J", "K"], - ); - - // let's add a block "P" which is a descendent of block "O" - let is_descendent_of = |base: &&str, block: &&str| -> Result { - match (*base, *block) { - (b, "P") => Ok(vec!["A", "F", "H", "L", "O"].into_iter().any(|n| n == b)), - _ => Ok(false), - } - }; - - tree.import("P", 6, (), &is_descendent_of).unwrap(); - - // this should re-order the tree, since the branch "A -> B -> C -> D -> E" is no longer tied - // with 5 blocks depth. additionally "O" should be visited before "M" now, since it has one - // descendent "P" which makes that branch 6 blocks long. - assert_eq!( - tree.iter().map(|(h, _, _)| *h).collect::>(), - ["A", "F", "H", "L", "O", "P", "M", "I", "G", "B", "C", "D", "E", "J", "K"] - ); - } - - #[test] - fn drain_filter_works() { - let (mut tree, _) = test_fork_tree(); - - let filter = |h: &&str, _: &u64, _: &()| match *h { - "A" | "B" | "F" | "G" => FilterAction::KeepNode, - "C" => FilterAction::KeepTree, - "H" | "J" => FilterAction::Remove, - _ => panic!("Unexpected filtering for node: {}", *h), - }; - - let removed = tree.drain_filter(filter); - - assert_eq!( - tree.iter().map(|(h, _, _)| *h).collect::>(), - ["A", "B", "C", "D", "E", "F", "G"] - ); - - assert_eq!( - removed.map(|(h, _, _)| h).collect::>(), - ["H", "L", "M", "O", "I", "J", "K"] - ); - } - - #[test] - fn find_node_index_works() { - let (tree, is_descendent_of) = test_fork_tree(); - - let path = tree - .find_node_index_where(&"D", &4, &is_descendent_of, &|_| true) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 0, 0]); - - let path = tree - .find_node_index_where(&"O", &5, &is_descendent_of, &|_| true) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 1, 0, 0]); - - let path = tree - .find_node_index_where(&"N", &6, &is_descendent_of, &|_| true) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 1, 0, 0, 0]); - } - - #[test] - fn find_node_index_with_predicate_works() { - let is_descendent_of = |parent: &char, child: &char| match *parent { - 'A' => Ok(['B', 'C', 'D', 'E', 'F'].contains(child)), - 'B' => Ok(['C', 'D'].contains(child)), - 'C' => Ok(['D'].contains(child)), - 'E' => Ok(['F'].contains(child)), - 'D' | 'F' => Ok(false), - _ => Err(TestError), - }; - - // A(t) --- B(f) --- C(t) --- D(f) - // \-- E(t) --- F(f) - let mut tree: ForkTree = ForkTree::new(); - tree.import('A', 1, true, &is_descendent_of).unwrap(); - tree.import('B', 2, false, &is_descendent_of).unwrap(); - tree.import('C', 3, true, &is_descendent_of).unwrap(); - tree.import('D', 4, false, &is_descendent_of).unwrap(); - - tree.import('E', 2, true, &is_descendent_of).unwrap(); - tree.import('F', 3, false, &is_descendent_of).unwrap(); - - let path = tree - .find_node_index_where(&'D', &4, &is_descendent_of, &|&value| !value) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 0]); - - let path = tree - .find_node_index_where(&'D', &4, &is_descendent_of, &|&value| value) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 0, 0]); - - let path = tree - .find_node_index_where(&'F', &3, &is_descendent_of, &|&value| !value) - .unwrap(); - assert_eq!(path, None); - - let path = tree - .find_node_index_where(&'F', &3, &is_descendent_of, &|&value| value) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 1]); - } - - #[test] - fn find_node_works() { - let (tree, is_descendent_of) = test_fork_tree(); - - let node = tree.find_node_where(&"B", &2, &is_descendent_of, &|_| true).unwrap().unwrap(); - assert_eq!((node.hash, node.number), ("A", 1)); - - let node = tree.find_node_where(&"D", &4, &is_descendent_of, &|_| true).unwrap().unwrap(); - assert_eq!((node.hash, node.number), ("C", 3)); - - let node = tree.find_node_where(&"O", &5, &is_descendent_of, &|_| true).unwrap().unwrap(); - assert_eq!((node.hash, node.number), ("L", 4)); - - let node = tree.find_node_where(&"N", &6, &is_descendent_of, &|_| true).unwrap().unwrap(); - assert_eq!((node.hash, node.number), ("M", 5)); - } - - #[test] - fn post_order_traversal_requirement() { - let (mut tree, is_descendent_of) = test_fork_tree(); - - // Test for the post-order DFS traversal requirement as specified by the - // `find_node_index_where` and `import` comments. - let is_descendent_of_for_post_order = |parent: &&str, child: &&str| match *parent { - "A" => Err(TestError), - "K" if *child == "Z" => Ok(true), - _ => is_descendent_of(parent, child), - }; - - // Post order traversal requirement for `find_node_index_where` - let path = tree - .find_node_index_where(&"N", &6, &is_descendent_of_for_post_order, &|_| true) - .unwrap() - .unwrap(); - assert_eq!(path, [0, 1, 0, 0, 0]); - - // Post order traversal requirement for `import` - let res = tree.import(&"Z", 100, (), &is_descendent_of_for_post_order); - assert_eq!(res, Ok(false)); - assert_eq!( - tree.iter().map(|node| *node.0).collect::>(), - vec!["A", "B", "C", "D", "E", "F", "H", "L", "M", "O", "I", "G", "J", "K", "Z"], - ); - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/Cargo.toml b/bitacross-worker/sidechain/peer-fetch/Cargo.toml deleted file mode 100644 index 66c10302c9..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/Cargo.toml +++ /dev/null @@ -1,37 +0,0 @@ -[package] -name = "its-peer-fetch" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -# crates.io -async-trait = { version = "0.1.50" } -jsonrpsee = { version = "0.2.0", features = ["client", "ws-server", "macros"] } -log = { version = "0.4" } -serde = "1.0" -serde_json = "1.0" -thiserror = { version = "1.0" } - -# local -itc-rpc-client = { path = "../../core/rpc-client" } -itp-node-api = { path = "../../core-primitives/node-api" } -itp-types = { path = "../../core-primitives/types" } -its-primitives = { path = "../primitives" } -its-rpc-handler = { path = "../rpc-handler" } -its-storage = { path = "../storage" } - -[dev-dependencies] -# crates.io -anyhow = "1.0.40" -tokio = { version = "1.6.1", features = ["full"] } -# local -itp-node-api = { path = "../../core-primitives/node-api", features = ["mocks"] } -itp-test = { path = "../../core-primitives/test" } -its-storage = { path = "../storage", features = ["mocks"] } -its-test = { path = "../test" } - -[features] -default = ["std"] -std = [] -mocks = [] diff --git a/bitacross-worker/sidechain/peer-fetch/src/block_fetch_client.rs b/bitacross-worker/sidechain/peer-fetch/src/block_fetch_client.rs deleted file mode 100644 index 320d916d7a..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/block_fetch_client.rs +++ /dev/null @@ -1,141 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, untrusted_peer_fetch::FetchUntrustedPeers, FetchBlocksFromPeer}; -use async_trait::async_trait; -use its_primitives::{ - traits::SignedBlock as SignedBlockTrait, - types::{BlockHash, ShardIdentifier}, -}; -use its_rpc_handler::constants::RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER; -use jsonrpsee::{ - types::to_json_value, - ws_client::{traits::Client, WsClientBuilder}, -}; -use log::info; -use serde::de::DeserializeOwned; -use std::marker::PhantomData; - -/// Sidechain block fetcher implementation. -/// -/// Fetches block from a peer with an RPC request. -pub struct BlockFetcher { - peer_fetcher: PeerFetcher, - _phantom: PhantomData, -} - -impl BlockFetcher -where - SignedBlock: SignedBlockTrait + DeserializeOwned, - PeerFetcher: FetchUntrustedPeers + Send + Sync, -{ - pub fn new(peer_fetcher: PeerFetcher) -> Self { - BlockFetcher { peer_fetcher, _phantom: Default::default() } - } -} - -#[async_trait] -impl FetchBlocksFromPeer for BlockFetcher -where - SignedBlock: SignedBlockTrait + DeserializeOwned, - PeerFetcher: FetchUntrustedPeers + Send + Sync, -{ - type SignedBlockType = SignedBlock; - - async fn fetch_blocks_from_peer( - &self, - last_imported_block_hash: BlockHash, - maybe_until_block_hash: Option, - shard_identifier: ShardIdentifier, - ) -> Result> { - let sync_source_rpc_url = - self.peer_fetcher.get_untrusted_peer_url_of_shard(&shard_identifier)?; - - let rpc_parameters = vec![to_json_value(( - last_imported_block_hash, - maybe_until_block_hash, - shard_identifier, - ))?]; - - info!("Got untrusted url for peer block fetching: {}", sync_source_rpc_url); - - let client = WsClientBuilder::default().build(sync_source_rpc_url.as_str()).await?; - - info!("Sending fetch blocks from peer request"); - - client - .request::>( - RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER, - rpc_parameters.into(), - ) - .await - .map_err(|e| e.into()) - } -} - -#[cfg(test)] -mod tests { - - use super::*; - use crate::{ - block_fetch_server::BlockFetchServerModuleBuilder, - mocks::untrusted_peer_fetch_mock::UntrustedPeerFetcherMock, - }; - use its_primitives::types::block::SignedBlock; - use its_storage::fetch_blocks_mock::FetchBlocksMock; - use its_test::sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}; - use jsonrpsee::ws_server::WsServerBuilder; - use std::{net::SocketAddr, sync::Arc}; - - async fn run_server( - blocks: Vec, - web_socket_url: &str, - ) -> anyhow::Result { - let mut server = WsServerBuilder::default().build(web_socket_url).await?; - - let storage_block_fetcher = Arc::new(FetchBlocksMock::default().with_blocks(blocks)); - let module = BlockFetchServerModuleBuilder::new(storage_block_fetcher).build().unwrap(); - - server.register_module(module).unwrap(); - - let socket_addr = server.local_addr()?; - tokio::spawn(async move { server.start().await }); - Ok(socket_addr) - } - - #[tokio::test] - async fn fetch_blocks_without_bounds_from_peer_works() { - const W1_URL: &str = "127.0.0.1:2233"; - - let blocks_to_fetch = vec![ - SidechainBlockBuilder::random().build_signed(), - SidechainBlockBuilder::random().build_signed(), - ]; - run_server(blocks_to_fetch.clone(), W1_URL).await.unwrap(); - - let peer_fetch_mock = UntrustedPeerFetcherMock::new(format!("ws://{}", W1_URL)); - - let peer_fetcher_client = BlockFetcher::::new(peer_fetch_mock); - - let blocks_fetched = peer_fetcher_client - .fetch_blocks_from_peer(BlockHash::default(), None, ShardIdentifier::default()) - .await - .unwrap(); - - assert_eq!(blocks_to_fetch, blocks_fetched); - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/block_fetch_server.rs b/bitacross-worker/sidechain/peer-fetch/src/block_fetch_server.rs deleted file mode 100644 index 592153f6eb..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/block_fetch_server.rs +++ /dev/null @@ -1,103 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::Result; -use its_primitives::types::{BlockHash, BlockNumber, ShardIdentifier, SignedBlock}; -use its_rpc_handler::constants::{ - RPC_METHOD_NAME_BLOCK_HASH, RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER, - RPC_METHOD_NAME_LATEST_BLOCK, -}; -use its_storage::interface::FetchBlocks; -use jsonrpsee::{types::error::CallError, RpcModule}; -use log::*; -use std::sync::Arc; - -/// RPC server module builder for fetching sidechain blocks from peers. -pub struct BlockFetchServerModuleBuilder { - sidechain_block_fetcher: Arc, -} - -impl BlockFetchServerModuleBuilder -where - // Have to use the concrete `SignedBlock` type, because the ShardIdentifier type - // does not have the Serialize/Deserialize trait bound. - FetchBlocksFromStorage: FetchBlocks + Send + Sync + 'static, -{ - pub fn new(sidechain_block_fetcher: Arc) -> Self { - BlockFetchServerModuleBuilder { sidechain_block_fetcher } - } - - pub fn build(self) -> Result>> { - let mut fetch_sidechain_blocks_module = RpcModule::new(self.sidechain_block_fetcher); - fetch_sidechain_blocks_module.register_method( - RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER, - |params, sidechain_block_fetcher| { - debug!("{}: {:?}", RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER, params); - - let (from_block_hash, maybe_until_block_hash, shard_identifier) = - params.one::<(BlockHash, Option, ShardIdentifier)>()?; - info!("Got request to fetch sidechain blocks from peer. Fetching sidechain blocks from storage \ - (last imported block hash: {:?}, until block hash: {:?}, shard: {}", - from_block_hash, maybe_until_block_hash, shard_identifier); - - match maybe_until_block_hash { - Some(until_block_hash) => sidechain_block_fetcher - .fetch_blocks_in_range( - &from_block_hash, - &until_block_hash, - &shard_identifier, - ) - .map_err(|e| { - error!("Failed to fetch sidechain blocks from storage: {:?}", e); - CallError::Failed(e.into()) - }), - None => sidechain_block_fetcher - .fetch_all_blocks_after(&from_block_hash, &shard_identifier) - .map_err(|e| { - error!("Failed to fetch sidechain blocks from storage: {:?}", e); - CallError::Failed(e.into()) - }), - } - }, - )?; - - fetch_sidechain_blocks_module.register_method( - RPC_METHOD_NAME_LATEST_BLOCK, - |params, sidechain_block_fetcher| { - debug!("{}: {:?}", RPC_METHOD_NAME_LATEST_BLOCK, params); - let shard = params.parse::()?; - match sidechain_block_fetcher.latest_block(&shard) { - None => Ok(None), - Some(e) => Ok(Some(e)), - } - }, - )?; - - fetch_sidechain_blocks_module.register_method( - RPC_METHOD_NAME_BLOCK_HASH, - |params, sidechain_block_fetcher| { - debug!("{}: {:?}", RPC_METHOD_NAME_BLOCK_HASH, params); - let (block_number, shard) = params.parse::<(BlockNumber, ShardIdentifier)>()?; - match sidechain_block_fetcher.block_hash(block_number, &shard) { - None => Ok(None), - Some(e) => Ok(Some(e)), - } - }, - )?; - Ok(fetch_sidechain_blocks_module) - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/error.rs b/bitacross-worker/sidechain/peer-fetch/src/error.rs deleted file mode 100644 index 569cd01a1d..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/error.rs +++ /dev/null @@ -1,44 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Sidechain peer fetch error. - -pub type Result = core::result::Result; - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("RPC client error: {0}")] - RpcClient(#[from] itc_rpc_client::error::Error), - #[error("Node API extensions error: {0:?}")] - NodeApiExtensions(itp_node_api::api_client::ApiClientError), - #[error("Node API factory error: {0}")] - NodeApiFactory(#[from] itp_node_api::node_api_factory::NodeApiFactoryError), - #[error("Serialization error: {0}")] - Serialization(#[from] serde_json::Error), - #[error("JSON RPC error: {0}")] - JsonRpc(#[from] jsonrpsee::types::Error), - #[error("Could not find any peers on-chain for shard: {0:?}")] - NoPeerFoundForShard(its_primitives::types::ShardIdentifier), - #[error(transparent)] - Other(#[from] Box), -} - -impl From for Error { - fn from(error: itp_node_api::api_client::ApiClientError) -> Self { - Error::NodeApiExtensions(error) - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/lib.rs b/bitacross-worker/sidechain/peer-fetch/src/lib.rs deleted file mode 100644 index 5af3326970..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/lib.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] - -pub mod block_fetch_client; -pub mod block_fetch_server; -pub mod error; -pub mod untrusted_peer_fetch; - -#[cfg(feature = "mocks")] -pub mod mocks; - -use crate::error::Result; -use async_trait::async_trait; -use its_primitives::{ - traits::SignedBlock, - types::{BlockHash, ShardIdentifier}, -}; -use std::vec::Vec; - -/// Trait to fetch block from peer validateers. -/// -/// This is used by an outdated validateer to get the most recent state. -#[async_trait] -pub trait FetchBlocksFromPeer { - type SignedBlockType: SignedBlock; - - async fn fetch_blocks_from_peer( - &self, - last_imported_block_hash: BlockHash, - maybe_until_block_hash: Option, - shard_identifier: ShardIdentifier, - ) -> Result>; -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/mocks/fetch_blocks_from_peer_mock.rs b/bitacross-worker/sidechain/peer-fetch/src/mocks/fetch_blocks_from_peer_mock.rs deleted file mode 100644 index 09f9bb92fc..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/mocks/fetch_blocks_from_peer_mock.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{FetchBlocksFromPeer, Result}; -use async_trait::async_trait; -use its_primitives::{ - traits::SignedBlock as SignedBlockTrait, - types::{BlockHash, ShardIdentifier}, -}; -use std::collections::HashMap; - -pub struct FetchBlocksFromPeerMock { - signed_blocks_map: HashMap>, -} - -impl FetchBlocksFromPeerMock { - pub fn with_signed_blocks( - mut self, - blocks_map: HashMap>, - ) -> Self { - self.signed_blocks_map = blocks_map; - self - } -} - -impl Default for FetchBlocksFromPeerMock { - fn default() -> Self { - FetchBlocksFromPeerMock { signed_blocks_map: HashMap::new() } - } -} - -#[async_trait] -impl FetchBlocksFromPeer for FetchBlocksFromPeerMock -where - SignedBlock: SignedBlockTrait, -{ - type SignedBlockType = SignedBlock; - - async fn fetch_blocks_from_peer( - &self, - _last_imported_block_hash: BlockHash, - _maybe_until_block_hash: Option, - shard_identifier: ShardIdentifier, - ) -> Result> { - Ok(self.signed_blocks_map.get(&shard_identifier).cloned().unwrap_or_default()) - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/mocks/mod.rs b/bitacross-worker/sidechain/peer-fetch/src/mocks/mod.rs deleted file mode 100644 index 392f8e9b82..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/mocks/mod.rs +++ /dev/null @@ -1,19 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod fetch_blocks_from_peer_mock; -pub mod untrusted_peer_fetch_mock; diff --git a/bitacross-worker/sidechain/peer-fetch/src/mocks/untrusted_peer_fetch_mock.rs b/bitacross-worker/sidechain/peer-fetch/src/mocks/untrusted_peer_fetch_mock.rs deleted file mode 100644 index 8b37b69e00..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/mocks/untrusted_peer_fetch_mock.rs +++ /dev/null @@ -1,35 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, untrusted_peer_fetch::FetchUntrustedPeers}; -use its_primitives::types::ShardIdentifier; - -pub struct UntrustedPeerFetcherMock { - url: String, -} - -impl UntrustedPeerFetcherMock { - pub fn new(url: String) -> Self { - UntrustedPeerFetcherMock { url } - } -} - -impl FetchUntrustedPeers for UntrustedPeerFetcherMock { - fn get_untrusted_peer_url_of_shard(&self, _shard: &ShardIdentifier) -> Result { - Ok(self.url.clone()) - } -} diff --git a/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs b/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs deleted file mode 100644 index b87b0ca5c1..0000000000 --- a/bitacross-worker/sidechain/peer-fetch/src/untrusted_peer_fetch.rs +++ /dev/null @@ -1,61 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, Result}; -use itc_rpc_client::direct_client::{DirectApi, DirectClient as DirectWorkerApi}; -use itp_node_api::{api_client::PalletTeebagApi, node_api_factory::CreateNodeApi}; -use itp_types::WorkerType; -use its_primitives::types::ShardIdentifier; -use std::sync::Arc; - -/// Trait to fetch untrusted peer servers. -pub trait FetchUntrustedPeers { - fn get_untrusted_peer_url_of_shard(&self, shard: &ShardIdentifier) -> Result; -} - -/// Fetches the untrusted peer servers -/// FIXME: Should probably be combined with the peer fetch in -/// service/src/worker.rs -pub struct UntrustedPeerFetcher { - node_api_factory: Arc, -} - -impl UntrustedPeerFetcher -where - NodeApiFactory: CreateNodeApi + Send + Sync, -{ - pub fn new(node_api: Arc) -> Self { - UntrustedPeerFetcher { node_api_factory: node_api } - } -} - -impl FetchUntrustedPeers for UntrustedPeerFetcher -where - NodeApiFactory: CreateNodeApi + Send + Sync, -{ - fn get_untrusted_peer_url_of_shard(&self, shard: &ShardIdentifier) -> Result { - let node_api = self.node_api_factory.create_api()?; - - let validateer = node_api - .primary_enclave_for_shard(WorkerType::BitAcross, shard, None)? - .ok_or_else(|| Error::NoPeerFoundForShard(*shard))?; - - let trusted_worker_client = - DirectWorkerApi::new(String::from_utf8_lossy(validateer.url.as_slice()).to_string()); - Ok(trusted_worker_client.get_untrusted_worker_url()?) - } -} diff --git a/bitacross-worker/sidechain/primitives/Cargo.toml b/bitacross-worker/sidechain/primitives/Cargo.toml deleted file mode 100644 index 548f4d5a18..0000000000 --- a/bitacross-worker/sidechain/primitives/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "its-primitives" -version = "0.1.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -homepage = 'https://litentry.com/' -repository = 'https://github.com/litentry/litentry-parachain' -license = "Apache-2.0" -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "full"] } -itp-types = { path = "../../core-primitives/types", default-features = false } -scale-info = { version = "2.4.0", default-features = false, features = ["derive"] } -serde = { version = "1.0.13", default-features = false } - -# substrate dependencies -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - - -[features] -default = ["std", "full_crypto"] -full_crypto = [ - "sp-core/full_crypto", -] -std = [ - "codec/std", - "scale-info/std", - "serde/std", - "itp-types/std", - # substrate - "sp-core/std", - "sp-runtime/std", - "sp-std/std", -] diff --git a/bitacross-worker/sidechain/primitives/src/lib.rs b/bitacross-worker/sidechain/primitives/src/lib.rs deleted file mode 100644 index 708d9a7942..0000000000 --- a/bitacross-worker/sidechain/primitives/src/lib.rs +++ /dev/null @@ -1,21 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] - -pub mod traits; -pub mod types; diff --git a/bitacross-worker/sidechain/primitives/src/traits/mod.rs b/bitacross-worker/sidechain/primitives/src/traits/mod.rs deleted file mode 100644 index 06e1e2d393..0000000000 --- a/bitacross-worker/sidechain/primitives/src/traits/mod.rs +++ /dev/null @@ -1,176 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Some basic abstractions used in sidechain -//! -//! Todo: This crate should be more generic and supply blanket implementations for -//! some generic structs. - -use codec::{Decode, Encode}; -use sp_core::{crypto::Public, H256}; -use sp_runtime::traits::{BlakeTwo256, Hash, Member}; -use sp_std::{fmt::Debug, prelude::*}; - -pub trait Header: Encode + Decode + Clone { - /// Identifier for the shards. - type ShardIdentifier: Encode + Decode + sp_std::hash::Hash + Copy + Member + Into; - - /// Get block number. - fn block_number(&self) -> u64; - /// get parent hash of block - fn parent_hash(&self) -> H256; - /// get shard id of block - fn shard_id(&self) -> Self::ShardIdentifier; - /// get hash of the block's payload - fn block_data_hash(&self) -> H256; - - /// get the `blake2_256` hash of the header. - fn hash(&self) -> H256 { - self.using_encoded(BlakeTwo256::hash) - } - - fn next_finalization_block_number(&self) -> u64; - - fn new( - block_number: u64, - parent_hash: H256, - shard: Self::ShardIdentifier, - block_data_hash: H256, - next_finalization_block_number: u64, - ) -> Self; -} - -pub trait BlockData: Encode + Decode + Send + Sync + Debug + Clone { - /// Public key type of the block author - type Public: Public; - - /// get timestamp of block - fn timestamp(&self) -> u64; - /// get layer one head of block - fn layer_one_head(&self) -> H256; - /// get author of block - fn block_author(&self) -> &Self::Public; - /// get reference of extrinsics of block - fn signed_top_hashes(&self) -> &[H256]; - /// get encrypted payload - fn encrypted_state_diff(&self) -> &Vec; - /// get the `blake2_256` hash of the block - fn hash(&self) -> H256 { - self.using_encoded(BlakeTwo256::hash) - } - - fn new( - author: Self::Public, - layer_one_head: H256, - signed_top_hashes: Vec, - encrypted_payload: Vec, - timestamp: u64, - ) -> Self; -} - -/// Abstraction around a sidechain block. -pub trait Block: Encode + Decode + Send + Sync + Debug + Clone { - /// Sidechain block header type. - type HeaderType: Header; - - /// Sidechain block data type. - type BlockDataType: BlockData; - - /// Public key type of the block author - type Public: Public; - - /// get the `blake2_256` hash of the block - fn hash(&self) -> H256 { - self.header().hash() - } - - /// Get header of the block. - fn header(&self) -> &Self::HeaderType; - - /// Get header of the block. - fn block_data(&self) -> &Self::BlockDataType; - - fn new(header: Self::HeaderType, block_data: Self::BlockDataType) -> Self; -} - -/// ShardIdentifier for a [`SignedBlock`] -pub type ShardIdentifierFor = -<<::Block as Block>::HeaderType as Header>::ShardIdentifier; - -/// A block and it's corresponding signature by the [`Block`] author. -pub trait SignedBlock: Encode + Decode + Send + Sync + Debug + Clone { - /// The block type of the [`SignedBlock`] - type Block: Block; - - /// Public key type of the signer and the block author - type Public: Public; - - /// Signature type of the [`SignedBlock`]'s signature - type Signature; - - /// create a new block instance - fn new(block: Self::Block, signer: Self::Signature) -> Self; - - /// get block reference - fn block(&self) -> &Self::Block; - - /// get signature reference - fn signature(&self) -> &Self::Signature; - - /// get `blake2_256` hash of block - fn hash(&self) -> H256 { - self.block().hash() - } - - /// Verify the signature of a [`Block`] - fn verify_signature(&self) -> bool; -} - -#[cfg(feature = "full_crypto")] -pub use crypto::*; - -#[cfg(feature = "full_crypto")] -mod crypto { - use super::*; - use sp_core::Pair; - - /// Provide signing logic blanket implementations for all block types satisfying the trait bounds. - pub trait SignBlock< - SidechainBlock: Block, - SignedSidechainBlock: SignedBlock, - > - { - fn sign_block(self, signer: &P) -> SignedSidechainBlock - where - ::Signature: From<

::Signature>; - } - - impl SignBlock - for SidechainBlock - where - SidechainBlock: Block, - SignedSidechainBlock: SignedBlock, - { - fn sign_block(self, signer: &P) -> SignedSidechainBlock - where - ::Signature: From<

::Signature>, - { - let signature = self.using_encoded(|b| signer.sign(b)).into(); - SignedSidechainBlock::new(self, signature) - } - } -} diff --git a/bitacross-worker/sidechain/primitives/src/types/block.rs b/bitacross-worker/sidechain/primitives/src/types/block.rs deleted file mode 100644 index 8e7902d62d..0000000000 --- a/bitacross-worker/sidechain/primitives/src/types/block.rs +++ /dev/null @@ -1,159 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - traits::{Block as BlockTrait, SignedBlock as SignedBlockTrait}, - types::{block_data::BlockData, header::SidechainHeader as Header}, -}; -use codec::{Decode, Encode}; -use sp_core::{ed25519, H256}; -use sp_runtime::{traits::Verify, MultiSignature}; - -pub type BlockHash = H256; -pub type BlockNumber = u64; -pub type ShardIdentifier = H256; -pub type Timestamp = u64; - -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; - -//FIXME: Should use blocknumber from sgxruntime -// Problem: sgxruntime only with sgx, no std enviornment -// but block.rs should be available in std? -//use sgx_runtime::BlockNumber; - -pub type Signature = MultiSignature; - -/// signed version of block to verify block origin -#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct SignedBlock { - /// Plain sidechain block without author signature. - pub block: Block, - /// Block author signature. - pub signature: Signature, -} - -/// Simplified block structure for relay chain submission as an extrinsic. -#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct Block { - /// Sidechain Header - pub header: Header, - - /// Sidechain Block data - pub block_data: BlockData, -} - -impl BlockTrait for Block { - type HeaderType = Header; - - type BlockDataType = BlockData; - - type Public = ed25519::Public; - - fn header(&self) -> &Self::HeaderType { - &self.header - } - - fn block_data(&self) -> &Self::BlockDataType { - &self.block_data - } - - fn new(header: Self::HeaderType, block_data: Self::BlockDataType) -> Self { - Self { header, block_data } - } -} - -impl SignedBlockTrait for SignedBlock { - type Block = Block; - - type Public = ed25519::Public; - - type Signature = Signature; - - fn new(block: Self::Block, signature: Self::Signature) -> Self { - Self { block, signature } - } - - /// get block reference - fn block(&self) -> &Self::Block { - &self.block - } - - /// get signature reference - fn signature(&self) -> &Signature { - &self.signature - } - - /// Verifies the signature of a Block - fn verify_signature(&self) -> bool { - self.block.using_encoded(|p| { - self.signature.verify(p, &self.block.block_data().block_author.into()) - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::traits::{Block as BlockT, BlockData, Header, SignBlock}; - use sp_core::Pair; - use std::time::{SystemTime, UNIX_EPOCH}; - - /// gets the timestamp of the block as seconds since unix epoch - fn timestamp_now() -> Timestamp { - SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_millis() as Timestamp - } - - fn test_block() -> Block { - let header = Header::new(0, H256::random(), H256::random(), Default::default(), 1); - let block_data = BlockData::new( - ed25519::Pair::from_string("//Alice", None).unwrap().public().into(), - H256::random(), - Default::default(), - Default::default(), - timestamp_now(), - ); - - Block::new(header, block_data) - } - - #[test] - fn signing_works() { - let block = test_block(); - let signer = ed25519::Pair::from_string("//Alice", None).unwrap(); - - let signature: Signature = - Signature::Ed25519(signer.sign(block.encode().as_slice().into())); - let signed_block: SignedBlock = block.clone().sign_block(&signer); - - assert_eq!(signed_block.block(), &block); - assert_eq!(signed_block.signature(), &signature); - assert!(signed_block.verify_signature()); - } - - #[test] - fn tampered_block_verify_signature_fails() { - let signer = ed25519::Pair::from_string("//Alice", None).unwrap(); - - let mut signed_block: SignedBlock = test_block().sign_block(&signer); - signed_block.block.header.block_number = 1; - - assert!(!signed_block.verify_signature()); - } -} diff --git a/bitacross-worker/sidechain/primitives/src/types/block_data.rs b/bitacross-worker/sidechain/primitives/src/types/block_data.rs deleted file mode 100644 index a48d4148e4..0000000000 --- a/bitacross-worker/sidechain/primitives/src/types/block_data.rs +++ /dev/null @@ -1,82 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::traits::BlockData as BlockDataTrait; -use codec::{Decode, Encode}; -use sp_core::{ed25519, H256}; -use sp_std::vec::Vec; - -pub type Timestamp = u64; - -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; - -#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct BlockData { - pub timestamp: u64, - /// Parentchain header this block is based on. - pub layer_one_head: H256, - /// Must be registered on layer one as an enclave for the respective shard. - pub block_author: ed25519::Public, - /// Hashes of signed trusted operations. - pub signed_top_hashes: Vec, - /// Encrypted state payload. - pub encrypted_state_diff: Vec, -} - -impl BlockDataTrait for BlockData { - type Public = ed25519::Public; - - /// Get timestamp of block. - fn timestamp(&self) -> Timestamp { - self.timestamp - } - /// Get layer one head of block. - fn layer_one_head(&self) -> H256 { - self.layer_one_head - } - /// Get author of block. - fn block_author(&self) -> &Self::Public { - &self.block_author - } - /// Get reference of extrinisics of block. - fn signed_top_hashes(&self) -> &[H256] { - &self.signed_top_hashes - } - /// Get encrypted payload. - fn encrypted_state_diff(&self) -> &Vec { - &self.encrypted_state_diff - } - /// Constructs block data. - fn new( - block_author: Self::Public, - layer_one_head: H256, - signed_top_hashes: Vec, - encrypted_state_diff: Vec, - timestamp: Timestamp, - ) -> BlockData { - // create block - BlockData { - timestamp, - layer_one_head, - signed_top_hashes, - block_author, - encrypted_state_diff, - } - } -} diff --git a/bitacross-worker/sidechain/primitives/src/types/header.rs b/bitacross-worker/sidechain/primitives/src/types/header.rs deleted file mode 100644 index 962917f534..0000000000 --- a/bitacross-worker/sidechain/primitives/src/types/header.rs +++ /dev/null @@ -1,91 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//!Primitives for the sidechain -use crate::traits::Header as HeaderTrait; -use codec::{Decode, Encode}; -use scale_info::TypeInfo; -use sp_core::H256; -use sp_runtime::traits::{BlakeTwo256, Hash}; -use sp_std::prelude::*; - -#[cfg(feature = "std")] -use serde::{Deserialize, Serialize}; - -pub use itp_types::ShardIdentifier; - -#[derive(PartialEq, Eq, Clone, Encode, Decode, Debug, Copy, Default, TypeInfo)] -#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] -pub struct SidechainHeader { - /// The parent hash. - pub parent_hash: H256, - - /// The block number. - pub block_number: u64, - - /// The Shard id. - pub shard_id: ShardIdentifier, - - /// The payload hash. - pub block_data_hash: H256, - - /// The latest finalized block number - pub next_finalization_block_number: u64, -} - -impl SidechainHeader { - /// get the `blake2_256` hash of the header. - pub fn hash(&self) -> H256 { - self.using_encoded(BlakeTwo256::hash) - } -} - -impl HeaderTrait for SidechainHeader { - type ShardIdentifier = H256; - - fn block_number(&self) -> u64 { - self.block_number - } - fn parent_hash(&self) -> H256 { - self.parent_hash - } - fn shard_id(&self) -> Self::ShardIdentifier { - self.shard_id - } - fn block_data_hash(&self) -> H256 { - self.block_data_hash - } - fn next_finalization_block_number(&self) -> u64 { - self.next_finalization_block_number - } - - fn new( - block_number: u64, - parent_hash: H256, - shard: Self::ShardIdentifier, - block_data_hash: H256, - next_finalization_block_number: u64, - ) -> SidechainHeader { - SidechainHeader { - block_number, - parent_hash, - shard_id: shard, - block_data_hash, - next_finalization_block_number, - } - } -} diff --git a/bitacross-worker/sidechain/primitives/src/types/mod.rs b/bitacross-worker/sidechain/primitives/src/types/mod.rs deleted file mode 100644 index 2056953387..0000000000 --- a/bitacross-worker/sidechain/primitives/src/types/mod.rs +++ /dev/null @@ -1,22 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -pub mod block; -pub mod block_data; -pub mod header; - -pub use block::*; diff --git a/bitacross-worker/sidechain/rpc-handler/Cargo.toml b/bitacross-worker/sidechain/rpc-handler/Cargo.toml deleted file mode 100644 index 8ac4d88b9e..0000000000 --- a/bitacross-worker/sidechain/rpc-handler/Cargo.toml +++ /dev/null @@ -1,62 +0,0 @@ -[package] -name = "its-rpc-handler" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -# sgx dependencies -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } - -# local dependencies -itp-rpc = { path = "../../core-primitives/rpc", default-features = false } -itp-stf-primitives = { path = "../../core-primitives/stf-primitives", default-features = false } -itp-top-pool-author = { path = "../../core-primitives/top-pool-author", default-features = false } -itp-types = { path = "../../core-primitives/types", default-features = false } -itp-utils = { path = "../../core-primitives/utils", default-features = false } -its-primitives = { path = "../primitives", default-features = false } - -litentry-primitives = { path = "../../litentry/primitives", default-features = false } - -# sgx enabled external libraries -futures_sgx = { package = "futures", git = "https://github.com/mesalock-linux/futures-rs-sgx", optional = true } -jsonrpc-core_sgx = { package = "jsonrpc-core", git = "https://github.com/scs/jsonrpc", branch = "no_std_v18", default-features = false, optional = true } -rust-base58_sgx = { package = "rust-base58", rev = "sgx_1.1.3", git = "https://github.com/mesalock-linux/rust-base58-sgx", optional = true, default-features = false, features = ["mesalock_sgx"] } - -# std compatible external libraries (make sure these versions match with the sgx-enabled ones above) -futures = { version = "0.3.8", optional = true } -jsonrpc-core = { version = "18", optional = true } -rust-base58 = { package = "rust-base58", version = "0.0.4", optional = true } - -# no-std compatible libraries -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } -log = { version = "0.4", default-features = false } -sp-core = { default-features = false, features = ["full_crypto"], git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# bitacross -bc-task-sender = { path = "../../bitacross/core/bc-task-sender", default-features = false } - -[features] -default = ["std"] -std = [ - "futures", - "itp-rpc/std", - "itp-stf-primitives/std", - "itp-top-pool-author/std", - "itp-types/std", - "its-primitives/std", - "litentry-primitives/std", - "jsonrpc-core", - "log/std", - "rust-base58", - "bc-task-sender/std", -] -sgx = [ - "futures_sgx", - "sgx_tstd", - "itp-rpc/sgx", - "itp-top-pool-author/sgx", - "jsonrpc-core_sgx", - "rust-base58_sgx", - "bc-task-sender/sgx", -] diff --git a/bitacross-worker/sidechain/rpc-handler/src/constants.rs b/bitacross-worker/sidechain/rpc-handler/src/constants.rs deleted file mode 100644 index bff9ea019b..0000000000 --- a/bitacross-worker/sidechain/rpc-handler/src/constants.rs +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Sidechain constants - -// RPC method names. -pub const RPC_METHOD_NAME_IMPORT_BLOCKS: &str = "sidechain_importBlock"; -pub const RPC_METHOD_NAME_FETCH_BLOCKS_FROM_PEER: &str = "sidechain_fetchBlocksFromPeer"; -pub const RPC_METHOD_NAME_LATEST_BLOCK: &str = "sidechain_latestBlock"; -pub const RPC_METHOD_NAME_BLOCK_HASH: &str = "sidechain_blockHash"; diff --git a/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs b/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs deleted file mode 100644 index 021f565c17..0000000000 --- a/bitacross-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs +++ /dev/null @@ -1,359 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; -use core::fmt::Debug; - -#[cfg(feature = "std")] -use rust_base58::base58::FromBase58; - -#[cfg(feature = "sgx")] -use base58::FromBase58; - -use bc_task_sender::{BitAcrossRequest, BitAcrossRequestSender}; -use codec::{Decode, Encode}; -use futures::channel::oneshot; -use itp_rpc::RpcReturnValue; -use itp_stf_primitives::types::AccountId; -use itp_top_pool_author::traits::AuthorApi; -use itp_types::{DirectRequestStatus, RsaRequest, ShardIdentifier, TrustedOperationStatus}; -use itp_utils::{FromHexPrefixed, ToHexPrefixed}; -use jsonrpc_core::{futures::executor, serde_json::json, Error as RpcError, IoHandler, Params}; -use litentry_primitives::AesRequest; -use log::*; -use std::{ - borrow::ToOwned, - format, - string::{String, ToString}, - sync::Arc, - vec, - vec::Vec, -}; - -type Hash = sp_core::H256; - -pub fn add_top_pool_direct_rpc_methods( - top_pool_author: Arc, - mut io_handler: IoHandler, -) -> IoHandler -where - R: AuthorApi + Send + Sync + 'static, - TCS: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, - G: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, -{ - let watch_author = top_pool_author.clone(); - - // Submit BitAcross Request - io_handler.add_method("bitacross_submitRequest", move |params: Params| { - debug!("worker_api_direct rpc was called: bitacross_submitRequest"); - async move { - let json_value = match request_bit_across_inner(params).await { - Ok(value) => value.to_hex(), - Err(error) => compute_hex_encoded_return_error(&error), - }; - Ok(json!(json_value)) - } - }); - io_handler.add_sync_method("author_submitAndWatchRsaRequest", move |params: Params| { - debug!("worker_api_direct rpc was called: author_submitAndWatchRsaRequest"); - let json_value = match author_submit_extrinsic_inner( - watch_author.clone(), - params, - Some("author_submitAndWatchBroadcastedRsaRequest".to_owned()), - ) { - Ok(hash_value) => RpcReturnValue { - do_watch: true, - value: vec![], - status: DirectRequestStatus::TrustedOperationStatus( - TrustedOperationStatus::Submitted, - hash_value, - ), - } - .to_hex(), - Err(error) => compute_hex_encoded_return_error(error.as_str()), - }; - Ok(json!(json_value)) - }); - - // author_submitAndWatchBroadcastedRsaRequest - let watch_author = top_pool_author.clone(); - io_handler.add_sync_method( - "author_submitAndWatchBroadcastedRsaRequest", - move |params: Params| { - let json_value = match author_submit_extrinsic_inner(watch_author.clone(), params, None) - { - Ok(hash_value) => { - RpcReturnValue { - do_watch: true, - value: vec![], - status: DirectRequestStatus::TrustedOperationStatus( - TrustedOperationStatus::Submitted, - hash_value, - ), - } - } - .to_hex(), - Err(error) => compute_hex_encoded_return_error(error.as_str()), - }; - Ok(json!(json_value)) - }, - ); - - // author_submitRsaRequest - let submit_author = top_pool_author.clone(); - io_handler.add_sync_method("author_submitRsaRequest", move |params: Params| { - debug!("worker_api_direct rpc was called: author_submitRsaRequest"); - let json_value = match author_submit_extrinsic_inner(submit_author.clone(), params, None) { - Ok(hash_value) => RpcReturnValue { - do_watch: false, - value: vec![], - status: DirectRequestStatus::TrustedOperationStatus( - TrustedOperationStatus::Submitted, - hash_value, - ), - } - .to_hex(), - Err(error) => compute_hex_encoded_return_error(error.as_str()), - }; - Ok(json!(json_value)) - }); - - // Litentry: a morphling of `author_submitAndWatchRsaRequest` - // a different name is used to highlight the request type - let watch_author = top_pool_author.clone(); - io_handler.add_sync_method("author_submitAndWatchAesRequest", move |params: Params| { - debug!("worker_api_direct rpc was called: author_submitAndWatchAesRequest"); - let json_value = match author_submit_aes_request_inner( - watch_author.clone(), - params, - Some("author_submitAndWatchBroadcastedAesRequest".to_owned()), - ) { - Ok(hash_value) => RpcReturnValue { - do_watch: true, - value: vec![], - status: DirectRequestStatus::TrustedOperationStatus( - TrustedOperationStatus::Submitted, - hash_value, - ), - } - .to_hex(), - Err(error) => compute_hex_encoded_return_error(error.as_str()), - }; - Ok(json!(json_value)) - }); - - let watch_author = top_pool_author.clone(); - io_handler.add_sync_method( - "author_submitAndWatchBroadcastedAesRequest", - move |params: Params| { - let json_value = - match author_submit_aes_request_inner(watch_author.clone(), params, None) { - Ok(hash_value) => RpcReturnValue { - do_watch: true, - value: vec![], - status: DirectRequestStatus::TrustedOperationStatus( - TrustedOperationStatus::Submitted, - hash_value, - ), - } - .to_hex(), - Err(error) => compute_hex_encoded_return_error(error.as_str()), - }; - Ok(json!(json_value)) - }, - ); - - // author_pendingExtrinsics - let pending_author = top_pool_author.clone(); - io_handler.add_sync_method("author_pendingExtrinsics", move |params: Params| { - debug!("worker_api_direct rpc was called: author_pendingExtrinsics"); - match params.parse::>() { - Ok(shards) => { - let mut retrieved_operations = vec![]; - for shard_base58 in shards.iter() { - let shard = match decode_shard_from_base58(shard_base58.as_str()) { - Ok(id) => id, - Err(msg) => { - let error_msg: String = - format!("Could not retrieve pending calls due to: {}", msg); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - if let Ok(vec_of_operations) = pending_author.pending_tops(shard) { - retrieved_operations.push(vec_of_operations); - } - } - let json_value = RpcReturnValue { - do_watch: false, - value: retrieved_operations.encode(), - status: DirectRequestStatus::Ok, - }; - Ok(json!(json_value.to_hex())) - }, - Err(e) => { - let error_msg: String = format!("Could not retrieve pending calls due to: {}", e); - Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - } - }); - - let pending_author = top_pool_author; - io_handler.add_sync_method("author_pendingTrustedCallsFor", move |params: Params| { - debug!("worker_api_direct rpc was called: author_pendingTrustedCallsFor"); - match params.parse::<(String, String)>() { - Ok((shard_base58, account_hex)) => { - let shard = match decode_shard_from_base58(shard_base58.as_str()) { - Ok(id) => id, - Err(msg) => { - let error_msg: String = - format!("Could not retrieve pending trusted calls due to: {}", msg); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - let account = match AccountId::from_hex(account_hex.as_str()) { - Ok(acc) => acc, - Err(msg) => { - let error_msg: String = - format!("Could not retrieve pending trusted calls due to: {:?}", msg); - return Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - }; - let trusted_calls = pending_author.get_pending_trusted_calls_for(shard, &account); - let json_value = RpcReturnValue { - do_watch: false, - value: trusted_calls.encode(), - status: DirectRequestStatus::Ok, - }; - Ok(json!(json_value.to_hex())) - }, - Err(e) => { - let error_msg: String = - format!("Could not retrieve pending trusted calls due to: {}", e); - Ok(json!(compute_hex_encoded_return_error(error_msg.as_str()))) - }, - } - }); - - io_handler -} - -// converts the rpc methods vector to a string and adds commas and brackets for readability -pub fn decode_shard_from_base58(shard_base58: &str) -> Result { - let shard_vec = match shard_base58.from_base58() { - Ok(vec) => vec, - Err(_) => return Err("Invalid base58 format of shard id".to_owned()), - }; - let shard = match ShardIdentifier::decode(&mut shard_vec.as_slice()) { - Ok(hash) => hash, - Err(_) => return Err("Shard ID is not of type H256".to_owned()), - }; - Ok(shard) -} - -fn compute_hex_encoded_return_error(error_msg: &str) -> String { - RpcReturnValue::from_error_message(error_msg).to_hex() -} - -// we expect our `params` to be "by-position array" -// see https://www.jsonrpc.org/specification#parameter_structures -fn get_request_payload(params: Params) -> Result { - let s_vec = params.parse::>().map_err(|e| format!("{}", e))?; - - let s = s_vec.get(0).ok_or_else(|| "Empty params".to_string())?; - debug!("Request payload: {}", s); - Ok(s.to_owned()) -} - -fn author_submit_extrinsic_inner( - author: Arc, - params: Params, - json_rpc_method: Option, -) -> Result -where - R: AuthorApi + Send + Sync + 'static, - TCS: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, - G: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, -{ - let payload = get_request_payload(params)?; - let request = RsaRequest::from_hex(&payload).map_err(|e| format!("{:?}", e))?; - - let response: Result = if let Some(method) = json_rpc_method { - executor::block_on(async { author.watch_and_broadcast_top(request, method).await }) - } else { - executor::block_on(async { author.watch_top(request).await }) - }; - - match &response { - Ok(h) => debug!("Trusted operation submitted successfully ({:?})", h), - Err(e) => warn!("Submitting trusted operation failed: {:?}", e), - } - - response.map_err(|e| format!("{:?}", e)) -} - -fn author_submit_aes_request_inner( - author: Arc, - params: Params, - json_rpc_method: Option, -) -> Result -where - R: AuthorApi + Send + Sync + 'static, - TCS: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, - G: PartialEq + Encode + Decode + Debug + Send + Sync + 'static, -{ - let payload = get_request_payload(params)?; - let request = AesRequest::from_hex(&payload).map_err(|e| format!("{:?}", e))?; - - let response: Result = if let Some(method) = json_rpc_method { - executor::block_on(async { author.watch_and_broadcast_top(request, method).await }) - } else { - executor::block_on(async { author.watch_top(request).await }) - }; - - match &response { - Ok(h) => debug!("AesRequest submitted successfully ({:?})", h), - Err(e) => warn!("Submitting AesRequest failed: {:?}", e), - } - - response.map_err(|e| format!("{:?}", e)) -} - -async fn request_bit_across_inner(params: Params) -> Result { - let payload = get_request_payload(params)?; - let request = AesRequest::from_hex(&payload) - .map_err(|e| format!("AesRequest construction error: {:?}", e))?; - - let bit_across_request_sender = BitAcrossRequestSender::new(); - let (sender, receiver) = oneshot::channel::, String>>(); - - bit_across_request_sender.send(BitAcrossRequest { sender, request })?; - - // we only expect one response, hence no loop - match receiver.await { - Ok(Ok(response)) => - Ok(RpcReturnValue { do_watch: false, value: response, status: DirectRequestStatus::Ok }), - Ok(Err(e)) => { - log::error!("Received error in jsonresponse: {:?} ", e); - Err(compute_hex_encoded_return_error(&e)) - }, - Err(_) => { - // This case will only happen if the sender has been dropped - Err(compute_hex_encoded_return_error("The sender has been dropped")) - }, - } -} diff --git a/bitacross-worker/sidechain/rpc-handler/src/import_block_api.rs b/bitacross-worker/sidechain/rpc-handler/src/import_block_api.rs deleted file mode 100644 index a34ff829ef..0000000000 --- a/bitacross-worker/sidechain/rpc-handler/src/import_block_api.rs +++ /dev/null @@ -1,126 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexport_prelude::*; - -use crate::constants::RPC_METHOD_NAME_IMPORT_BLOCKS; -use itp_utils::FromHexPrefixed; -use its_primitives::types::SignedBlock; -use jsonrpc_core::{IoHandler, Params, Value}; -use log::*; -use std::{borrow::ToOwned, fmt::Debug, string::String, vec::Vec}; - -pub fn add_import_block_rpc_method( - import_fn: ImportFn, - mut io_handler: IoHandler, -) -> IoHandler -where - ImportFn: Fn(SignedBlock) -> Result<(), Error> + Sync + Send + 'static, - Error: Debug, -{ - let sidechain_import_import_name: &str = RPC_METHOD_NAME_IMPORT_BLOCKS; - io_handler.add_sync_method(sidechain_import_import_name, move |sidechain_blocks: Params| { - debug!("{} rpc. Params: {:?}", RPC_METHOD_NAME_IMPORT_BLOCKS, sidechain_blocks); - - let hex_encoded_block_vec: Vec = sidechain_blocks.parse()?; - - let blocks = Vec::::from_hex(&hex_encoded_block_vec[0]).map_err(|_| { - jsonrpc_core::error::Error::invalid_params_with_details( - "Could not decode Vec", - hex_encoded_block_vec, - ) - })?; - - debug!("{}. Blocks: {:?}", RPC_METHOD_NAME_IMPORT_BLOCKS, blocks); - - for block in blocks { - info!("Add block {} to import queue", block.block.header.block_number); - let _ = import_fn(block).map_err(|e| { - let error = jsonrpc_core::error::Error::invalid_params_with_details( - "Failed to import Block.", - e, - ); - error!("{:?}", error); - }); - } - - Ok(Value::String("ok".to_owned())) - }); - - io_handler -} - -#[cfg(test)] -pub mod tests { - - use super::*; - - fn rpc_response(result: T) -> String { - format!(r#"{{"jsonrpc":"2.0","result":{},"id":1}}"#, result.to_string()) - } - - fn io_handler() -> IoHandler { - let io_handler = IoHandler::new(); - add_import_block_rpc_method::<_, String>(|_| Ok(()), io_handler) - } - - #[test] - pub fn sidechain_import_block_is_ok() { - let io = io_handler(); - let enclave_req = r#"{"jsonrpc":"2.0","method":"sidechain_importBlock","params":["0x04a7417cf9370af5ea5cf64f107aa49ebf320dbf10c6d0ef200ef7c5d57c9f4b956d000000000000007dba6b8e1f8f38f7f517dbd4a3eaeb27a97958d7a1d1541f69db5d24b3c48cd0dc376b08fcb44dca19a08a0445023a5f4bef80019b518296313e83fc105c669064000000000000005f08a5f98301000081bd02d7e1f8b6ab9a64fa8fdaa379fc1c9208bf0d341689c2342ce8a314e174768f40dfe0fadf2e7347f2ec83a541427a0931ce54ce7a4506184198c2e7aed3006d031b2cc662bbcd54ca1cc09f0021d956673c4905b07edf0b9f323d2078fc4d8cbaefe34353bc731f9a1ef14dfd6b58274a6efbbc6c2c4261d304b979305f501819df33452f2f276add2f3650b825c700abf23790a6787baf1cabb208633eb33fb66e987a99193fbd2c07374502dc0fdff6d7a5d462b2a9c0196711437aa6a30ce52ae6e4818a643df256c026b08d7ccca2de46f368630512073b271397719f34c9b8612c7f1707d06b45206da268f49b5b5159b3418093512700ecb67ccbc5bd9a1731a9c67372b39ec3761d12afb445a6c8580b97a090f4bb06ff70001bc44f7f91ada7f92f0064188d08c16594ddb4fd09f65bee5f4b3c92b80091d3fe5bc89f3fb95a96941563126a6379b806981dd7f225c7e3ac4e1ee0509de406"],"id":1}"#; - - let response_string = io.handle_request_sync(enclave_req).unwrap(); - - assert_eq!(response_string, rpc_response("\"ok\"")); - } - - #[test] - pub fn sidechain_import_block_returns_invalid_param_err() { - let io = io_handler(); - let enclave_req = - r#"{"jsonrpc":"2.0","method":"sidechain_importBlock","params":[4,214,133,100],"id":1}"#; - - let response_string = io.handle_request_sync(enclave_req).unwrap(); - - let err_msg = r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid params: invalid type: integer `4`, expected a string."},"id":1}"#; - assert_eq!(response_string, err_msg); - } - - #[test] - pub fn sidechain_import_block_returns_decode_err() { - let io = io_handler(); - let enclave_req = r#"{"jsonrpc":"2.0","method":"sidechain_importBlock","params":["SophisticatedInvalidParam"],"id":1}"#; - - let response_string = io.handle_request_sync(enclave_req).unwrap(); - - let err_msg = r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid parameters: Could not decode Vec","data":"[\"SophisticatedInvalidParam\"]"},"id":1}"#; - assert_eq!(response_string, err_msg); - } - - pub fn sidechain_import_block_returns_decode_err_for_valid_hex() { - let io = io_handler(); - - let enclave_req = - r#"{"jsonrpc":"2.0","method":"sidechain_importBlock","params": ["0x11"],"id":1}"#; - - let response_string = io.handle_request_sync(enclave_req).unwrap(); - - let err_msg = r#"{"jsonrpc":"2.0","error":{"code":-32602,"message":"Invalid parameters: Could not decode Vec","data":"[17]"},"id":1}"#; - assert_eq!(response_string, err_msg); - } -} diff --git a/bitacross-worker/sidechain/rpc-handler/src/lib.rs b/bitacross-worker/sidechain/rpc-handler/src/lib.rs deleted file mode 100644 index 5daf1d1f7e..0000000000 --- a/bitacross-worker/sidechain/rpc-handler/src/lib.rs +++ /dev/null @@ -1,38 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![feature(trait_alias)] -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -extern crate core; -#[cfg(all(not(feature = "std"), feature = "sgx"))] -extern crate sgx_tstd as std; - -// re-export module to properly feature gate sgx and regular std environment -#[cfg(all(not(feature = "std"), feature = "sgx"))] -pub mod sgx_reexport_prelude { - pub use futures_sgx as futures; - pub use jsonrpc_core_sgx as jsonrpc_core; - pub use rust_base58_sgx as base58; -} - -pub mod constants; -pub mod direct_top_pool_api; -pub mod import_block_api; diff --git a/bitacross-worker/sidechain/sidechain-crate/Cargo.toml b/bitacross-worker/sidechain/sidechain-crate/Cargo.toml deleted file mode 100644 index dee5728123..0000000000 --- a/bitacross-worker/sidechain/sidechain-crate/Cargo.toml +++ /dev/null @@ -1,36 +0,0 @@ -[package] -name = "its-sidechain" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[features] -default = ["std"] -std = [ - "its-block-composer/std", - "its-consensus-aura/std", - "its-consensus-common/std", - "its-consensus-slots/std", - "its-rpc-handler/std", - "its-primitives/std", - "its-state/std", - "its-validateer-fetch/std", -] -sgx = [ - "its-block-composer/sgx", - "its-consensus-aura/sgx", - "its-consensus-common/sgx", - "its-consensus-slots/sgx", - "its-rpc-handler/sgx", - "its-state/sgx", -] - -[dependencies] -its-block-composer = { path = "../block-composer", default-features = false } -its-consensus-aura = { path = "../consensus/aura", default-features = false } -its-consensus-common = { path = "../consensus/common", default-features = false } -its-consensus-slots = { path = "../consensus/slots", default-features = false } -its-primitives = { path = "../primitives", default-features = false } -its-rpc-handler = { path = "../rpc-handler", default-features = false } -its-state = { path = "../state", default-features = false } -its-validateer-fetch = { path = "../validateer-fetch", default-features = false } diff --git a/bitacross-worker/sidechain/sidechain-crate/src/lib.rs b/bitacross-worker/sidechain/sidechain-crate/src/lib.rs deleted file mode 100644 index 59821318a8..0000000000 --- a/bitacross-worker/sidechain/sidechain-crate/src/lib.rs +++ /dev/null @@ -1,39 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Reexport all the sidechain stuff in one crate - -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -pub use its_block_composer as block_composer; - -pub use its_consensus_aura as aura; - -pub use its_consensus_common as consensus_common; - -pub use its_consensus_slots as slots; - -pub use its_primitives as primitives; - -pub use its_rpc_handler as rpc_handler; - -pub use its_state as state; - -pub use its_validateer_fetch as validateer_fetch; diff --git a/bitacross-worker/sidechain/state/Cargo.toml b/bitacross-worker/sidechain/state/Cargo.toml deleted file mode 100644 index 538fb34c50..0000000000 --- a/bitacross-worker/sidechain/state/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "its-state" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "chain-error"] } -frame-support = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -log = { version = "0.4", default-features = false } - -# optional std deps -thiserror = { version = "1.0.9", optional = true } - -# sgx deps -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", optional = true } - -# sgx forks -thiserror_sgx = { package = "thiserror", version = "1.0.9", git = "https://github.com/mesalock-linux/thiserror-sgx", tag = "sgx_1.1.3", optional = true } - -# local deps -itp-sgx-externalities = { default-features = false, path = "../../core-primitives/substrate-sgx/externalities" } -itp-storage = { path = "../../core-primitives/storage", default-features = false } -its-primitives = { path = "../primitives", default-features = false } -sp-io = { optional = true, default-features = false, features = ["disable_oom", "disable_panic_handler", "disable_allocator"], path = "../../core-primitives/substrate-sgx/sp-io" } - -# substrate deps -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[features] -default = ["std"] -std = [ - "log/std", - # substrate - "sp-core/std", - # local crates - "itp-sgx-externalities/std", - "itp-storage/std", - "its-primitives/std", - "sp-io/std", - # optional std crates - "codec/std", - "thiserror", - "sp-runtime/std", -] -sgx = [ - # teaclave - "sgx_tstd", - # local crates - "itp-sgx-externalities/sgx", - "itp-storage/sgx", - "sp-io/sgx", - # sgx versions of std crates - "thiserror_sgx", -] diff --git a/bitacross-worker/sidechain/state/src/error.rs b/bitacross-worker/sidechain/state/src/error.rs deleted file mode 100644 index 82838b376e..0000000000 --- a/bitacross-worker/sidechain/state/src/error.rs +++ /dev/null @@ -1,31 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -use crate::sgx_reexports::*; - -use std::string::String; - -#[derive(Debug, thiserror::Error, PartialEq, Eq)] -pub enum Error { - #[error("Invalid apriori state hash supplied")] - InvalidAprioriHash, - #[error("Invalid storage diff")] - InvalidStorageDiff, - #[error("Codec error when accessing module: {1}, storage: {2}. Error: {0:?}")] - DB(codec::Error, String, String), -} diff --git a/bitacross-worker/sidechain/state/src/impls.rs b/bitacross-worker/sidechain/state/src/impls.rs deleted file mode 100644 index b69727085c..0000000000 --- a/bitacross-worker/sidechain/state/src/impls.rs +++ /dev/null @@ -1,184 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -//! Implement the sidechain state traits. - -use crate::{Error, SidechainState, StateUpdate}; -use codec::{Decode, Encode}; -use core::fmt::Debug; -use frame_support::ensure; -use itp_sgx_externalities::{SgxExternalitiesTrait, StateHash}; -use itp_storage::keys::storage_value_key; -use log::{debug, error, info}; -use sp_io::{storage, KillStorageResult}; - -impl SidechainState for T -where - ::SgxExternalitiesType: Encode, -{ - type Externalities = Self; - type StateUpdate = StateUpdate; - - fn apply_state_update(&mut self, state_payload: &Self::StateUpdate) -> Result<(), Error> { - info!("Current state size: {}", self.state().encoded_size()); - debug!("Current hash: {}", self.hash()); - debug!("State_payload hash: {}", state_payload.state_hash_apriori()); - debug!("self is: {:?}", &self); - debug!("state_payload is: {:?}", &state_payload); - ensure!(self.hash() == state_payload.state_hash_apriori(), Error::InvalidAprioriHash); - - self.execute_with(|| { - state_payload.state_update.iter().for_each(|(k, v)| { - match v { - Some(value) => storage::set(k, value), - None => storage::clear(k), - }; - }) - }); - - ensure!(self.hash() == state_payload.state_hash_aposteriori(), Error::InvalidStorageDiff); - self.prune_state_diff(); - Ok(()) - } - - fn get_with_name(&self, module_prefix: &str, storage_prefix: &str) -> Option { - let res = self - .get(&storage_value_key(module_prefix, storage_prefix)) - .map(|v| Decode::decode(&mut v.as_slice())) - .transpose(); - - match res { - Ok(res) => res, - Err(e) => { - error!( - "Error decoding storage: {}, {}. Error: {:?}", - module_prefix, storage_prefix, e - ); - None - }, - } - } - - fn set_with_name(&mut self, module_prefix: &str, storage_prefix: &str, value: V) { - self.set(&storage_value_key(module_prefix, storage_prefix), &value.encode()) - } - - fn clear_with_name(&mut self, module_prefix: &str, storage_prefix: &str) { - self.clear(&storage_value_key(module_prefix, storage_prefix)) - } - - fn clear_prefix_with_name( - &mut self, - module_prefix: &str, - storage_prefix: &str, - ) -> KillStorageResult { - self.clear_sidechain_prefix(&storage_value_key(module_prefix, storage_prefix)) - } - - fn set(&mut self, key: &[u8], value: &[u8]) { - self.execute_with(|| sp_io::storage::set(key, value)) - } - - fn clear(&mut self, key: &[u8]) { - self.execute_with(|| sp_io::storage::clear(key)) - } - - fn clear_sidechain_prefix(&mut self, prefix: &[u8]) -> KillStorageResult { - self.execute_with(|| sp_io::storage::clear_prefix(prefix, None)) - } -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::StateUpdate; - use frame_support::{assert_err, assert_ok}; - use itp_sgx_externalities::{SgxExternalities, SgxExternalitiesTrait}; - use sp_core::H256; - - pub fn default_db() -> SgxExternalities { - SgxExternalities::default() - } - - #[test] - pub fn apply_state_update_works() { - let mut state1 = default_db(); - let mut state2 = default_db(); - - let apriori = state1.hash(); - state1.set(b"Hello", b"World"); - let aposteriori = state1.hash(); - - let mut state_update = StateUpdate::new(apriori, aposteriori, state1.state_diff().clone()); - - assert_ok!(state2.apply_state_update(&mut state_update)); - assert_eq!(state2.hash(), aposteriori); - assert_eq!(state2.get(b"Hello").unwrap(), b"World"); - assert!(state2.state_diff().is_empty()); - } - - #[test] - pub fn apply_state_update_returns_storage_hash_mismatch_err() { - let mut state1 = default_db(); - let mut state2 = default_db(); - - let apriori = H256::from([1; 32]); - state1.set(b"Hello", b"World"); - let aposteriori = state1.hash(); - - let mut state_update = StateUpdate::new(apriori, aposteriori, state1.state_diff().clone()); - - assert_err!(state2.apply_state_update(&mut state_update), Error::InvalidAprioriHash); - assert_eq!(state2, default_db()); - } - - #[test] - pub fn apply_state_update_returns_invalid_storage_diff_err() { - let mut state1 = default_db(); - let mut state2 = default_db(); - - let apriori = state1.hash(); - state1.set(b"Hello", b"World"); - let aposteriori = H256::from([1; 32]); - - let mut state_update = StateUpdate::new(apriori, aposteriori, state1.state_diff().clone()); - - assert_err!(state2.apply_state_update(&mut state_update), Error::InvalidStorageDiff); - // After an error, the state is not guaranteed to be reverted and is potentially corrupted! - assert_ne!(state2, default_db()); - } - - #[test] - pub fn sp_io_storage_set_creates_storage_diff() { - let mut state1 = default_db(); - - state1.execute_with(|| { - storage::set(b"hello", b"world"); - }); - - assert_eq!(state1.state_diff().get(&b"hello"[..]).unwrap(), &Some(b"world".encode())); - } - - #[test] - pub fn create_state_diff_without_setting_externalities_works() { - let mut state1 = default_db(); - - state1.set(b"hello", b"world"); - - assert_eq!(state1.state_diff().get(&b"hello"[..]).unwrap(), &Some(b"world".encode())); - } -} diff --git a/bitacross-worker/sidechain/state/src/lib.rs b/bitacross-worker/sidechain/state/src/lib.rs deleted file mode 100644 index 01f5e086ec..0000000000 --- a/bitacross-worker/sidechain/state/src/lib.rs +++ /dev/null @@ -1,208 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -extern crate sgx_tstd as std; - -mod error; -mod impls; - -pub use error::*; -pub use impls::*; - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -mod sgx_reexports { - pub use thiserror_sgx as thiserror; -} - -use codec::{Decode, Encode}; -use itp_sgx_externalities::{SgxExternalitiesDiffType, SgxExternalitiesTrait, StateHash}; -use its_primitives::{ - traits::Block as SidechainBlockTrait, - types::{BlockHash, BlockNumber, Timestamp}, -}; -use sp_core::H256; -use sp_io::KillStorageResult; -use sp_runtime::traits::Header as ParentchainHeaderTrait; - -/// Contains the necessary data to update the `SidechainDB` when importing a `SidechainBlock`. -#[derive(PartialEq, Eq, Clone, Debug, Encode, Decode)] -pub struct StateUpdate { - /// state hash before the `state_update` was applied. - state_hash_apriori: H256, - /// state hash after the `state_update` was applied. - state_hash_aposteriori: H256, - /// state diff applied to state with hash `state_hash_apriori` - /// leading to state with hash `state_hash_aposteriori` - state_update: SgxExternalitiesDiffType, -} - -impl StateUpdate { - /// get state hash before the `state_update` was applied. - pub fn state_hash_apriori(&self) -> H256 { - self.state_hash_apriori - } - /// get state hash after the `state_update` was applied. - pub fn state_hash_aposteriori(&self) -> H256 { - self.state_hash_aposteriori - } - /// reference to the `state_update` - pub fn state_update(&self) -> &SgxExternalitiesDiffType { - &self.state_update - } - - /// create new `StatePayload` instance. - pub fn new(apriori: H256, aposteriori: H256, update: SgxExternalitiesDiffType) -> StateUpdate { - StateUpdate { - state_hash_apriori: apriori, - state_hash_aposteriori: aposteriori, - state_update: update, - } - } -} -/// Abstraction around the sidechain state. -pub trait SidechainState: Clone { - type Externalities: SgxExternalitiesTrait + StateHash; - - type StateUpdate: Encode + Decode; - - /// Apply the state update to the state. - /// - /// Does not guarantee state consistency in case of a failure. - /// Caller is responsible for discarding corrupt/inconsistent state. - fn apply_state_update(&mut self, state_payload: &Self::StateUpdate) -> Result<(), Error>; - - /// Get a storage value by its full name. - fn get_with_name(&self, module_prefix: &str, storage_prefix: &str) -> Option; - - /// Set a storage value by its full name. - fn set_with_name(&mut self, module_prefix: &str, storage_prefix: &str, value: V); - - /// Clear a storage value by its full name. - fn clear_with_name(&mut self, module_prefix: &str, storage_prefix: &str); - - /// Clear all storage values for the given prefix. - fn clear_prefix_with_name( - &mut self, - module_prefix: &str, - storage_prefix: &str, - ) -> KillStorageResult; - - /// Set a storage value by its storage hash. - fn set(&mut self, key: &[u8], value: &[u8]); - - /// Clear a storage value by its storage hash. - fn clear(&mut self, key: &[u8]); - - /// Clear a all storage values starting the given prefix. - fn clear_sidechain_prefix(&mut self, prefix: &[u8]) -> KillStorageResult; -} - -/// trait to set and get the last sidechain block of the sidechain state -pub trait LastBlockExt { - /// get the last block of the sidechain state - fn get_last_block(&self) -> Option; - - /// set the last block of the sidechain state - fn set_last_block(&mut self, block: &SidechainBlock); -} - -impl - LastBlockExt for E -{ - fn get_last_block(&self) -> Option { - self.get_with_name("System", "LastBlock") - } - - fn set_last_block(&mut self, block: &SidechainBlock) { - self.set_last_block_hash(&block.hash()); - self.set_with_name("System", "LastBlock", block) - } -} - -/// System extension for the `SidechainDB`. -pub trait SidechainSystemExt { - /// Get the last block number. - fn get_block_number(&self) -> Option; - - /// Set the last block number. - fn set_block_number(&mut self, number: &BlockNumber); - - /// Get the last block hash. - fn get_last_block_hash(&self) -> Option; - - /// Set the last block hash. - fn set_last_block_hash(&mut self, hash: &BlockHash); - - /// Get the timestamp of. - fn get_timestamp(&self) -> Option; - - /// Set the timestamp. - fn set_timestamp(&mut self, timestamp: &Timestamp); - - /// Resets the events. - fn reset_events(&mut self); - - /// Litentry: set the parentchain block number from the parentchain header - /// The reasons to put it here instead of calling `ParentchainPalletInterface::update_parentchain_block` somewhere are: - /// 1. The Stf::update_parentchain_block is too heavy weighted, where the whole state is loaded upon each parentchain - /// block import - btw it's not reachable for now as `storage_hashes_to_update_on_block` is always empty - /// 2. It represents the parentchain block number on which the current sidechain block is built, it's more natural to - /// call it in the state preprocessing before proposing a sidechain block - fn set_parentchain_block_number(&mut self, header: &PH); -} - -impl SidechainSystemExt for T { - fn get_block_number(&self) -> Option { - self.get_with_name("System", "Number") - } - - fn set_block_number(&mut self, number: &BlockNumber) { - self.set_with_name("System", "Number", number) - } - - fn get_last_block_hash(&self) -> Option { - self.get_with_name("System", "LastHash") - } - - fn set_last_block_hash(&mut self, hash: &BlockHash) { - self.set_with_name("System", "LastHash", hash) - } - - fn get_timestamp(&self) -> Option { - self.get_with_name("System", "Timestamp") - } - - fn set_timestamp(&mut self, timestamp: &Timestamp) { - self.set_with_name("System", "Timestamp", timestamp) - } - - fn reset_events(&mut self) { - self.clear_with_name("System", "Events"); - self.clear_with_name("System", "EventCount"); - self.clear_prefix_with_name("System", "EventTopics"); - } - - fn set_parentchain_block_number(&mut self, header: &PH) { - self.set_with_name("Parentchain", "Number", header.number()) - } -} diff --git a/bitacross-worker/sidechain/storage/Cargo.toml b/bitacross-worker/sidechain/storage/Cargo.toml deleted file mode 100644 index 7df586896f..0000000000 --- a/bitacross-worker/sidechain/storage/Cargo.toml +++ /dev/null @@ -1,34 +0,0 @@ -[package] -name = "its-storage" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -# crate.io -codec = { package = "parity-scale-codec", version = "3.0.0", features = ["derive"] } -log = "0.4" -parking_lot = "0.12.1" -rocksdb = { version = "0.20.1", default_features = false } -serde = { version = "1.0", features = ["derive"] } -thiserror = "1.0" - -# integritee -itp-settings = { path = "../../core-primitives/settings" } - -its-primitives = { path = "../primitives" } - -# Substrate dependencies -sp-core = { git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -[dev-dependencies] -# crate.io -mockall = "0.11" -temp-dir = "0.1" -# local -itp-time-utils = { path = "../../core-primitives/time-utils" } -its-test = { path = "../test" } -itp-types = { path = "../../core-primitives/types" } - -[features] -mocks = [] diff --git a/bitacross-worker/sidechain/storage/src/db.rs b/bitacross-worker/sidechain/storage/src/db.rs deleted file mode 100644 index 6e51a749fb..0000000000 --- a/bitacross-worker/sidechain/storage/src/db.rs +++ /dev/null @@ -1,67 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use super::{Error, Result}; -use codec::{Decode, Encode}; -use rocksdb::{WriteBatch, DB}; -use std::path::PathBuf; - -/// Sidechain DB Storage structure: -/// STORED_SHARDS_KEY -> Vec<(Shard)> -/// (LAST_BLOCK_KEY, Shard) -> (Blockhash, BlockNr) (look up current blockchain state) -/// (Shard , Block number) -> Blockhash (needed for block pruning) -/// Blockhash -> Signed Block (actual block storage) - -/// Interface struct to rocks DB -pub struct SidechainDB { - db: DB, -} - -impl SidechainDB { - pub fn open_default(path: PathBuf) -> Result { - Ok(SidechainDB { db: DB::open_default(path)? }) - } - - /// returns the decoded value of the DB entry, if there is one - pub fn get(&self, key: K) -> Result> { - match self.db.get(key.encode())? { - None => Ok(None), - Some(encoded_hash) => Ok(Some(V::decode(&mut encoded_hash.as_slice())?)), - } - } - - /// writes a batch to the DB - pub fn write(&mut self, batch: WriteBatch) -> Result<()> { - self.db.write(batch).map_err(Error::Operational) - } - - /// adds a given key value pair to the batch - pub fn add_to_batch(batch: &mut WriteBatch, key: K, value: V) { - batch.put(key.encode(), &value.encode()) - } - - /// adds a delte key command to the batch - pub fn delete_to_batch(batch: &mut WriteBatch, key: K) { - batch.delete(key.encode()) - } - - /// add an entry to the DB - #[cfg(test)] - pub fn put(&mut self, key: K, value: V) -> Result<()> { - self.db.put(key.encode(), value.encode()).map_err(Error::Operational) - } -} diff --git a/bitacross-worker/sidechain/storage/src/error.rs b/bitacross-worker/sidechain/storage/src/error.rs deleted file mode 100644 index 983909f1a4..0000000000 --- a/bitacross-worker/sidechain/storage/src/error.rs +++ /dev/null @@ -1,34 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use thiserror::Error; - -pub type Result = std::result::Result; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Could not interact with file storage: {0:?}")] - Operational(#[from] rocksdb::Error), - #[error("Last Block of shard {0} not found")] - LastBlockNotFound(String), - #[error("Failed to find parent block")] - FailedToFindParentBlock, - #[error("Could not decode: {0:?}")] - Decode(#[from] codec::Error), - #[error("Given block is not a successor of the last known block")] - HeaderAncestryMismatch, -} diff --git a/bitacross-worker/sidechain/storage/src/fetch_blocks_mock.rs b/bitacross-worker/sidechain/storage/src/fetch_blocks_mock.rs deleted file mode 100644 index 4ba476164d..0000000000 --- a/bitacross-worker/sidechain/storage/src/fetch_blocks_mock.rs +++ /dev/null @@ -1,71 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{error::Result, interface::FetchBlocks, storage::LastSidechainBlock}; -use its_primitives::{ - traits::{Block, ShardIdentifierFor}, - types::{BlockHash, BlockNumber, SignedBlock}, -}; - -#[derive(Default)] -pub struct FetchBlocksMock { - blocks_to_be_fetched: Vec, -} - -impl FetchBlocksMock { - pub fn with_blocks(mut self, blocks: Vec) -> Self { - self.blocks_to_be_fetched = blocks; - self - } -} - -impl FetchBlocks for FetchBlocksMock { - fn fetch_all_blocks_after( - &self, - _block_hash: &BlockHash, - _shard_identifier: &ShardIdentifierFor, - ) -> Result> { - Ok(self.blocks_to_be_fetched.clone()) - } - - fn fetch_blocks_in_range( - &self, - _block_hash_from: &BlockHash, - _block_hash_until: &BlockHash, - _shard_identifier: &ShardIdentifierFor, - ) -> Result> { - Ok(self.blocks_to_be_fetched.clone()) - } - - fn latest_block( - &self, - _shard_identifier: &ShardIdentifierFor, - ) -> Option { - self.blocks_to_be_fetched.get(0).map(|block| LastSidechainBlock { - hash: block.block.hash(), - number: block.block.header.block_number, - }) - } - - fn block_hash( - &self, - _block_number: BlockNumber, - _shard_identifier: &ShardIdentifierFor, - ) -> Option { - None - } -} diff --git a/bitacross-worker/sidechain/storage/src/interface.rs b/bitacross-worker/sidechain/storage/src/interface.rs deleted file mode 100644 index 03b41d6d76..0000000000 --- a/bitacross-worker/sidechain/storage/src/interface.rs +++ /dev/null @@ -1,154 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[cfg(test)] -use mockall::predicate::*; -#[cfg(test)] -use mockall::*; - -use super::{ - storage::{LastSidechainBlock, SidechainStorage}, - Result, -}; -use its_primitives::{ - traits::{ShardIdentifierFor, SignedBlock as SignedBlockT}, - types::{BlockHash, BlockNumber}, -}; -use parking_lot::RwLock; -use std::path::PathBuf; - -/// Lock wrapper around sidechain storage -pub struct SidechainStorageLock { - storage: RwLock>, -} - -impl SidechainStorageLock { - pub fn from_base_path(path: PathBuf) -> Result> { - Ok(SidechainStorageLock { - storage: RwLock::new(SidechainStorage::::load_from_base_path(path)?), - }) - } -} - -/// Storage interface Trait -#[cfg_attr(test, automock)] -pub trait BlockStorage { - // Type is not working because broadcaster needs to work with the same block type, - // so it needs to be defined somewhere more global. - // type SignedBlock: SignedBlockT; - fn store_blocks(&self, blocks: Vec) -> Result<()>; -} - -pub trait BlockPruner { - /// Prune all blocks except the newest n, where n = `number_of_blocks_to_keep`. - fn prune_blocks_except(&self, number_of_blocks_to_keep: u64); -} - -#[cfg_attr(test, automock)] -pub trait FetchBlocks { - /// Fetch all child blocks of a specified block. - /// - /// Returns an empty vector if specified block hash cannot be found in storage. - fn fetch_all_blocks_after( - &self, - block_hash: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result>; - - /// Fetch all blocks within a range, defined by a starting block (lower bound) and end block (upper bound) hash. - /// - /// Does NOT include the bound defining blocks in the result. ]from..until[. - /// Returns an empty vector if 'from' cannot be found in storage. - /// Returns the same as 'fetch_all_blocks_after' if 'until' cannot be found in storage. - fn fetch_blocks_in_range( - &self, - block_hash_from: &BlockHash, - block_hash_until: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result>; - - // litentry - fn latest_block( - &self, - shard_identifier: &ShardIdentifierFor, - ) -> Option; - - fn block_hash( - &self, - block_number: BlockNumber, - shard_identifier: &ShardIdentifierFor, - ) -> Option; -} - -impl BlockStorage for SidechainStorageLock { - fn store_blocks(&self, blocks: Vec) -> Result<()> { - self.storage.write().store_blocks(blocks) - } -} - -impl BlockPruner for SidechainStorageLock { - fn prune_blocks_except(&self, number_of_blocks_to_keep: BlockNumber) { - self.storage.write().prune_shards(number_of_blocks_to_keep); - } -} - -impl FetchBlocks for SidechainStorageLock { - fn fetch_all_blocks_after( - &self, - block_hash: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result> { - self.storage.read().get_blocks_after(block_hash, shard_identifier) - } - - fn fetch_blocks_in_range( - &self, - block_hash_from: &BlockHash, - block_hash_until: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result> { - self.storage - .read() - .get_blocks_in_range(block_hash_from, block_hash_until, shard_identifier) - } - - fn latest_block( - &self, - shard_identifier: &ShardIdentifierFor, - ) -> Option { - self.storage - .read() - .last_block_of_shard(shard_identifier) - .map(|e| LastSidechainBlock { hash: e.hash, number: e.number }) - } - - fn block_hash( - &self, - block_number: BlockNumber, - shard_identifier: &ShardIdentifierFor, - ) -> Option { - match self.storage.read().get_block_hash(shard_identifier, block_number) { - Ok(Some(block_hash)) => - Some(LastSidechainBlock { hash: block_hash, number: block_number }), - Ok(None) => None, - Err(e) => { - log::error!("failed to get block_hash. due to:{:?}", e); - None - }, - } - } -} diff --git a/bitacross-worker/sidechain/storage/src/lib.rs b/bitacross-worker/sidechain/storage/src/lib.rs deleted file mode 100644 index 7b6030ff90..0000000000 --- a/bitacross-worker/sidechain/storage/src/lib.rs +++ /dev/null @@ -1,70 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(test, feature(assert_matches))] - -use its_primitives::types::BlockNumber; -use std::{ - sync::Arc, - thread, - time::{Duration, SystemTime}, -}; - -mod db; -mod error; -pub mod interface; -mod storage; - -#[cfg(test)] -mod storage_tests_get_blocks_after; - -#[cfg(test)] -mod storage_tests_get_blocks_in_range; - -#[cfg(test)] -mod test_utils; - -#[cfg(feature = "mocks")] -pub mod fetch_blocks_mock; - -pub use error::{Error, Result}; -pub use interface::{BlockPruner, BlockStorage, SidechainStorageLock}; -pub use storage::LastSidechainBlock; - -pub fn start_sidechain_pruning_loop( - storage: &Arc, - purge_interval: u64, - purge_limit: BlockNumber, -) where - D: BlockPruner, -{ - let interval_time = Duration::from_secs(purge_interval); - let mut interval_start = SystemTime::now(); - loop { - if let Ok(elapsed) = interval_start.elapsed() { - if elapsed >= interval_time { - // update interval time - interval_start = SystemTime::now(); - storage.prune_blocks_except(purge_limit); - } else { - // sleep for the rest of the interval - let sleep_time = interval_time - elapsed; - thread::sleep(sleep_time); - } - } - } -} diff --git a/bitacross-worker/sidechain/storage/src/storage.rs b/bitacross-worker/sidechain/storage/src/storage.rs deleted file mode 100644 index 4e5667627b..0000000000 --- a/bitacross-worker/sidechain/storage/src/storage.rs +++ /dev/null @@ -1,1176 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use super::{db::SidechainDB, Error, Result}; -use codec::{Decode, Encode}; -use itp_settings::files::SIDECHAIN_STORAGE_PATH; -use its_primitives::{ - traits::{Block as BlockTrait, Header as HeaderTrait, SignedBlock as SignedBlockT}, - types::{BlockHash, BlockNumber}, -}; -use log::*; -use rocksdb::WriteBatch; -use serde::Serialize; -use sp_core::H256; -use std::{collections::HashMap, fmt::Debug, path::PathBuf}; - -/// key value of sidechain db of last block -const LAST_BLOCK_KEY: &[u8] = b"last_sidechainblock"; -/// key value of the stored shards vector -const STORED_SHARDS_KEY: &[u8] = b"stored_shards"; - -/// ShardIdentifier type -type ShardIdentifierFor = - <<::Block as BlockTrait>::HeaderType as HeaderTrait>::ShardIdentifier; - -/// Helper struct, contains the blocknumber -/// and blockhash of the last sidechain block -#[derive(PartialEq, Eq, Clone, Copy, Encode, Decode, Debug, Default, Serialize)] -pub struct LastSidechainBlock { - /// hash of the last sidechain block - pub hash: H256, - /// block number of the last sidechain block - pub number: BlockNumber, -} - -/// Struct used to insert newly produced sidechainblocks -/// into the database -pub struct SidechainStorage { - /// database - db: SidechainDB, - /// shards in database - shards: Vec>, - /// map to last sidechain block of every shard - last_blocks: HashMap, LastSidechainBlock>, -} - -impl SidechainStorage { - /// Loads or initializes the DB at a given path. - /// - /// Loads existing shards and their last blocks in memory for better performance. - pub fn load_from_base_path(base_path: PathBuf) -> Result> { - // load db - let db = SidechainDB::open_default(base_path.join(SIDECHAIN_STORAGE_PATH))?; - let mut storage = SidechainStorage { db, shards: vec![], last_blocks: HashMap::new() }; - storage.shards = storage.load_shards_from_db()?; - // get last block of each shard - for shard in storage.shards.iter() { - if let Some(last_block) = storage.load_last_block_from_db(shard)? { - storage.last_blocks.insert(*shard, last_block); - } else { - // an empty shard sidechain storage should not exist. Consider deleting this shard from the shards list. - error!("Sidechain storage of shard {:?} is empty", shard); - } - } - Ok(storage) - } - - /// gets all shards of currently loaded sidechain db - pub fn shards(&self) -> &Vec> { - &self.shards - } - - /// gets the last block of the current sidechain DB and the given shard - pub fn last_block_of_shard( - &self, - shard: &ShardIdentifierFor, - ) -> Option<&LastSidechainBlock> { - self.last_blocks.get(shard) - } - - /// gets the block hash of the sidechain block of the given shard and block number, if there is such a block - pub fn get_block_hash( - &self, - shard: &ShardIdentifierFor, - block_number: BlockNumber, - ) -> Result> { - self.db.get((*shard, block_number)) - } - - /// gets the block of the given blockhash, if there is such a block - #[allow(unused)] - pub fn get_block(&self, block_hash: &BlockHash) -> Result> { - self.db.get(block_hash) - } - - /// Get all blocks after (i.e. children of) a specified block. - pub fn get_blocks_after( - &self, - block_hash: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result> { - // Ensure we find the block in storage (otherwise we would return all blocks for a specific shard). - // The exception is, if the hash is the default hash, which represents block 0. In that case we want to return all blocks. - if block_hash != &BlockHash::default() && self.get_block(block_hash)?.is_none() { - warn!("Could not find starting block in storage, returning empty vector"); - return Ok(Vec::new()) - } - - // We get the latest block and then traverse the parents until we find our starting block. - let last_block_of_shard = self.last_block_of_shard(shard_identifier).ok_or_else(|| { - Error::LastBlockNotFound("Failed to find last block information".to_string()) - })?; - let latest_block = self.get_block(&last_block_of_shard.hash)?.ok_or_else(|| { - Error::LastBlockNotFound("Failed to retrieve last block from storage".to_string()) - })?; - - let mut current_block = latest_block; - let mut blocks_to_return = Vec::::new(); - while ¤t_block.hash() != block_hash { - let parent_block_hash = current_block.block().header().parent_hash(); - - blocks_to_return.push(current_block); - - if parent_block_hash == BlockHash::default() { - break - } - - current_block = - self.get_block(&parent_block_hash)?.ok_or(Error::FailedToFindParentBlock)?; - } - - // Reverse because we iterate from newest to oldest, but result should be oldest first. - blocks_to_return.reverse(); - - Ok(blocks_to_return) - } - - /// Get blocks in a range, defined by 'from' and 'until' (result does NOT include the bound defining blocks). - pub fn get_blocks_in_range( - &self, - block_hash_from: &BlockHash, - block_hash_until: &BlockHash, - shard_identifier: &ShardIdentifierFor, - ) -> Result> { - let all_blocks_from_lower_bound = - self.get_blocks_after(block_hash_from, shard_identifier)?; - - Ok(all_blocks_from_lower_bound - .into_iter() - .take_while(|b| b.hash() != *block_hash_until) - .collect()) - } - - /// Update sidechain storage with blocks. - /// - /// Blocks are iterated through one by one. In case more than one block per shard is included, - /// be sure to give them in the correct order (oldest first). - pub fn store_blocks(&mut self, blocks_to_store: Vec) -> Result<()> { - let mut batch = WriteBatch::default(); - let mut new_shard = false; - for block in blocks_to_store.into_iter() { - if let Err(e) = self.add_block_to_batch(&block, &mut new_shard, &mut batch) { - error!("Could not store block {:?} due to: {:?}", block, e); - }; - } - // Update stored_shards_key -> vec only if a new shard was included, - if new_shard { - SidechainDB::add_to_batch(&mut batch, STORED_SHARDS_KEY, self.shards().clone()); - } - // Store everything. - self.db.write(batch) - } - - /// purges a shard and its block from the db storage - pub fn purge_shard(&mut self, shard: &ShardIdentifierFor) -> Result<()> { - // get last block of shard - let last_block = self.get_last_block_of_shard(shard)?; - - // remove last block from db storage - let mut batch = WriteBatch::default(); - self.delete_last_block(&mut batch, &last_block, shard); - - // Remove the rest of the blocks from the db - let mut current_block_number = last_block.number; - while let Some(previous_block) = self.get_previous_block(shard, current_block_number)? { - current_block_number = previous_block.number; - self.delete_block(&mut batch, &previous_block.hash, ¤t_block_number, shard); - } - // Remove shard from list. - // STORED_SHARDS_KEY -> Vec<(Shard)> - self.shards.retain(|&x| x != *shard); - // Add updated shards to batch. - SidechainDB::add_to_batch(&mut batch, STORED_SHARDS_KEY, &self.shards); - // Update DB - self.db.write(batch) - } - - /// purges a shard and its block from the db storage - /// FIXME: Add delete functions? - pub fn prune_shard_from_block_number( - &mut self, - shard: &ShardIdentifierFor, - block_number: BlockNumber, - ) -> Result<()> { - let last_block = self.get_last_block_of_shard(shard)?; - if last_block.number == block_number { - // given block number is last block of chain - purge whole shard - self.purge_shard(shard) - } else { - // iterate through chain and add all blocks to WriteBatch (delete cmd) - let mut batch = WriteBatch::default(); - let mut current_block_number = block_number; - // Remove blocks from db until no block anymore - while let Some(block_hash) = self.get_block_hash(shard, current_block_number)? { - self.delete_block(&mut batch, &block_hash, ¤t_block_number, shard); - current_block_number -= 1; - } - // Update DB - self.db.write(batch) - } - } - - /// Prunes all shards except for the newest blocks (according to blocknumber). - pub fn prune_shards(&mut self, number_of_blocks_to_keep: BlockNumber) { - for shard in self.shards().clone() { - // get last block: - if let Some(last_block) = self.last_block_of_shard(&shard) { - let threshold_block = last_block.number - number_of_blocks_to_keep; - if let Err(e) = self.prune_shard_from_block_number(&shard, threshold_block) { - error!("Could not purge shard {:?} due to {:?}", shard, e); - } - } else { - error!("Last block not found in shard {:?}", shard); - } - } - } - - fn add_block_to_batch( - &mut self, - signed_block: &SignedBlock, - new_shard: &mut bool, - batch: &mut WriteBatch, - ) -> Result<()> { - let shard = &signed_block.block().header().shard_id(); - if self.shards.contains(shard) { - if !self.verify_block_ancestry(signed_block.block()) { - // Do not include block if its not a direct ancestor of the last block in line. - return Err(Error::HeaderAncestryMismatch) - } - } else { - self.shards.push(*shard); - *new_shard = true; - } - // Add block to DB batch. - self.add_last_block(batch, signed_block); - Ok(()) - } - - fn verify_block_ancestry(&self, block: &::Block) -> bool { - let shard = &block.header().shard_id(); - let current_block_nr = block.header().block_number(); - if let Some(last_block) = self.last_block_of_shard(shard) { - if last_block.number != current_block_nr - 1 { - error!("[Sidechain DB] Sidechainblock (nr: {:?}) is not a succession of the previous block (nr: {:?}) in shard: {:?}", - current_block_nr, last_block.number, *shard); - return false - } - } else { - error!( - "[Sidechain DB] Shard {:?} does not have a last block. Skipping block (nr: {:?}) inclusion", - *shard, current_block_nr - ); - return false - } - true - } - - /// Implementations of helper functions, not meant for pub use - /// gets the previous block of given shard and block number, if there is one. - fn get_previous_block( - &self, - shard: &ShardIdentifierFor, - current_block_number: BlockNumber, - ) -> Result> { - let prev_block_number = current_block_number - 1; - Ok(self - .get_block_hash(shard, prev_block_number)? - .map(|block_hash| LastSidechainBlock { hash: block_hash, number: prev_block_number })) - } - fn load_shards_from_db(&self) -> Result>> { - Ok(self.db.get(STORED_SHARDS_KEY)?.unwrap_or_default()) - } - - fn load_last_block_from_db( - &self, - shard: &ShardIdentifierFor, - ) -> Result> { - self.db.get((LAST_BLOCK_KEY, *shard)) - } - - fn get_last_block_of_shard( - &self, - shard: &ShardIdentifierFor, - ) -> Result { - match self.last_blocks.get(shard) { - Some(last_block) => Ok(*last_block), - None => { - // Try to read from db: - self.load_last_block_from_db(shard)? - .ok_or_else(|| Error::LastBlockNotFound(format!("{:?}", *shard))) - }, - } - } - - /// Adds the block to the WriteBatch. - fn add_last_block(&mut self, batch: &mut WriteBatch, block: &SignedBlock) { - let hash = block.hash(); - let block_number = block.block().header().block_number(); - let shard = block.block().header().shard_id(); - // Block hash -> Signed Block. - SidechainDB::add_to_batch(batch, hash, block); - - // (Shard, Block number) -> Blockhash (for block pruning). - SidechainDB::add_to_batch(batch, (shard, block_number), hash); - - // (last_block_key, shard) -> (Blockhash, BlockNr) current blockchain state. - let last_block = LastSidechainBlock { hash, number: block_number }; - self.last_blocks.insert(shard, last_block); // add in memory - SidechainDB::add_to_batch(batch, (LAST_BLOCK_KEY, shard), last_block); - } - - /// Add delete block to the WriteBatch. - fn delete_block( - &self, - batch: &mut WriteBatch, - block_hash: &H256, - block_number: &BlockNumber, - shard: &ShardIdentifierFor, - ) { - // Block hash -> Signed Block. - SidechainDB::delete_to_batch(batch, block_hash); - // (Shard, Block number) -> Blockhash (for block pruning). - SidechainDB::delete_to_batch(batch, (shard, block_number)); - } - - /// Add delete command to remove last block to WriteBatch and remove it from memory. - /// - /// This includes adding a delete command of the following: - /// - Block hash -> Signed Block. - /// - (Shard, Block number) -> Blockhash (for block pruning). - /// - ((LAST_BLOCK_KEY, shard) -> BlockHash) -> Blockhash (for block pruning). - /// - /// Careful usage of this command: In case the last block is deleted, (LAST_BLOCK_KEY, shard) will be empty - /// even though there might be a new last block (i.e. the previous block of the removed last block). - fn delete_last_block( - &mut self, - batch: &mut WriteBatch, - last_block: &LastSidechainBlock, - shard: &ShardIdentifierFor, - ) { - // Add delete block to batch. - // (LAST_BLOCK_KEY, Shard) -> LastSidechainBlock. - SidechainDB::delete_to_batch(batch, (LAST_BLOCK_KEY, *shard)); - self.delete_block(batch, &last_block.hash, &last_block.number, shard); - - // Delete last block from local memory. - // Careful here: This deletes the local memory before db has been actually pruned - // (it's only been added to the write batch). - // But this can be fixed upon reloading the db / restarting the worker. - self.last_blocks.remove(shard); - } -} - -#[cfg(test)] -mod test { - use super::*; - use crate::test_utils::{ - create_signed_block_with_shard as create_signed_block, create_temp_dir, get_storage, - }; - use itp_types::ShardIdentifier; - use its_primitives::{traits::SignedBlock as SignedBlockT, types::SignedBlock}; - use sp_core::H256; - - #[test] - fn load_shards_from_db_works() { - // given - let temp_dir = create_temp_dir(); - let shard_one = H256::from_low_u64_be(1); - let shard_two = H256::from_low_u64_be(2); - // when - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // ensure db starts empty - assert_eq!(sidechain_db.load_shards_from_db().unwrap(), vec![]); - // write signed_block to db - sidechain_db.db.put(STORED_SHARDS_KEY, vec![shard_one, shard_two]).unwrap(); - } - - // then - { - // open new DB of same path: - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let loaded_shards = updated_sidechain_db.load_shards_from_db().unwrap(); - assert!(loaded_shards.contains(&shard_one)); - assert!(loaded_shards.contains(&shard_two)); - } - } - - #[test] - fn load_last_block_from_db_works() { - // given - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(20, shard); - let signed_last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - // when - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // ensure db starts empty - assert!(sidechain_db.load_last_block_from_db(&shard).unwrap().is_none()); - // write signed_block to db - sidechain_db.db.put((LAST_BLOCK_KEY, shard), signed_last_block.clone()).unwrap(); - } - - // then - { - // open new DB of same path: - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let loaded_block = - updated_sidechain_db.load_last_block_from_db(&shard).unwrap().unwrap(); - assert_eq!(loaded_block, signed_last_block); - } - } - - #[test] - fn create_new_sidechain_storage_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let shard_vector = vec![shard]; - let signed_block = create_signed_block(20, shard); - let signed_last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // ensure db starts empty - assert!(sidechain_db.load_last_block_from_db(&shard).unwrap().is_none()); - // write shards to db - sidechain_db.db.put((LAST_BLOCK_KEY, shard), signed_last_block.clone()).unwrap(); - // write shards to db - sidechain_db.db.put(STORED_SHARDS_KEY, shard_vector.clone()).unwrap(); - } - - { - // open new DB of same path: - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - assert_eq!(updated_sidechain_db.shards, shard_vector); - assert_eq!(*updated_sidechain_db.last_blocks.get(&shard).unwrap(), signed_last_block); - } - } - - #[test] - fn add_last_block_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let mut batch = WriteBatch::default(); - sidechain_db.add_last_block(&mut batch, &signed_block); - sidechain_db.db.write(batch).unwrap(); - - // ensure DB contains previously stored data: - let last_block = sidechain_db.last_block_of_shard(&shard).unwrap(); - assert_eq!(last_block.number, signed_block.block().header().block_number()); - assert_eq!(last_block.hash, signed_block.hash()); - let stored_block_hash = - sidechain_db.get_block_hash(&shard, last_block.number).unwrap().unwrap(); - assert_eq!(stored_block_hash, signed_block.hash()); - assert_eq!(sidechain_db.get_block(&stored_block_hash).unwrap().unwrap(), signed_block); - } - } - - #[test] - fn delete_block_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - { - // fill db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.db.put(signed_block.hash(), signed_block.clone()).unwrap(); - sidechain_db - .db - .put((shard, signed_block.block().header().block_number()), signed_block.hash()) - .unwrap(); - assert_eq!( - sidechain_db - .db - .get::<(ShardIdentifier, BlockNumber), H256>(( - shard, - signed_block.block().header().block_number() - )) - .unwrap() - .unwrap(), - signed_block.hash() - ); - assert_eq!( - sidechain_db.db.get::(signed_block.hash()).unwrap().unwrap(), - signed_block - ); - - // when - let mut batch = WriteBatch::default(); - sidechain_db.delete_block( - &mut batch, - &signed_block.hash(), - &signed_block.block().header().block_number(), - &shard, - ); - sidechain_db.db.write(batch).unwrap(); - } - - { - // open new DB of same path: - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // ensure DB does not contain block anymore: - assert!(updated_sidechain_db - .db - .get::<(ShardIdentifier, BlockNumber), H256>(( - shard, - signed_block.block().header().block_number() - )) - .unwrap() - .is_none()); - assert!(updated_sidechain_db - .db - .get::(signed_block.hash()) - .unwrap() - .is_none()); - } - } - - #[test] - fn delete_last_block_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - { - // fill db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.db.put(signed_block.hash(), signed_block.clone()).unwrap(); - sidechain_db - .db - .put((shard, signed_block.block().header().block_number()), signed_block.hash()) - .unwrap(); - sidechain_db.db.put((LAST_BLOCK_KEY, shard), last_block.clone()).unwrap(); - assert_eq!( - sidechain_db - .db - .get::<(ShardIdentifier, BlockNumber), H256>(( - shard, - signed_block.block().header().block_number() - )) - .unwrap() - .unwrap(), - signed_block.hash() - ); - assert_eq!( - sidechain_db.db.get::(signed_block.hash()).unwrap().unwrap(), - signed_block - ); - assert_eq!( - sidechain_db - .db - .get::<(&[u8], ShardIdentifier), LastSidechainBlock>((LAST_BLOCK_KEY, shard)) - .unwrap() - .unwrap(), - last_block - ); - - // when - let mut batch = WriteBatch::default(); - sidechain_db.delete_last_block(&mut batch, &last_block, &shard); - sidechain_db.db.write(batch).unwrap(); - - // then - assert!(sidechain_db.last_blocks.get(&shard).is_none()); - assert!(sidechain_db - .db - .get::<(ShardIdentifier, BlockNumber), H256>(( - shard, - signed_block.block().header().block_number() - )) - .unwrap() - .is_none()); - assert!(sidechain_db - .db - .get::(signed_block.hash()) - .unwrap() - .is_none()); - assert!(sidechain_db - .db - .get::<(&[u8], ShardIdentifier), LastSidechainBlock>((LAST_BLOCK_KEY, shard)) - .unwrap() - .is_none()); - } - } - - #[test] - fn verify_block_ancestry_returns_true_if_correct_successor() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - let signed_block_two = create_signed_block(9, shard); - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.shards.push(shard); - sidechain_db.last_blocks.insert(shard, last_block); - // when - let result = sidechain_db.verify_block_ancestry(&signed_block_two.block()); - - // then - assert!(result); - } - } - - #[test] - fn verify_block_ancestry_returns_false_if_not_correct_successor() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - let signed_block_two = create_signed_block(5, shard); - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.shards.push(shard); - sidechain_db.last_blocks.insert(shard, last_block); - - // when - let result = sidechain_db.verify_block_ancestry(&signed_block_two.block()); - - // then - assert!(!result); - } - } - - #[test] - fn verify_block_ancestry_returns_false_no_last_block_registered() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.shards.push(shard); - // when - let result = sidechain_db.verify_block_ancestry(&signed_block.block()); - - // then - assert!(!result); - } - } - - #[test] - fn verify_block_ancestry_returns_false_if_no_shard() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - { - let sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let result = sidechain_db.verify_block_ancestry(&signed_block.block()); - assert!(!result); - } - } - - #[test] - fn add_block_to_batch_works_with_new_shard() { - // given - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let mut new_shard = false; - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let mut batch = WriteBatch::default(); - assert!(batch.is_empty()); - - sidechain_db - .add_block_to_batch(&signed_block, &mut new_shard, &mut batch) - .unwrap(); - - assert!(new_shard); - assert!(!batch.is_empty()); - } - } - - #[test] - fn add_block_to_batch_does_not_add_shard_if_existent() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - let signed_block_two = create_signed_block(9, shard); - let mut new_shard = false; - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let mut batch = WriteBatch::default(); - assert!(batch.is_empty()); - sidechain_db.shards.push(shard); - sidechain_db.last_blocks.insert(shard, last_block); - - sidechain_db - .add_block_to_batch(&signed_block_two, &mut new_shard, &mut batch) - .unwrap(); - - assert!(!new_shard); - assert!(!batch.is_empty()); - } - } - - #[test] - fn add_block_to_batch_does_not_add_block_if_not_ancestor() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(8, shard); - let last_block = LastSidechainBlock { - hash: signed_block.hash(), - number: signed_block.block().header().block_number(), - }; - let signed_block_two = create_signed_block(10, shard); - let mut new_shard = false; - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let mut batch = WriteBatch::default(); - sidechain_db.shards.push(shard); - sidechain_db.last_blocks.insert(shard, last_block); - - let result = - sidechain_db.add_block_to_batch(&signed_block_two, &mut new_shard, &mut batch); - - assert!(result.is_err()); - assert!(!new_shard); - assert!(batch.is_empty()); - } - } - - #[test] - fn store_block_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block = create_signed_block(20, shard); - let signed_block_vector: Vec = vec![signed_block.clone()]; - - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // db needs to start empty - assert_eq!(sidechain_db.shards, vec![]); - sidechain_db.store_blocks(signed_block_vector).unwrap(); - } - - { - // open new DB of same path: - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // ensure DB contains previously stored data: - assert_eq!(*updated_sidechain_db.shards(), vec![shard]); - let last_block = updated_sidechain_db.last_block_of_shard(&shard).unwrap(); - assert_eq!(last_block.number, signed_block.block().header().block_number()); - assert_eq!(last_block.hash, signed_block.hash()); - let stored_block_hash = - updated_sidechain_db.get_block_hash(&shard, last_block.number).unwrap().unwrap(); - assert_eq!(stored_block_hash, signed_block.hash()); - assert_eq!( - updated_sidechain_db.get_block(&stored_block_hash).unwrap().unwrap(), - signed_block - ); - } - } - - #[test] - fn store_blocks_on_multi_sharding_works() { - let temp_dir = create_temp_dir(); - let shard_one = H256::from_low_u64_be(1); - let shard_two = H256::from_low_u64_be(2); - let signed_block_one = create_signed_block(20, shard_one); - let signed_block_two = create_signed_block(1, shard_two); - - let signed_block_vector: Vec = - vec![signed_block_one.clone(), signed_block_two.clone()]; - - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // db needs to start empty - assert_eq!(sidechain_db.shards, vec![]); - sidechain_db.store_blocks(signed_block_vector).unwrap(); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - assert_eq!(updated_sidechain_db.shards()[0], shard_one); - assert_eq!(updated_sidechain_db.shards()[1], shard_two); - let last_block_one: &LastSidechainBlock = - updated_sidechain_db.last_blocks.get(&shard_one).unwrap(); - let last_block_two: &LastSidechainBlock = - updated_sidechain_db.last_blocks.get(&shard_two).unwrap(); - assert_eq!(last_block_one.number, 20); - assert_eq!(last_block_two.number, 1); - assert_eq!(last_block_one.hash, signed_block_one.hash()); - assert_eq!(last_block_two.hash, signed_block_two.hash()); - } - } - - #[test] - fn store_mulitple_block_on_one_shard_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block_one = create_signed_block(20, shard); - let signed_block_two = create_signed_block(21, shard); - let signed_block_vector_one = vec![signed_block_one.clone()]; - let signed_block_vector_two = vec![signed_block_two.clone()]; - - { - // first iteration - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(signed_block_vector_one).unwrap(); - } - { - // second iteration - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(signed_block_vector_two).unwrap(); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // last block is really equal to second block: - let last_block: &LastSidechainBlock = - updated_sidechain_db.last_blocks.get(&shard).unwrap(); - assert_eq!(last_block.number, 21); - // storage contains both blocks: - // (shard,blocknumber) -> blockhash - let db_block_hash_one = - updated_sidechain_db.get_block_hash(&shard, 20).unwrap().unwrap(); - let db_block_hash_two = - updated_sidechain_db.get_block_hash(&shard, 21).unwrap().unwrap(); - assert_eq!(db_block_hash_one, signed_block_one.hash()); - assert_eq!(db_block_hash_two, signed_block_two.hash()); - - // block hash -> signed block - let db_block_one = - updated_sidechain_db.get_block(&signed_block_one.hash()).unwrap().unwrap(); - let db_block_two = - updated_sidechain_db.get_block(&signed_block_two.hash()).unwrap().unwrap(); - assert_eq!(db_block_one, signed_block_one); - assert_eq!(db_block_two, signed_block_two); - } - } - - #[test] - fn wrong_succession_order_does_not_get_accepted() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block_one = create_signed_block(7, shard); - let signed_block_two = create_signed_block(21, shard); - let signed_block_vector_one = vec![signed_block_one.clone()]; - let signed_block_vector_two = vec![signed_block_two.clone()]; - - { - // first iteration - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(signed_block_vector_one).unwrap(); - } - { - // second iteration - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(signed_block_vector_two).unwrap(); - } - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // last block is equal to first block: - let last_block: &LastSidechainBlock = - updated_sidechain_db.last_blocks.get(&shard).unwrap(); - assert_eq!(last_block.number, signed_block_one.block().header().block_number()); - - // storage contains only one blocks: - // (shard,blocknumber) -> blockhash - let db_block_hash_one = updated_sidechain_db - .get_block_hash(&shard, signed_block_one.block().header().block_number()) - .unwrap() - .unwrap(); - let db_block_hash_empty = updated_sidechain_db - .get_block_hash(&shard, signed_block_two.block().header().block_number()) - .unwrap(); - assert!(db_block_hash_empty.is_none()); - assert_eq!(db_block_hash_one, signed_block_one.hash()); - } - } - - #[test] - fn get_previous_block_returns_correct_block() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let signed_block_one = create_signed_block(1, shard); - // create sidechain_db - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![signed_block_one.clone()]).unwrap(); - // create last block one for comparison - let last_block = LastSidechainBlock { - hash: signed_block_one.hash(), - number: signed_block_one.block().header().block_number(), - }; - - // then - let some_block = sidechain_db - .get_previous_block(&shard, signed_block_one.block().header().block_number() + 1) - .unwrap() - .unwrap(); - - // when - assert_eq!(some_block, last_block); - } - } - - #[test] - fn get_previous_block_returns_none_when_no_block() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - { - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![create_signed_block(1, shard)]).unwrap(); - - let no_block = sidechain_db.get_previous_block(&shard, 1).unwrap(); - - assert!(no_block.is_none()); - } - } - - #[test] - fn purge_shard_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let block_one = create_signed_block(1, shard); - let block_two = create_signed_block(2, shard); - let block_three = create_signed_block(3, shard); - { - // create sidechain_db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![block_one.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_two.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_three.clone()]).unwrap(); - - sidechain_db.purge_shard(&shard).unwrap(); - - // test if local storage has been cleansed - assert!(!sidechain_db.shards.contains(&shard)); - assert!(sidechain_db.last_blocks.get(&shard).is_none()); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // test if local storage is still clean - assert!(!updated_sidechain_db.shards.contains(&shard)); - assert!(updated_sidechain_db.last_blocks.get(&shard).is_none()); - // test if db is clean - assert!(updated_sidechain_db.last_block_of_shard(&shard).is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 3).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 2).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 1).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_one.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_two.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_three.hash()).unwrap().is_none()); - } - } - - #[test] - fn purge_shard_from_block_works() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let block_one = create_signed_block(1, shard); - let block_two = create_signed_block(2, shard); - let block_three = create_signed_block(3, shard); - let last_block = LastSidechainBlock { - hash: block_three.hash(), - number: block_three.block().header().block_number(), - }; - - { - // create sidechain_db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![block_one.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_two.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_three.clone()]).unwrap(); - - sidechain_db.prune_shard_from_block_number(&shard, 2).unwrap(); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // test local memory - assert!(updated_sidechain_db.shards.contains(&shard)); - assert_eq!(*updated_sidechain_db.last_blocks.get(&shard).unwrap(), last_block); - // assert block three is still there - assert_eq!(*updated_sidechain_db.last_block_of_shard(&shard).unwrap(), last_block); - assert_eq!( - updated_sidechain_db.get_block_hash(&shard, 3).unwrap().unwrap(), - block_three.hash() - ); - assert_eq!( - updated_sidechain_db.get_block(&block_three.hash()).unwrap().unwrap(), - block_three - ); - // assert the lower blocks have been purged - assert!(updated_sidechain_db.get_block_hash(&shard, 2).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 1).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_two.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_one.hash()).unwrap().is_none()); - } - } - - #[test] - fn purge_shard_from_block_works_for_last_block() { - let temp_dir = create_temp_dir(); - let shard = H256::from_low_u64_be(1); - let block_one = create_signed_block(1, shard); - let block_two = create_signed_block(2, shard); - let block_three = create_signed_block(3, shard); - { - // create sidechain_db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![block_one.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_two.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_three.clone()]).unwrap(); - - sidechain_db.prune_shard_from_block_number(&shard, 3).unwrap(); - - // test if local storage has been cleansed - assert!(!sidechain_db.shards.contains(&shard)); - assert!(sidechain_db.last_blocks.get(&shard).is_none()); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // test if local storage is still clean - assert!(!updated_sidechain_db.shards.contains(&shard)); - assert!(updated_sidechain_db.last_blocks.get(&shard).is_none()); - // test if db is clean - assert!(updated_sidechain_db.last_block_of_shard(&shard).is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 3).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 2).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard, 1).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_one.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_two.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_three.hash()).unwrap().is_none()); - } - } - - #[test] - fn prune_shards_works_for_multiple_shards() { - let temp_dir = create_temp_dir(); - // shard one - let shard_one = H256::from_low_u64_be(1); - let block_one = create_signed_block(1, shard_one); - let block_two = create_signed_block(2, shard_one); - let block_three = create_signed_block(3, shard_one); - let last_block_one = LastSidechainBlock { - hash: block_three.hash(), - number: block_three.block().header().block_number(), - }; - // shard two - let shard_two = H256::from_low_u64_be(2); - let block_one_s = create_signed_block(1, shard_two); - let block_two_s = create_signed_block(2, shard_two); - let block_three_s = create_signed_block(3, shard_two); - let block_four_s = create_signed_block(4, shard_two); - let last_block_two = LastSidechainBlock { - hash: block_four_s.hash(), - number: block_four_s.block().header().block_number(), - }; - { - // create sidechain_db - let mut sidechain_db = get_storage(temp_dir.path().to_path_buf()); - sidechain_db.store_blocks(vec![block_one.clone(), block_one_s.clone()]).unwrap(); - sidechain_db.store_blocks(vec![block_two.clone(), block_two_s.clone()]).unwrap(); - sidechain_db - .store_blocks(vec![block_three.clone(), block_three_s.clone()]) - .unwrap(); - sidechain_db.store_blocks(vec![block_four_s.clone()]).unwrap(); - - sidechain_db.prune_shards(2); - } - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - // test if shard one has been cleansed of block 1, with 2 and 3 still beeing there: - assert_eq!( - *updated_sidechain_db.last_block_of_shard(&shard_one).unwrap(), - last_block_one - ); - assert_eq!( - updated_sidechain_db.get_block_hash(&shard_one, 3).unwrap().unwrap(), - block_three.hash() - ); - assert_eq!( - updated_sidechain_db.get_block(&block_three.hash()).unwrap().unwrap(), - block_three - ); - assert_eq!( - updated_sidechain_db.get_block_hash(&shard_one, 2).unwrap().unwrap(), - block_two.hash() - ); - assert_eq!( - updated_sidechain_db.get_block(&block_two.hash()).unwrap().unwrap(), - block_two - ); - assert!(updated_sidechain_db.get_block(&block_one.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard_one, 1).unwrap().is_none()); - // test if shard two has been cleansed of block 1 and 2, with 3 and 4 still beeing there: - assert_eq!( - *updated_sidechain_db.last_block_of_shard(&shard_two).unwrap(), - last_block_two - ); - assert_eq!( - updated_sidechain_db.get_block_hash(&shard_two, 4).unwrap().unwrap(), - block_four_s.hash() - ); - assert_eq!( - updated_sidechain_db.get_block(&block_four_s.hash()).unwrap().unwrap(), - block_four_s - ); - assert_eq!( - updated_sidechain_db.get_block_hash(&shard_two, 3).unwrap().unwrap(), - block_three_s.hash() - ); - assert_eq!( - updated_sidechain_db.get_block(&block_three_s.hash()).unwrap().unwrap(), - block_three_s - ); - assert!(updated_sidechain_db.get_block_hash(&shard_two, 2).unwrap().is_none()); - assert!(updated_sidechain_db.get_block_hash(&shard_two, 1).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_one_s.hash()).unwrap().is_none()); - assert!(updated_sidechain_db.get_block(&block_two_s.hash()).unwrap().is_none()); - } - } -} diff --git a/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_after.rs b/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_after.rs deleted file mode 100644 index 0795e54ca7..0000000000 --- a/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_after.rs +++ /dev/null @@ -1,124 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - error::Error, - test_utils::{ - create_signed_block_with_parenthash as create_signed_block, default_shard, - fill_storage_with_blocks, get_storage, - }, -}; -use its_primitives::{traits::SignedBlock, types::BlockHash}; -use std::assert_matches::assert_matches; - -#[test] -fn get_blocks_after_works_for_regular_case() { - let block_1 = create_signed_block(1, BlockHash::default()); - let block_2 = create_signed_block(2, block_1.hash()); - let block_3 = create_signed_block(3, block_2.hash()); - let block_4 = create_signed_block(4, block_3.hash()); - - let temp_dir = - fill_storage_with_blocks(vec![block_1.clone(), block_2.clone(), block_3, block_4.clone()]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let blocks_after_1 = updated_sidechain_db - .get_blocks_after(&block_1.hash(), &default_shard()) - .unwrap(); - - assert_eq!(3, blocks_after_1.len()); - assert_eq!(block_2.hash(), blocks_after_1.first().unwrap().hash()); - assert_eq!(block_4.hash(), blocks_after_1.last().unwrap().hash()); - } -} - -#[test] -fn get_blocks_after_returns_empty_vec_if_block_not_found() { - let block_1 = create_signed_block(1, BlockHash::random()); - - let temp_dir = fill_storage_with_blocks(vec![block_1.clone()]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let block_hash = BlockHash::from_low_u64_be(1); - // Off-chance that random() generates exactly the same hash - assert_ne!(block_1.hash(), block_hash); - - assert_eq!( - updated_sidechain_db.get_blocks_after(&block_hash, &default_shard()).unwrap(), - Vec::new() - ); - } -} - -#[test] -fn get_blocks_returns_none_if_last_is_already_most_recent_block() { - let block_1 = create_signed_block(1, BlockHash::random()); - - let temp_dir = fill_storage_with_blocks(vec![block_1.clone()]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - - assert_eq!( - updated_sidechain_db - .get_blocks_after(&block_1.hash(), &default_shard()) - .unwrap(), - Vec::new() - ); - } -} - -#[test] -fn get_blocks_after_returns_all_blocks_if_last_known_is_default() { - let block_1 = create_signed_block(1, BlockHash::default()); - let block_2 = create_signed_block(2, block_1.hash()); - let block_3 = create_signed_block(3, block_2.hash()); - - let blocks = vec![block_1.clone(), block_2.clone(), block_3.clone()]; - - let temp_dir = fill_storage_with_blocks(blocks.clone()); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let default_hash = BlockHash::default(); - - assert_eq!( - updated_sidechain_db.get_blocks_after(&default_hash, &default_shard()).unwrap(), - blocks - ); - } -} - -#[test] -fn given_block_with_invalid_ancestry_returns_error() { - let block_1 = create_signed_block(1, BlockHash::default()); - // Should be block_1 hash, but we deliberately introduce an invalid parent hash. - let block_2 = create_signed_block(2, BlockHash::random()); - - let temp_dir = fill_storage_with_blocks(vec![block_1.clone(), block_2]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - - assert_matches!( - updated_sidechain_db.get_blocks_after(&block_1.hash(), &default_shard()), - Err(Error::FailedToFindParentBlock) - ); - } -} diff --git a/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_in_range.rs b/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_in_range.rs deleted file mode 100644 index c0c505d4d0..0000000000 --- a/bitacross-worker/sidechain/storage/src/storage_tests_get_blocks_in_range.rs +++ /dev/null @@ -1,104 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::test_utils::{ - create_signed_block_with_parenthash as create_signed_block, default_shard, - fill_storage_with_blocks, get_storage, -}; -use itp_types::BlockHash; -use its_primitives::traits::SignedBlock; - -#[test] -fn get_blocks_in_range_works_for_regular_case() { - let block_1 = create_signed_block(1, BlockHash::default()); - let block_2 = create_signed_block(2, block_1.hash()); - let block_3 = create_signed_block(3, block_2.hash()); - let block_4 = create_signed_block(4, block_3.hash()); - let block_5 = create_signed_block(5, block_4.hash()); - - let temp_dir = fill_storage_with_blocks(vec![ - block_1.clone(), - block_2.clone(), - block_3, - block_4.clone(), - block_5.clone(), - ]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let blocks_2_to_4 = updated_sidechain_db - .get_blocks_in_range(&block_1.hash(), &block_5.hash(), &default_shard()) - .unwrap(); - - assert_eq!(3, blocks_2_to_4.len()); - assert_eq!(block_2.hash(), blocks_2_to_4.first().unwrap().hash()); - assert_eq!(block_4.hash(), blocks_2_to_4.last().unwrap().hash()); - } -} - -#[test] -fn get_blocks_in_range_returns_empty_vec_if_from_is_invalid() { - let block_1 = create_signed_block(1, BlockHash::default()); - let block_2 = create_signed_block(2, block_1.hash()); - let block_3 = create_signed_block(3, block_2.hash()); - let block_4 = create_signed_block(4, block_3.hash()); - - let temp_dir = fill_storage_with_blocks(vec![ - block_1.clone(), - block_2.clone(), - block_3.clone(), - block_4.clone(), - ]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let invalid_block_hash = BlockHash::from_low_u64_be(1); - - assert!(updated_sidechain_db - .get_blocks_in_range(&invalid_block_hash, &block_3.hash(), &default_shard()) - .unwrap() - .is_empty()); - } -} - -#[test] -fn get_blocks_in_range_returns_all_blocks_if_upper_bound_is_invalid() { - let block_1 = create_signed_block(1, BlockHash::default()); - let block_2 = create_signed_block(2, block_1.hash()); - let block_3 = create_signed_block(3, block_2.hash()); - let block_4 = create_signed_block(4, block_3.hash()); - let block_5 = create_signed_block(5, block_4.hash()); - - let temp_dir = fill_storage_with_blocks(vec![ - block_1.clone(), - block_2.clone(), - block_3.clone(), - block_4.clone(), - block_5.clone(), - ]); - - { - let updated_sidechain_db = get_storage(temp_dir.path().to_path_buf()); - let blocks_in_range = updated_sidechain_db - .get_blocks_in_range(&block_2.hash(), &BlockHash::from_low_u64_be(1), &default_shard()) - .unwrap(); - - assert_eq!(3, blocks_in_range.len()); - assert_eq!(block_3.hash(), blocks_in_range.first().unwrap().hash()); - assert_eq!(block_5.hash(), blocks_in_range.last().unwrap().hash()); - } -} diff --git a/bitacross-worker/sidechain/storage/src/test_utils.rs b/bitacross-worker/sidechain/storage/src/test_utils.rs deleted file mode 100644 index c0c7d8fdd8..0000000000 --- a/bitacross-worker/sidechain/storage/src/test_utils.rs +++ /dev/null @@ -1,96 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::storage::SidechainStorage; -use itp_time_utils::now_as_millis; -use itp_types::ShardIdentifier; -use its_primitives::types::{BlockHash, SignedBlock as SignedSidechainBlock}; -use its_test::{ - sidechain_block_builder::{SidechainBlockBuilder, SidechainBlockBuilderTrait}, - sidechain_block_data_builder::SidechainBlockDataBuilder, - sidechain_header_builder::SidechainHeaderBuilder, -}; -use sp_core::{crypto::Pair, ed25519, H256}; -use std::{path::PathBuf, vec::Vec}; -use temp_dir::TempDir; - -pub fn fill_storage_with_blocks(blocks: Vec) -> TempDir { - let dir = create_temp_dir(); - let mut sidechain_db = get_storage(dir.path().to_path_buf()); - sidechain_db.store_blocks(blocks).unwrap(); - dir -} - -pub fn create_temp_dir() -> TempDir { - TempDir::new().unwrap() -} - -pub fn get_storage(path: PathBuf) -> SidechainStorage { - SidechainStorage::::load_from_base_path(path).unwrap() -} - -pub fn default_shard() -> ShardIdentifier { - ShardIdentifier::default() -} - -pub fn create_signed_block_with_parenthash( - block_number: u64, - parent_hash: BlockHash, -) -> SignedSidechainBlock { - let header = default_header_builder() - .with_parent_hash(parent_hash) - .with_block_number(block_number) - .build(); - - let block_data = default_block_data_builder().build(); - - SidechainBlockBuilder::default() - .with_header(header) - .with_block_data(block_data) - .build_signed() -} - -pub fn create_signed_block_with_shard( - block_number: u64, - shard: ShardIdentifier, -) -> SignedSidechainBlock { - let header = default_header_builder() - .with_shard(shard) - .with_block_number(block_number) - .build(); - - let block_data = default_block_data_builder().build(); - - SidechainBlockBuilder::default() - .with_header(header) - .with_block_data(block_data) - .build_signed() -} - -fn default_header_builder() -> SidechainHeaderBuilder { - SidechainHeaderBuilder::default() - .with_parent_hash(H256::random()) - .with_block_number(Default::default()) - .with_shard(default_shard()) -} - -fn default_block_data_builder() -> SidechainBlockDataBuilder { - SidechainBlockDataBuilder::default() - .with_timestamp(now_as_millis()) - .with_layer_one_head(H256::random()) - .with_signer(ed25519::Pair::from_string("//Alice", None).unwrap()) -} diff --git a/bitacross-worker/sidechain/test/Cargo.toml b/bitacross-worker/sidechain/test/Cargo.toml deleted file mode 100644 index d80a6a825b..0000000000 --- a/bitacross-worker/sidechain/test/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "its-test" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -homepage = 'https://litentry.com/' -repository = 'https://github.com/litentry/litentry-parachain' -license = "Apache-2.0" -edition = "2021" - -[dependencies] - -# sgx dependencies -sgx_tstd = { branch = "master", git = "https://github.com/apache/teaclave-sgx-sdk.git", features = ["untrusted_time"], optional = true } - -# Substrate dependencies -sp-core = { default_features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# local -itp-types = { path = "../../core-primitives/types", default_features = false } -its-primitives = { path = "../primitives", default_features = false, features = ["full_crypto"] } - -[features] -default = ["std"] -std = [ - "itp-types/std", - "its-primitives/std", - # substrate - "sp-core/std", -] -sgx = [ - "sgx_tstd", -] diff --git a/bitacross-worker/sidechain/test/src/lib.rs b/bitacross-worker/sidechain/test/src/lib.rs deleted file mode 100644 index e9164d6d8b..0000000000 --- a/bitacross-worker/sidechain/test/src/lib.rs +++ /dev/null @@ -1,29 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - 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. -*/ - -#![feature(trait_alias)] -#![cfg_attr(not(feature = "std"), no_std)] - -#[cfg(all(feature = "std", feature = "sgx"))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); - -#[cfg(all(not(feature = "std"), not(feature = "sgx")))] -compile_error!("feature \"std\" and feature \"sgx\" cannot be disabled at the same time"); - -#[cfg(all(not(feature = "std"), feature = "sgx"))] -#[macro_use] -extern crate sgx_tstd as std; - -pub mod sidechain_block_builder; -pub mod sidechain_block_data_builder; -pub mod sidechain_header_builder; diff --git a/bitacross-worker/sidechain/test/src/sidechain_block_builder.rs b/bitacross-worker/sidechain/test/src/sidechain_block_builder.rs deleted file mode 100644 index 1261cf51bc..0000000000 --- a/bitacross-worker/sidechain/test/src/sidechain_block_builder.rs +++ /dev/null @@ -1,105 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -//! Builder pattern for a signed sidechain block. - -use crate::{ - sidechain_block_data_builder::SidechainBlockDataBuilder, - sidechain_header_builder::SidechainHeaderBuilder, -}; -use its_primitives::{ - traits::{Block as BlockT, SignBlock}, - types::{block_data::BlockData, header::SidechainHeader as Header, Block, SignedBlock}, -}; -use sp_core::{ed25519, Pair}; - -type Seed = [u8; 32]; -const ENCLAVE_SEED: Seed = *b"12345678901234567890123456789012"; - -#[derive(Clone)] -pub struct SidechainBlockBuilder { - signer: ed25519::Pair, - header: Header, - block_data: BlockData, -} - -impl Default for SidechainBlockBuilder { - fn default() -> Self { - SidechainBlockBuilder { - signer: Pair::from_seed(&ENCLAVE_SEED), - header: SidechainHeaderBuilder::default().build(), - block_data: SidechainBlockDataBuilder::default().build(), - } - } -} - -pub trait SidechainBlockBuilderTrait { - type Block: BlockT; - fn random() -> Self; - fn with_header(self, header: Header) -> Self; - fn with_block_data(self, block_data: BlockData) -> Self; - fn with_signer(self, signer: ed25519::Pair) -> Self; - fn build(&self) -> Self::Block; - fn build_signed(&self) -> SignedBlock; -} - -impl SidechainBlockBuilderTrait for SidechainBlockBuilder { - type Block = Block; - fn random() -> Self { - SidechainBlockBuilder { - signer: Pair::from_seed(&ENCLAVE_SEED), - header: SidechainHeaderBuilder::random().build(), - block_data: SidechainBlockDataBuilder::random().build(), - } - } - - fn with_header(self, header: Header) -> Self { - let mut self_mut = self; - self_mut.header = header; - self_mut - } - - fn with_block_data(self, block_data: BlockData) -> Self { - let mut self_mut = self; - self_mut.block_data = block_data; - self_mut - } - - fn with_signer(self, signer: ed25519::Pair) -> Self { - let mut self_mut = self; - self_mut.signer = signer; - self_mut - } - - fn build(&self) -> Self::Block { - Block { header: self.header, block_data: self.block_data.clone() } - } - - fn build_signed(&self) -> SignedBlock { - let signer = self.signer; - self.build().sign_block(&signer) - } -} - -#[test] -fn build_signed_block_has_valid_signature() { - use its_primitives::traits::SignedBlock as SignedBlockTrait; - - let signed_block = SidechainBlockBuilder::default().build_signed(); - assert!(signed_block.verify_signature()); -} diff --git a/bitacross-worker/sidechain/test/src/sidechain_block_data_builder.rs b/bitacross-worker/sidechain/test/src/sidechain_block_data_builder.rs deleted file mode 100644 index e1197ce65c..0000000000 --- a/bitacross-worker/sidechain/test/src/sidechain_block_data_builder.rs +++ /dev/null @@ -1,102 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -//! Builder pattern for sidechain block data. - -use itp_types::H256; -use its_primitives::types::{ - block::{BlockHash, Timestamp}, - block_data::BlockData, -}; -use sp_core::{ed25519, Pair}; -use std::{time::SystemTime, vec}; - -type Seed = [u8; 32]; -const ENCLAVE_SEED: Seed = *b"12345678901234567890123456789012"; - -pub struct SidechainBlockDataBuilder { - timestamp: Timestamp, - layer_one_head: H256, - signer: ed25519::Pair, - signed_top_hashes: Vec, - encrypted_state_diff: Vec, -} - -impl Default for SidechainBlockDataBuilder { - fn default() -> Self { - SidechainBlockDataBuilder { - timestamp: Default::default(), - layer_one_head: Default::default(), - signer: Pair::from_seed(&ENCLAVE_SEED), - signed_top_hashes: Default::default(), - encrypted_state_diff: Default::default(), - } - } -} - -impl SidechainBlockDataBuilder { - pub fn random() -> Self { - SidechainBlockDataBuilder { - timestamp: now_as_millis(), - layer_one_head: BlockHash::random(), - signer: Pair::from_seed(&ENCLAVE_SEED), - signed_top_hashes: vec![H256::random(), H256::random()], - encrypted_state_diff: vec![1, 3, 42, 8, 11, 33], - } - } - - pub fn with_timestamp(mut self, timestamp: Timestamp) -> Self { - self.timestamp = timestamp; - self - } - - pub fn with_signer(mut self, signer: ed25519::Pair) -> Self { - self.signer = signer; - self - } - - pub fn with_layer_one_head(mut self, layer_one_head: H256) -> Self { - self.layer_one_head = layer_one_head; - self - } - - pub fn with_signed_top_hashes(mut self, signed_top_hashes: Vec) -> Self { - self.signed_top_hashes = signed_top_hashes; - self - } - - pub fn with_payload(mut self, payload: Vec) -> Self { - self.encrypted_state_diff = payload; - self - } - - pub fn build(self) -> BlockData { - BlockData { - timestamp: self.timestamp, - block_author: self.signer.public(), - layer_one_head: self.layer_one_head, - signed_top_hashes: self.signed_top_hashes, - encrypted_state_diff: self.encrypted_state_diff, - } - } -} - -/// gets the timestamp of the block as seconds since unix epoch -fn now_as_millis() -> u64 { - SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).unwrap().as_millis() as u64 -} diff --git a/bitacross-worker/sidechain/test/src/sidechain_header_builder.rs b/bitacross-worker/sidechain/test/src/sidechain_header_builder.rs deleted file mode 100644 index fca8b52b8c..0000000000 --- a/bitacross-worker/sidechain/test/src/sidechain_header_builder.rs +++ /dev/null @@ -1,92 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - Copyright (C) 2017-2019 Baidu, Inc. All Rights Reserved. - - 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. - -*/ - -//! Builder pattern for a sidechain header. - -use its_primitives::types::{header::SidechainHeader as Header, ShardIdentifier}; -use sp_core::H256; - -pub struct SidechainHeaderBuilder { - parent_hash: H256, - block_number: u64, - shard_id: ShardIdentifier, - block_data_hash: H256, - next_finalization_block_number: u64, -} - -impl Default for SidechainHeaderBuilder { - fn default() -> Self { - SidechainHeaderBuilder { - parent_hash: Default::default(), - block_number: 1, - shard_id: Default::default(), - block_data_hash: Default::default(), - next_finalization_block_number: 1, - } - } -} - -impl SidechainHeaderBuilder { - pub fn random() -> Self { - SidechainHeaderBuilder { - parent_hash: H256::random(), - block_number: 42, - shard_id: ShardIdentifier::random(), - block_data_hash: H256::random(), - next_finalization_block_number: 1, - } - } - - pub fn with_parent_hash(mut self, parent_hash: H256) -> Self { - self.parent_hash = parent_hash; - self - } - - pub fn with_block_number(mut self, block_number: u64) -> Self { - self.block_number = block_number; - self - } - - pub fn with_shard(mut self, shard_id: ShardIdentifier) -> Self { - self.shard_id = shard_id; - self - } - - pub fn with_block_data_hash(mut self, block_data_hash: H256) -> Self { - self.block_data_hash = block_data_hash; - self - } - - pub fn with_next_finalization_block_number( - mut self, - next_finalization_block_number: u64, - ) -> Self { - self.next_finalization_block_number = next_finalization_block_number; - self - } - - pub fn build(self) -> Header { - Header { - parent_hash: self.parent_hash, - block_number: self.block_number, - shard_id: self.shard_id, - block_data_hash: self.block_data_hash, - next_finalization_block_number: self.next_finalization_block_number, - } - } -} diff --git a/bitacross-worker/sidechain/validateer-fetch/Cargo.toml b/bitacross-worker/sidechain/validateer-fetch/Cargo.toml deleted file mode 100644 index 2df88400d5..0000000000 --- a/bitacross-worker/sidechain/validateer-fetch/Cargo.toml +++ /dev/null @@ -1,35 +0,0 @@ -[package] -name = "its-validateer-fetch" -version = "0.9.0" -authors = ['Trust Computing GmbH ', 'Integritee AG '] -edition = "2021" - -[dependencies] -codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive", "chain-error"] } -derive_more = "0.99.16" - -# substrate deps -sp-core = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-runtime = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } - -# local deps -itp-ocall-api = { path = "../../core-primitives/ocall-api", default-features = false } -itp-types = { path = "../../core-primitives/types", default-features = false } -lc-teebag-storage = { path = "../../litentry/core/teebag-storage", default-features = false } - -[features] -default = ["std"] -std = [ - "codec/std", - "sp-core/std", - "sp-runtime/std", - "sp-std/std", - "itp-types/std", - "itp-ocall-api/std", - "lc-teebag-storage/std", -] - -[dev-dependencies] -itp-test = { path = "../../core-primitives/test" } -itc-parentchain-test = { path = "../../core/parentchain/test" } diff --git a/bitacross-worker/sidechain/validateer-fetch/src/error.rs b/bitacross-worker/sidechain/validateer-fetch/src/error.rs deleted file mode 100644 index 3b5d4a3f2c..0000000000 --- a/bitacross-worker/sidechain/validateer-fetch/src/error.rs +++ /dev/null @@ -1,27 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use derive_more::{Display, From}; - -pub type Result = core::result::Result; - -#[derive(Debug, Display, From)] -pub enum Error { - Codec(codec::Error), - Onchain(itp_ocall_api::Error), - Other(&'static str), -} diff --git a/bitacross-worker/sidechain/validateer-fetch/src/lib.rs b/bitacross-worker/sidechain/validateer-fetch/src/lib.rs deleted file mode 100644 index 8c68402101..0000000000 --- a/bitacross-worker/sidechain/validateer-fetch/src/lib.rs +++ /dev/null @@ -1,24 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#![cfg_attr(not(feature = "std"), no_std)] - -mod error; -mod validateer; - -pub use error::Error; -pub use validateer::*; diff --git a/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs b/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs deleted file mode 100644 index e61c7ab5ee..0000000000 --- a/bitacross-worker/sidechain/validateer-fetch/src/validateer.rs +++ /dev/null @@ -1,80 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::error::{Error, Result}; -use itp_ocall_api::EnclaveOnChainOCallApi; -use itp_types::{parentchain::ParentchainId, AccountId, WorkerType}; -use lc_teebag_storage::{TeebagStorage, TeebagStorageKeys}; -use sp_core::H256; -use sp_runtime::traits::Header as HeaderT; -use sp_std::prelude::Vec; - -pub trait ValidateerFetch { - fn current_validateers>( - &self, - latest_header: &Header, - ) -> Result>; - - fn validateer_count>(&self, latest_header: &Header) - -> Result; -} - -impl ValidateerFetch for OnchainStorage { - fn current_validateers>( - &self, - header: &Header, - ) -> Result> { - let identifiers = self - .get_storage_verified( - TeebagStorage::enclave_identifier(WorkerType::BitAcross), - header, - &ParentchainId::Litentry, - )? - .into_tuple() - .1 - .ok_or_else(|| Error::Other("Could not get validateer list from chain"))?; - Ok(identifiers) - } - - fn validateer_count>(&self, header: &Header) -> Result { - Ok(self.current_validateers::

(header)?.len() as u64) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use codec::Encode; - use itc_parentchain_test::ParentchainHeaderBuilder; - use itp_test::mock::onchain_mock::{validateer_set, OnchainMock}; - use std::string::ToString; - - #[test] - pub fn get_validateer_count_works() { - let header = ParentchainHeaderBuilder::default().build(); - let mock = OnchainMock::default().add_validateer_set(&header, None); - assert_eq!(mock.validateer_count(&header).unwrap(), 4u64); - } - - #[test] - pub fn get_validateer_set_works() { - let header = ParentchainHeaderBuilder::default().build(); - let mock = OnchainMock::default().add_validateer_set(&header, None); - let validateers = validateer_set(); - assert_eq!(mock.current_validateers(&header).unwrap(), validateers); - } -} diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index 61b4d162e8..c53e335759 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -14,7 +14,6 @@ function worker_clippy() { function bitacross_clippy() { cargo clippy --release -- -D warnings cargo clippy --release --features evm -- -D warnings - cargo clippy --release --features sidechain -- -D warnings cargo clippy --release --features offchain-worker -- -D warnings } From 08454ad8582ff3c807470c395b3f16cadfdff272 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Mon, 19 Feb 2024 23:01:45 +0100 Subject: [PATCH 43/64] A brief readme for bitacross worker (#2499) * README.md * Update bitacross-worker/README.md Co-authored-by: Jonathan Alvarez * Update bitacross-worker/README.md Co-authored-by: Jonathan Alvarez * Update bitacross-worker/README.md Co-authored-by: Jonathan Alvarez * Update bitacross-worker/README.md Co-authored-by: Jonathan Alvarez --------- Co-authored-by: Jonathan Alvarez --- bitacross-worker/README.md | 201 +++++++++++++++++++- bitacross-worker/assets/teebag_registry.gif | Bin 0 -> 1703551 bytes 2 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 bitacross-worker/assets/teebag_registry.gif diff --git a/bitacross-worker/README.md b/bitacross-worker/README.md index e2be743ff3..90b9f0d9c4 100755 --- a/bitacross-worker/README.md +++ b/bitacross-worker/README.md @@ -1 +1,200 @@ -# bitacross worker \ No newline at end of file +# BitAcross worker + +This repository contains code for BitAcross offchain worker. The main responsibility of the worker is to +store custodian wallets and sign transactions submitted by relayers. + +## Wallets + +Supported wallets: +* ethereum (ecdsa based on secp256k1) +* bitcoin (schnorr based on secp256k1) + +Wallets (private keys) are generated during the initialization (on first startup) and sealed to encrypted file using Intel Protected File System while public keys are published on parachain's bitacross pallet in compressed SEC1-encoded format. + + +## Transaction signing + +Signing requests are processed by a dedicated JSON-RPC `bitacross_submitRequest` method and results in raw signature bytes. Only requests signed by registered relayers are permitted. + +Typescript code related to the RPC integration and can be found in [tee-worker's ts-tests](https://github.com/litentry/litentry-parachain/blob/a6b78ed68396280655271f9cd30e17535d54da81/tee-worker/ts-tests/integration-tests/common/di-utils.ts). + +Rust code used in CLI module can also be used as a reference and can be found [here](https://github.com/litentry/litentry-parachain/blob/a6b78ed68396280655271f9cd30e17535d54da81/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/utils.rs). + + +### Step by step guide for request preparing/sending and response handling. + +1. Prepare `DirectCall`, for example `SignBitcoin` variant which will reflect bitcoin's transaction signing request. Generate 256-bit AES-GCM as request enc/dec key. The first parameter is relayer identity, second generated aes key and third is transaction payload to sign. + +```rust +pub enum DirectCall { + SignBitcoin(Identity, RequestAesKey, Vec), + SignEthereum(Identity, RequestAesKey, Vec), +} +``` + +2. Prepare `DirectCallSigned`. Scale encode created direct call from step 1, append scale encoded mrenclave and shard identifier (use mrenclave) to it, do a Blake2 256-bit hash of it and sign it using relayer's private key, then prepare struct containing direct call and signature. Mrenclave can be obtained from parachain's teebag pallet enclave registry storage. + +```Rust +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] +pub struct DirectCallSigned { + pub call: DirectCall, + pub signature: LitentryMultiSignature, +} +``` + +3. Prepare `AesRequest`. Fill payload property with aes encrypted scale encoded `DirectCallSigned` prepared in previous step. Get worker's shielding key and use it to encrypt aes key from step 1 and fill `key` property with it. Fill shard identifier (use mrenclave again). Shielding key can be obtained from parachain's teebag pallet enclave registry storage. + +```rust +pub struct AesRequest { + pub shard: ShardIdentifier, + pub key: Vec, + pub payload: AesOutput, +} +``` + +4. Prepare `RpcRequest`. Scale encode `AesRequest` prepared in previous step, turn the bytes into hex representation and put in `params` vec. Generate locally unique `id`. Use following consts for `jsonrpc` and `method`: `"2.0"`, `bitacross_submitRequest` + +```rust +pub struct RpcRequest { + pub jsonrpc: String, + pub method: String, + pub params: Vec, + pub id: Id, +} +``` + +5. Send prepared request to worker's jsonrpc endpoint, in the following example `websocat` is used to send request to locally run bitacross worker. + +```bash +echo '{"jsonrpc":"2.0","method":"bitacross_submitRequest","params":["0x6565c4529cd2af40f89e5d526c6e890019a2fd33cfdc9ee3cd14a0bf1427a61601065c22cde40abe4ad0550a4beba5d05a55380117a57824a57c5949a472fb0639d1ebb1baff0f5453e222418844044ed75352f9a76b4f3fd57f8db4deabf4074eb552784b32c1a881ac27d143148e06a3607455ebafb7dd3ab1669013502bfd7b840d6698363015f55fede5275dfe7d05827315301772e4b75bf745f74b71c443b97b7d22010d54b89fcc1105cbfc72a58dfbd4c10e34ef6019dad859abafdb4f82118f5f339255cb5d2400243bc2e982b4c60341572b6253e0815ed90de74b64145aef8d8304a576ba11c73421b9c86a053619908c475be5d223acc942460afb7e248836f58d2e639d3e32365bbc7ba9fe838b3329db6432fce3427569523f513e7cc82098db4ccaf024a286ad94e6be775ba1f9e918f0867e20a8dbb409232ba297878eff52740e705f59dab2a1c5827d1f8bf7adfa7cdf9e345c16fda757016337f398201af14c820782dac82bc9c5f8df93c917cba29f89e5a1e323dafcf2465e258f1d6dcf9808e5202e6fa3766433981f619c580b831c0d49eed759a0ca1555021c688b72490ffd3f4391c60c04ba904d83aa9497cce62eb6d0e55124692c5124fabfabd70ab366ba81d152f2299ba99021a3705754d64d2b9455229d6ecd730a120a1003abe432a060e40931ad9eb3199cbb09a6b2c84af35735b51628d80210369c0f902905f7e7902d6787673691f2e923b6bc001cfa56f3568e95a95f1f084cd69e658e42c96e317cebc17d54de13f08a0fb007008777e7510d0aa8d124271afe"],"id":1}' | websocat -n1 -k -B 99999999 wss://localhost:2000 +{"jsonrpc":"2.0","result":"0x7d014101d9197274039df1280452819ede02d0867aa57185251d19c9e2c74bd22d1f3b8e1db031b068e3ee7229631a804c1d03e2d9af7055851ac9609dae5e8c7c8dccf4961b31a5cc98bfa356fa262a7376525300a2c03d6f2b59e28578fdcee00000","id":1} +``` + +6. Get result from response `result` field. It's a hex representation of scale encoded `RpcReturnValue`. In case of success, the signature can be obtained from `value` property (it's aes encrypted). + +Types definitions: + +```rust +pub type RequestAesKey = [u8; 32]; + +#[derive( +Encode, Decode, Clone, Debug, PartialEq, Eq, TypeInfo, MaxEncodedLen, EnumIter, Ord, PartialOrd, +)] +pub enum Identity { + // web2 + #[codec(index = 0)] + Twitter(IdentityString), + #[codec(index = 1)] + Discord(IdentityString), + #[codec(index = 2)] + Github(IdentityString), + + // web3 + #[codec(index = 3)] + Substrate(Address32), + #[codec(index = 4)] + Evm(Address20), + // bitcoin addresses are derived (one-way hash) from the pubkey + // by using `Address33` as the Identity handle, it requires that pubkey + // is retrievable by the wallet API when verifying the bitcoin account. + // e.g. unisat-wallet: https://docs.unisat.io/dev/unisat-developer-service/unisat-wallet#getpublickey + #[codec(index = 5)] + Bitcoin(Address33), +} + +pub enum LitentryMultiSignature { + /// An Ed25519 signature. + #[codec(index = 0)] + Ed25519(ed25519::Signature), + /// An Sr25519 signature. + #[codec(index = 1)] + Sr25519(sr25519::Signature), + /// An ECDSA/SECP256k1 signature. + #[codec(index = 2)] + Ecdsa(ecdsa::Signature), + /// An ECDSA/keccak256 signature. An Ethereum signature. hash message with keccak256 + #[codec(index = 3)] + Ethereum(EthereumSignature), + /// Same as above, but the payload bytes are prepended with a readable prefix and `0x` + #[codec(index = 4)] + EthereumPrettified(EthereumSignature), + /// Bitcoin signed message, a hex-encoded string of original &[u8] message, without `0x` prefix + #[codec(index = 5)] + Bitcoin(BitcoinSignature), + /// Same as above, but the payload bytes are prepended with a readable prefix and `0x` + #[codec(index = 6)] + BitcoinPrettified(BitcoinSignature), +} + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq, Clone, Debug)] +pub struct EthereumSignature(pub [u8; 65]); + +#[derive(Encode, Decode, MaxEncodedLen, TypeInfo, PartialEq, Eq, Clone, Debug)] +pub struct BitcoinSignature(pub [u8; 65]); + +#[derive( +Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen, Ord, PartialOrd, +)] +pub struct Address20([u8; 20]); + +#[derive( +Encode, Decode, Copy, Clone, Default, PartialEq, Eq, TypeInfo, MaxEncodedLen, Ord, PartialOrd, +)] +pub struct Address32([u8; 32]); + +#[derive(Encode, Decode, Copy, Clone, PartialEq, Eq, TypeInfo, MaxEncodedLen, PartialOrd, Ord)] +pub struct Address33([u8; 33]); + +#[derive(Debug, Default, Clone, Eq, PartialEq, Encode, Decode)] +pub struct AesOutput { + pub ciphertext: Vec, + pub aad: Vec, + pub nonce: RequestAesKeyNonce, // IV +} + +#[derive(Encode, Decode, Debug, Eq, PartialEq)] +pub struct RpcReturnValue { + pub value: Vec, + pub do_watch: bool, + pub status: DirectRequestStatus, +} + +#[derive(Debug, Clone, PartialEq, Encode, Decode, Eq)] +pub enum DirectRequestStatus { + /// Direct request was successfully executed + #[codec(index = 0)] + Ok, + /// Trusted Call Status + /// Litentry: embed the top hash here - TODO - use generic type? + #[codec(index = 1)] + TrustedOperationStatus(TrustedOperationStatus, H256), + /// Direct request could not be executed + #[codec(index = 2)] + Error, +} +``` + +### Using CLI + +There are two commands related to transaction signing: + +* request-direct-call-sign-bitcoin +* request-direct-call-sign-ethereum + +They take single argument representing raw payload bytes to sign. + +#### Example usage + +```bash +./bitacross-cli trusted -m 7ppBUcnjGir4szRHCG59p2dTnbtRwKRbLZPpR32ACjbK request-direct-call-sign-bitcoin 00 +``` + +### Obtaining data from parachain's teebag pallet + +Mrencalve, worker's url and public shielding key can be obtained during the runtime from parachain's teebag pallet registry. + +The following gif ilustrates how it can be done manually: + +![demo](./assets/teebag_registry.gif) + +These values can also be obtained programmatically using substrate's `state_getStorage` RPC method. See [this](https://docs.substrate.io/build/remote-procedure-calls/) documentation for more information. \ No newline at end of file diff --git a/bitacross-worker/assets/teebag_registry.gif b/bitacross-worker/assets/teebag_registry.gif new file mode 100644 index 0000000000000000000000000000000000000000..b337fb6c3ae6a7694f440837f665ecd5f7a906f8 GIT binary patch literal 1703551 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

O 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
op&sn4#p@mx5 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

  • @x*KK=FgwLSj-Z^hnX z_W3hpg8#mGuLcAdqqd1aY!SE?PU_k)!!$PlA@kJ-RkM+Kx2CH0Ha9k@`co?xaJu%HFDhIH%{=z8FkV%pVTB*BFPN@SgH|?ZHeR= zM~RzX8B(-`a+Hx_T1A(bLD2blqY1Bx0RZKL z(%1Ch4$&CtR@#}LHNx~=EA?4RU)q+B+0iSGFj_}%<{%yZ@l?nZNFf%i64qm zp&}U6sn#z{$nf7K=abdfh(U5+bW|5>TAOUDW~*iqYXC+OgWh!im7O0Gh()tP2w+mP zKPmx4CGv1Os+L59R7y@UkNQA|!nJ;Q)Wxfe_|yyuaD`cbRD6=;1pR=(nTSAE0 zwa7Fqt>_J9UBnA;WHL4Xlp$L}8z0SjagaHT$sZ1J&?EuEeN)+8WI3WiL`VW5L)sjX z)OVzfaxegplS7B1xiC*u0=Kz+Zt7OF&CT`<1ZfRGIup_ZowCNdmyBINrviu5q?AUT z4HG!18RwDWimgKv-Y~U-7L<)iJI>6xx`&vd5QF5d}l2L|{h- z0HNz!kOLihzzzyxZm&|9LKh6(4SNi%sA0h-KdWB)pf)Q1^bm*{R4EX7uG6|$#Ox?W zQCjnUwR(>#F;q@GmR61dlQZ^;j8_ylhy8a)VtFiI18le8{xFb@gj~o(feHi<#1Kg4 zFqXwO(-mu_zEc5*3rAcg_QIu0>7`{Yy}Yp+PmIiL)aF^lOq(oorV^eAMJUd;VU;MD zIYLrSL}RkhJTVFp6s0qw3-{(l1M9que)NnIy%#3o87){os8rwtrwL1%qkW!=R6AWR zNRt{;jxIH-;U#Lh1fbIO(J+G>nm)u_dDT)ZsIAGB>Rqpk&Ak3Ki*mgbxF{6M#yx}} z0#S)dI6@PwKn#E?>|9~rIxNPc_O5`f?HHYU+unZv8@7w$V7E+r!@$vCg*6!Ng`(x! z>*f@%)%)o^Tw=0ya1KE0^ngXP*jz?up9&-TslVGioi-zk``pL!^#DM; zwNWRXSGTVCTp3t3i!adaMhfzQ8YGm_h%;RO`mRP7a(62*WMMZZzt^QBd@Yx!N+rJG zHhil#E#N4;?79g0f3`B=1_5WJs(zbdKfd>gk`1SQe;$3`R6oV1XA<@q7C7cq^K7Ym zTYlV{O@PCFqF?@&Ihp(wmDKP)?~U-oKepL0N&M(>6ffGkqSkAbM(XFbuW#da*QDNU zoo7wc>ym%1U~wM4_*LMoS=3FLZK$029pH%6h5;hrXUyC6sfL_6UsbSHq{UxtxEX6u zpKZ+s4ZPn~P+(Pb;8)Cpet;m24P9>t9Rsc)KKo_uv1d>s)RGlM0lK{g1AQN&O_%-2f;6@S7#|>E5epDf^>7Z2T()Qs8 zfE0n)sgptUASLP3Q+xy38H5V>(^Z^+H$32bRM8s3ATpSecVO~faA9lvD zOd(dJp?nbH+mRC*)!qz|-aHASS5$&^(P0?TAzHYBAxZ@ak{~wB!zRvyKLh|ah#U%T ziXQ%9Tw<>bUBz`wDJ~;A z?!+RdBcDWL#=sB;8jA(G<8AE!VlMI`HuxeaPNO$&Voc~FH}a#qd>?BR*t&0Hi(wBS3nhFZyCO8U#Obqc~E^D8@&qF(gU;1br-EN%q8BP~vO6 zqC}UZo38GxiY!hyBl2>q|UJArCFoa-&;y_@-)ICLJ3MI@10MOyaLeeHVs^2`W(I&Mlmb=+W^%^FaT)~MRZ7p{$4FjhO6Esw z>8E94Oj&LP?xC3g1wc4q-&CBQ5x$0kDy4bG$bd>0fdZ#}&E-}IBz_fVS6(PKbmlcS z=X(8SYuqP&Os9VyWqu3-FrDZW#mGz==2;xyYII*h$Y@s3Xz5`LcDe?Ras>+sl!fAl znr-GZ%x6{@sdKXb+CU-mOt7Cps8v{4X;on9S!k)Fz-XiFsNH#< zRKCkJ7H3v&1&CG*iBcJSsGyV{q>#mFZK}u_fvH%)nCJQEWm<)p9?oc;36e5SNWLjh z(J5Q%M;-(poq7p)5uQ}&rG9;d4W=hU+*f|>msrG?rS>TV{U_q^pqDtp2G(59L1}p+ z>OrDvj;Lx!Na54<*a%1;S76~*$SSRZg{^w3m^|p=R1tNF=tKG(QO=pGG9;@S>ailr zm(Y}1;HfvQ1+>bDusSGj@+v(=#fEwtViM!BuIhe#tEy~UwMOHg#>cd}3qXd`Mh@zm z6{=Z^YdR+X>wCZ{qGBt)j^4HAt5=xjRt6+S5+*hpsJjyC&?Vt}6r;R0A-%@yxAH5# zo@-q4V@-17XA-72Zlh0Tq&8k;Mh0ZhkmGXR#X*OvaCdULs0r_KsIN<_G4diBt=>z zXr5+IS|dhoWiG0V?m4W`uHt^orhg)B(t@hlMny;Vq{a4S%_fA*uBpxXV`=(j)oyH> z@)tRpWzdFg)e(Yhrfk|yrqU8ixuV6#g5`Y9LpXirFEVaecEdDMY{!~rPS#}QR-LTWfWx(Ps*8a*cVx4R# z=I0(`;EF5h+GXL|CCB3APflab=B(RxEzII$>*DS=POjYA=P=raiT*Ah60HJ?E=dk= zTo!N3KJ9USA#|G@}x~}xTZq0(_H)6x#I^Hlc%4{gC_C9HJ;)cDdD)>63_`W1z z`sB;7vE&w8fO!e(i+30Bcmf6>rU8G zGCgXsLv~{0S|r!5Z~^P=%PKJ^lkhD|@6=M`G`Pb+Aaa-rA6zi%DsOKMrv}+dDJ;9A zEYqW3ZlzA%qw`*E7!xqherOR3vii>Knigj_dt-5$iTLH-GNWT4G@fkq@3Kb!b0zz# zJdY+A7qURwa_hovH_mRy(yYKrF2^b^BG1DxpK|~dG$Nm}ANrmGp+)wtGX^Ki=jIPc z)pJ@*vpbe1UizzN4kj?4=H<%nH&gEeck?t%!_2zFCmM5^FrZlkkwhD09o0r2{O+68 z!h}wRUUkAwQ$@qHa$3yqI9l?e5p{A7G6dtKKwB;ON-YRqBQ{PiACq)Y>a0Suba4)| zKX7$HLmdMKLaAy6lg_l979~#SPbg}^1XS-94#eih#1?1)j#03V!NnHbHKb{D#)yCj zlygE*3F<iCNJG`WF~jA3RuT~CfO0%>!A~Z1zbe_pl0=F;`!9k%lF4W+FMGVFkiqg*D7rGelXRc1i|9 zA*=ywl%Mwc&pdzwZ)=4&Y=J<;HADEdKj`*UFtqIOb;ZzwU!U1~>@pCOW>5m-PJS~O zW9{8`<6iRdK3+FR7ITE7DMEKQ490{^2f$;RHhCj+O|OO=yXw!pcL0b0Y@^l$1OVXD zQhNtLTgOC-YXEPfmIw$0BAl2Rra0h!Lvv1m2GBzli#0G-!&) z1c|%2Kx%*BY74CfL`~kZ{tssXF)BThJrOYk`Kfxu#k@zcK}TJ zi6=ygyMmjgxLbomk7q$M$c7&5L8e6Ur&+fKvwLwpALU^@S7c72uxT8;8GoQr_ z`%ITm!IRf_03gB^O``_rbd3kVOUXe1Y=I7F0BnztiDvYZdp<1x_;hcwpCft~&+G%or!nKC zF{d;Mvb1Geb}`fUoIX0i^P50y+*=s!1)EJYGG+jn_gne3~0s&s`e1sx4Te*uLDDkZ?-_xLbGQ&FqArOC&ZA)M9*is))zu8fP=;(Rc%}Q zK!lJ8@ME)gdE}t)<0?2XPIeiO=D_x1@`m$hA~d2Kv_Yt}p?gWe_x-y8SX(SAhgT=r zgdz(Wfr9FiU|NDAk3DR6!^D&L(1ck+M1x;fFO?Yo!WON$*jv5lM|Fuu{mP5|rH_)~ z3k2eCJJW-r9l?6YhWYIPy+AbRuqOmCRKWy{5;#15d>%eF1Q7)XfSddHAw>PyLxmZd zp*=5gUoN-w?xgF!Y(8=$B3E}pSARkux=IJYqVK(o_&xZC8{nfw%B-@_uuzMKkPguP zmAe89VS~22c&96}u{ZfQSi(So0~yRdjMFwi1PCxsU_paTUh3~-2NPoF;z=@dGY zXi=j_hoTHnOCS}Dz$Rp28sHl^i2=3<^cVpD7)FLVP);CZG3+Xe1%nzq8vs!NDh4M^ zDLHRmw*c7YVZ%Gm-@pLf2IByQ3VKGUdth zDpw9b&l^(HMuDbQy_$7v*RNs6mOYzxZQHkT=hnTOcW>Xnfd>~poOp5L$B`#j9_Wyu z(*^}8mp+|(b?eHXXV<>{;$efps!TW=tx5n(UcYJ4fJlUfTHV0qDcF+hs;CStbql|? zyg|bZ1({$FuevK}KY_?I52TpnLxezq`fEvqyX^9dAuYsHYC?fZBIF^$xC>E4on#x) z#1m1RNuny;vxpvnEV}Qi0KjTUK7r`}gGmI#O5!R3&_M~kj?F{YBnX@>!~({{x3n@y1Z=>;aFv$~WJtWVFoLL-Kiskk zs=H8{6grDq!c?HEYALOvN3D7Y#sGGV$q=y$;>aKk>!Q^#Mm_A0NDgO3)L0WsJXYCd zg(3s1svzugG>qD^)S@Fh1qZ~3#u_#zjI>bdufi~r2peJf!mA;52NNvag8qwlBEiVK ztIW_0v#c+8{ToxD$uhflo*9|{EvnFi5l&d)g&A(x;fEo{E|5Tox-sI5F-ExHjftvI zhlZwVsGBK>WNoK?@x;2tcW=3hJ?nEm|Ip_{%G%zk|yYeqFUQ7=&U94qIaT!xKyiMDec{>%)9uSNNB(O9q6-y@D1Ry zFa^>qG_r-tSoG0JFWvOhQBS?li3JhlM_b!cUG~|F1KoC!X=O;Y?;8Vt|b@7!2jIUl8+6la zpz@6=-eH7&>@Z#mO~ zAylYEeU4B6j|HVIA!d`9gq)Qu3en0nX-uHmXy?GRxvNGdG?58_t4tYFv5u9jWi{(r z(dsaxu9YTWW$RnNMpU@YwT?@&65<3`B}mrmKfy$km%uqDjeZ7{mis2>EGN&jE|#&4 zb?jpy8(G%Sm9mw!ETZIkSc(Wd~F6zn}a4D zB^z7W&X%^dwXF#^n_J!PHgvJ!?Qg%<*~K{SseL)%;}S>O&2)0P9VBdJTFb%15~Qk| z@rGODcw6mmm%H8d?swq?T=9-~x0xmHdA9>rF0Ca??uzJ@Hj_CF0+zWXwNQLL^PeyI zt1jQb?lHj||6l!3{o*a1Wy{l7^HsV$xSAE&Q(wN0hpviEfxS zN8PW|(!eD)@rhBK;+Q&^j{jvbc^Uj-85iX&NImVBIz!#Sa@49|vQUOG%-UwoQpH6! z@{y68kqrQ7^O-!^O@0{W;NeQ z%58S@iDLZbwaM8b6ZElX413i)gXwaH`KD?8g;>ocbIpY|^q~=*=-bel(Tz43lpP)E znaG!NA5AP{LRo3L2zJXkE{Fv~Bj$pZS<$67^{G*pIWQrnwUxodLFZ=Xx%dq!=CJCHZd{kd z#rF2M!5waxE}PtpR(83WJZoOxSEN<_^JMNdQeBV9AMW6)LC8#MaqpYo{r0yEfq_&y z=NsKJ?h$MPUhqZ(hOqVeGNy&r)lQf8ah{QGL<5%0ga4c39ryUhA;y-IDxBow^#&qh z69@;D{LQ67lW4@$5W?!W$DIu2U(57z=89>^Ek-%WfgW_B54}#DFe1y5K3+})MK(s~ zZBUcm)^B{ywA`$6nC^vD&i(mct_?I->q2n!Eqy0MH~ZPqp7!6Ay`?o-LjWN0c8070 z{}My__fFUjo4nI*5ZZy^iiv`Wqo9HzsHlX(@UC{nFP`y@FRMmo5_h=M{q2$;${aM} z_z+GkJAa2M#fX9mOo*QJqc?r&QGa^V8$|Q1cm3;OpX8!go+z1SL-eg|Gw#!t-u0=J=z$)VPIe4}2(=Ie8KUk4g7!#o0Hg5ZIFC&La17Bf4b{-B0D&GxU;ui6 z9)u@#3;|P6@D7WR2FL43|!vAcM?xay0zYP?T!v5TG9_g_jrDGVq(Gds0 z5t@nrFvb63!WL~&4DB)43}O6+g7A8=AQ`eDuaPSRjwZUuiXOxpkl`PQF&_cI4gR4S zE{h%}$04xffGkAJqE4t}D&bCY)kbb6BC@C)GA3nm-;iP={fQYEQ6pI~2|6;?#w!|L zawWq`?}V}_#g46JGAWfZ*p9*`t*9X`G9z`82L7QNg3_oQ$sw$=C}DCE36Lwn^0Jt2 zDao=d*Q_a>vWpDjBD+y19dQJzGA!kiJI-w`?J~Q}GB5Q~_KYGe|JAY}qS7tB5iXzM zE)5ekAnh;}6WI2$F&(qS(9$oTk}U<(8#{6_G4m$sGBY`oSswEkboAELn*Mxhi&ff4jTIbjnyy)!&d!#T;bJjn_={gN5zp)C_& z8lC|T^q>@YVLm-lJUJ6Q?K3~^&OG_EKUE4n)3O!*Ap#un0HC22?w~mJb1w07K^?Rv z{4+u&RGk2HBE10|T7elxVE_i86^MZm1Y+B+i`yR5FdMW)|53DVCUiwv6qPE}DH-7z z=;0rjp*u$`MHll#aWq9+v`2lEgkIDs^?*BVbVo5$M~(DBe{@NiG8Kn=#gAx2{VBb|^ zB{u0k_GDW$Hhq#|siGCgm1IknnLe=Syp#<(d%}B_X&OXBzt#w$18Y|*9MKZamRLfJ2!cu zw*#G5W@mSL`!;&9*8;7VVnzUkrg60)jQGfGHUH z+;kH_6i6BkgUv_ZMpwK9m~}Z=gH5A?|4o?q#FPb|l7Y!fh06uNnm0S#AcJowgjv^x zJ9s=#c!yo@N?CX&MnQM8!zY6nT!-R^j|E?C7)6jcnVL97p16*p*wuKriW~2|>T_KM zBHx(xP+DOW%xP7lSW1C70HSq?XM&7p;)~T)jL)fki>h0v7%6V{i<#kTx>bm^_>2Rf ziyOj^9abRlR}WbBDBRedtT>U?ZbatTB(#GVibW^jDAvG1f;vOr#Yypc-gX6Rf6F_AXvZ*jOv(I6_9nylmp-d z#-N*pnLAXvn{lF}73`w}Af%C^2}+vDDtf1BGLB%HS*Gb(M*yX5f?!-$qC?~cj+v1e zqKIwcs8IyF7y+qeqNTZGsyD`;+8JDPStovZV}zQhjY6qKLVBHrgl-J8ZU@t}Z)unQ|r=VGts_mM0t8ta`K;?9^)F6Fwmo1Y!rhGPJ{YdkLnp z9j&xY8zoEaCNv=cK0yCO&~7mb)EO3{(Vy5h}qNh5G}K z`-ty)fQ?&E!&z_*Z76n|wOQ}P93lo^ySmkPyf)h*x?9e^dRf}rSZupB-}|!S+k?+? zwybw(wmb<}gBD|}bzzXm5Kp_DB8@LHvdlTH; z5PZXV>(DCUx5Hb%|7}9G4Pv>6FTeys6h^PY0{|0PJOBio^nm-kGu-=LY`uZ`l$oUm z3Rxy}9L74_uss}fzPlinTfZH=As~FY8Jxj2K@ti?{q!w+(`|Njw0=`^irn$!%P=3c?cr+{&@s!eem8U#QZ*-1s=Ak+ZuX z+<=Z`HIs{aM)+DqE_#)JxzvFi&Kcse=iJp_S=CJ<$YbILTAe1Qn3BJ`n@>HHOUX!< zn1OHhrwfg@{~aR99o)nz{mGp?05pNMA^Wx{rp04$`(!-JyBx+bJk<3MuH*o$jxt=o z0U2f~SQVSCh#679AqJlHpoVc{|kkt-_U>(TAn)y?uCnD8xXB z(;Tr8TIRDOnNZ1 znV{djD1JSCW<;)mOV)G3=11h#rHrgsF4ui3l2(kuWZVJ3cC=oy0O3*ym5Q@~k# z>1Es#{{mdblitFe{^>1vmQeboSq!K9c)SQ-&nI~0$tGI$pc0Z!B2M5aVt||_ZOvGr z?Hhu%lYHlUUc85X=T(%u3c|l7Ui2zl@L;^fL*MVA{!nb`$@XVy6!`t3VS1Tj4FpsGMS6g!dtc7e4D^;f?y3F6m7KR1 zUbP`y0gdh3F#*6i{oCoD@akUNL7nwmkIvzhVD#ARYyGEuGS*-p=VAFOE_>OZLm~j; z8@K=j3mQBaz#GDa3>!LpxbRy5fe$jwz3T>;eP|D0ZW zfg(v<30{a{h8k|jVTT@mcpQD^2_<5QCNkt%XD1%W)QT>?=pKnMst00?ZRJ-`Ze(Ru z)m6V)^;=mG*;u1o5#n`_U&#d^2$4=c31yU0PDy2UGhT^hmRf4Y-j-f|31*noRY_(? zfE47FSzhfJAXr=h_fSV=+80-Hd`;*{L3=UB;+%f|322~#4r-s6h8~J&iCrcdTM-GV z=NY4trbMWd0mOAyfCT<1)>pZ8TA!SiS|lirGJ zuA(ifYp=fkYEi8n26@$+W$hMZR?21?XP?4Swh*clD%a|>)?SNkw%T$B|7^FoM(XWB zyoO6|xsq;+V2Bc z!!W-{vw1n++>^`21%jzTW?4*?Rmk=@R*&*lo3hVq83J_x|1xr>(^g-Nb=HOQtXV6g zFcSa>VmveoK_;oRvub6J5P*!Er46jjH^c3c*F2T|sKR)k&GlefTgIcZ$nrS2r$Xxn zr-6Sj&UoXFKmOg!rNJG|P;|c(65VcR?$PDF$_?=daBSX_ijGz;|9JqY_q`D0km(z* zy6@)JsVgyM-d(!^>C^W`{ zbESCn-hU5%_*p;i9`c{@ViDftgQooY0gMuZOe#VIziH+##LL+3b1A-ChDRvo+pMpl%2)E z;wA#p3n77Eq7|=*#Vq2-ec)ojPgF2Egme&XJsD$YZU6$<|9tRJ8w}nU0x$v#9t3%< zXo5rD7)R24BZ*2Bj~10Oh}%W+XxHnHx>%8iF(v3sURZ+_2;#_sSm=+GtfVE;r9^PK z4J9Eg4JSv#N7l@*Ty5;z8ada;UtN+TK&wOm2?sCWY|0S{L{MC^P?0(KL6NKMr7wRu zr&Nl_8=(}YiArhAWIhj=8bOn?*rll_2F_j=3cwXS^2h)Xf`!cNrZ>L{9%YI%mB<{Y zIV1K>L5Qq3Ig;iIxv3rfFeH+Z3>7%Upe~;P;ce)aDv-ZhCJfPdMZ?( z3~i`GZ4=Ol^6Z}yt>~@rc`2Y7#1X83L?s9^$!bEx{|fblo0x!kX=%Bt&EZ?Ti+@ovnD7c**r*BNm|z`%5|@N zwc>Q(3fSb~G_ZuNYF{Ha1+ShIuWL{&Sm$}y#)9;)l&!1+gI3tgvT?AQ?QC2vtDS;0 zHnLoOENKmbSFfrzsi1AGYjx+@*n-KevaKy?Uc1kNbXB*KMF?XLQqtY_0k_00uGRi( z+vL88w#sepP>f3)(Pne21gWcCdy5cyiWR!t|LtzUQqoE1iZ?UN9j|#u=v}~-Hlfya zh$HX%Sm_ev59zJ1eT8-2{I;jO`t5H+?mHQHjGJQ{MC8#_N3P>$V`h2i!ehDf>yN)>s(*ioIx#!7Gg6Ef{3~_>l9B+{h4U@^_n2RUdbt0 zQ*4Hq*r%mxh!%!tOkEoVQ?RbHm}6Ys-tHEqywtY2a}6+litHN`$O16TCjt@V10v{S zhzQ&r45{TmEv0U_<8rW{7^G|6LGpU_;@n>G?tgq4J!7;~T)B2Z;t!g{Bik>QtX$ z$ulBfjz3b*>+LFs?F{3y%U#pvMD83v;*WUioFVRz1CtoC^VAq(0{d=mz|JZ)Sr1cXg!D)=zDT= zP23gp`|HFd`O}FIoFJCK_T&$ugLxce0#%^-G!l9HiX>my!>__RbWRL{EWaSP??z5` zVh}21pCEQm{whK^@$Lse1YUr=5fm;F5j5oXNmnJ&Wgh_A-^BI?iOayzzXBJ>%b(Lf)Xa$!RVyZ`jB4!Z(gH~uqE@J|8qQHbyP5g)i#UZP%4KI4Y3DtH<1O}mwN?Kc6z9X{fA3GxJ`&C zcY+WXF9=Blwp-!mSOpjLWeGSNtF~M#lL2){v1rzXnaX5B0^L_#sir^@Y2SJVvsgVbf zj_at3To-llczM8}e*SP1F&Q$^kd8Ppe)Hgs@5YHRS&qeajyZvVP!|mjxK0fCZiqmY zB7=c3RSO_!{}B3jgxG)*!iNwRCy?iOQ*wtXU{;OPcv{w|0S0kv7%_v}NS7ogdxU^G zb;xWBQG_OlBC1%3X2^V^mIciag#@v4MlcUnNC^M<5ONTZ1QC6Tse#a-FOf+QVu=v+ z@RGoAd7m~BLm5*7Q3Vr#5+-2@1F@7B$d17GeJM#0iNuuYMi8W!d(WmNiI^oSmzM@H zg&>u6ei;#f8B--#5ZLycp*R6P6)8e-jIOs3RvAbJ;foFTio}R{E?0Rw$&Mm(2*9vT zhX8rN&~CLr4?HmtA}|kBr*H!Sk(DC?FWG-9hnurCcLAU;YMEV&^>zUudN76s0F#Y% zNuW_y|1Nen5EI}EouG~eaeJ@>ej2a^C?OG=DVEJPY&w7q(}|hGrjVTA5X2{z!lwh% z00<0t5S$QdBkF5+Xn-0p0Wd0#;NXA^7Y#pWo-vVp8XyAya0tOk5PevVQRi=4u$*4^ zQxi~r;*xv(hA6suB?#Jo3)-O6CsR=e5ivTU;|B~-h&m`q1t>wD{})r1fPMx+b@KoY zmQWBFh!A1Qj<@$$BvGc76P9>*d3Rb6I)FOgSr9HapIe7qhJd68h^7mnaM93*D2aKn zQ=ea=dT2F`CZ%A$wFWGR5X(5A1c?$ug1!1a#fvN=Yo)DLk zo5vG601jqZo0%C6Uk(q7dfS1RcFQEm%u$`*8O%s3(4j6}F ziGT?!eHD8UvgxE3g@T4~1xHm^Z8>ZGxdy3tRh`PRJa(5!Ig%~$Q~BB|cqo`1Q8~Zn zfX^8Kw)t`suuEs+kC7>A@}_g51~ys%4G3|HK>I!(SP-qS5W@F^U^zTd858;;|Cd@| zgeEx<(V%^4Ky^8Ba?8h&JmG-5hXbLn?zd|SlXU{C6ET4ZG;0VpTY@-iiGUfH z>BzM-8USIpI)!?-{U@wZM~a&1Ypa7g5`hxlShJXir2#;-6q^&m2M$KMa@Hz+xa6JX z_zkVMdzqQ0!zw`#nX!YnO^@m&H3&PRwOak@XG=B*iZhojtGm#3pqWS=EZ2IKOAwqe zrhdz)G1Y9IAqPJfd`tTg6EF{A*$)Vj2fi8rmMLw&DuxSDj--f&Qg{$XaCtws3ek{w zC<(qukb%5;6NC6`R4{Q0=Wl&WzuE^5S^$w+=eiPeuD|kERZxeA0K5p1{}aT!c^2!g z^h>#0aE!q_c9D_?hKadFNf2J}2M_6|699z{jCoXf5X@@92VH8LFxqt z8>E%9xWlS&>d9;qtfgDe)MQ_x~g+0ItYO`4Y-ti{s497X{94c!a=yi z{U-wDx_sc^Q)s1mxP%1#rvX@Sw*&F81YyG!Y&POL5agP}h61HoV#I`i#AsEXInl%q z3l321pdZY~fjJSa*K;VLrIDhZttSFnu$5Q}$^Tb5cszD=7^CAC|1us-!s=%bsMD}N zIiCa}%NmIiak#iK%(w*Xz)(j@HE0NlJB{MjSk$FhBqf)dI>gVMUJvtf=^B-a*EZQY z5v5EI5ht_6sBj4A4$y!N&9H^tFc003kHIEtV%ZE!TRDa)Y|eWS@GNTe%zTEqo`buw z+-r(22M$}g6W?HpV(Ow9co5NG6EE@4E_xF;fzXC3!dKgVQPscRGR>+}%|J1B*!&IJ z%*{rK5Z$agQQUA_cN1lY1-0N0|17u$5fi|G&_jB8JW60R+879SN3y2%Udb$GB!y`L3e8t-|6komo0{P^z>1g)(GB^w zbMp{*Q~(Y77?7Ij)exbVGi!#?@UcRN6N&r~a;-3n3?tqN8XwD%5`?e?F_dtc8CoC* zdMFVZ3L4n=5eBW#28d4Eh*=3`0enVhwVOM+i`ALkSKY{fe?1zmx^u%OYWippq#Xe6 z5Ti!G3}XG&rwtL9>6gpcl63u6c>N;M>Dlnno3=R=a%vDHZMoy|a4|ZF5JRB61!?hh zjRZjmE34Vh&0Xd)(e6Rk2ngEToD~s9Cjo(Z4RM*XX4UyfM5p2QM{|!q0RNk#hwAR*ckX5a@tRV~{m|5Gh`ZQm45;+7Ry7rqerhT-rX62R&p?%Lk{Ip{!KO& z*2C0dF0SOtg%NK{<5KSBU*118;=&)7f1Wr|Jj_7RF=#_5if1^|M zQ0A@ykdT+=YQE=_u3uVv>7{P!EVJL64jZ2i|LTV>>O)3Ur!MQWzAas9Q{O-jgZ}Ck zQKL&?>$^^6q(O?RqWMn?XbcL@>x>=DeWS` z3C?^J+1~9e<_EEU?dOi}WYXma0~q6hDU}rDOR?_hF7G`2U-XXe`5w*VQHJxb?|UT@ z|1R(YU*U%kyRJd-0Ok=1&+rXzR#)m8dEoF_JtPxv@fYt@UHO6#plI_u&-0X|8a*G~)%JHm1N232 z^f4qF({c3PmGVpP^iLn?prPncPxV!A|MgFg5Tao6TYvI@5di&u^xy@B8`9`<^A~!4Gi1Z~V|r{F*iV$qzlp@BBc{ z{Digq(cd@EZ~f0q{k^|x*$-FO@BO;V{oSwn;m=RsZ~mN0{**rc=|4{A@BVbj{;a?I z@n2EzZ~rt%|4=pk`9I9}4-kh0{|+Qr(BMIY2^B76*wEoah!G`Dq*&47MT{9WZsgd} z<42GoMUEs%GQdfcDOIjy+0x}pm@#F}q*>GEO`JJ(?&R6i=TD$3AqFK{)aX&9NtG^T z+Eiyrs8OX(rCQbMRjgUHZspq5>sPR0#f~Lg)}vFjY1OV}+tw}Hp>O5RrCZnTU8H99 z?&aIp?_a=y1rH`%*zjS*VDTFZQQwa@8;dx_wU)kg%6J{nfP(!$rt}--rV_f=+UK5r*2yL zb?n*I8rR<4`}dvI#g8Xn|K5CPKz>%r85H3Bd-x5(xBUig{(Z;X@$XkzpTF|>0u*pS z0t?g#rYlwo#61HWlW_% z#96nW*xKxX;nMpKq~=JEuY3q(*2i8arb@+c%-n1e?7U7?F!i)N&gj&A5p4GBOC znX*91k38Pw(rB!YCOT`bhgAA&uy?D=*p#xU;w3Kvs6y%hvVgkFuH#n9Wx4B)DQvv+ zCe3D)ujO;4|FqjayCtay9~>no?N%Hpx)*mGalIp#Tr(xd?lYyeSu(qAln&p#aH-Q) z_{hggr)_J~Q+H`{)>{|MX{PzyLT$Eb@BE~~akss5whu>Ab>LxYJova>H~x6DVvW7d zLUcd7cg|sY{&3C@kF`kRvsY<&?XgDwd+@um3umC!c0PLQ2}f!3-D~&z#qPW3K7OIY zxBq^sD5g^-_D{-O@b!7laJpN`{|@m!5z#Mu^Hbo<@b^Fnh9m%2NYGsB#Iydnq<y3~xBN43^DvPNd@M$^yHSXaW)eNChOisKqa~XF+5sV(N%k z#)c(PjcYUry~b3z06wvb{2O5PZupY1V38)N&;%`Bs2DUp?u>!t7aI-9M!I<6kBQ@5 z0i(wf5601syUPp@v7!@AphSdUQA?=^nW{m4vR#KHB@&m&umaW&dQoho4P%%?^c8M! zUV)=dwkQxwT1AwK3*|1sRZ3t+kS-O3AtZC>K34jVZT*|xBncRRpS=nwnargweJQtI zUej5@K4FUGTrU6hABRU!|SP?M9|>{>SY z|9Px#1~hy(E9Uc*X+8p~^O+uuE|k@oi+{bR_$rN5?xF z6o)Ql=L1Absb6C_WZK#Yi=TVKSRRI=Dv|Cr#D zuP52;JU?p^(~4}c!Ie{LjVm)j2o)`xWaJ!8>dMznQK_<}Eo@QZGu!eOCHQO!cP(ny zE%LUr2h2)fH^GRLl(vwgB`&;<`(BfQ2`!66UW?U8!t!vb(j$-(m)ZhK!c3iJk`2oI|ed_AnOk}Bm7WWwBi(RoMyQWCNz zPVscDCKZ_!xVMnlD`#uG!sPx4RO3{`MnTYsDzIbrK$p|A0*>Y4e-Y zYZDamD9AZtR}liB2NqQ6$8Op4N?1UH3}>%R-#CF&+Fa<2DOu74n*)vJ7T~LCxw==T zGN-HTOrutz5K$Hh80{M4GE=p#DH*exL6S9gfx|x>j&)Ad3+cPy8YjB;*Oa=!VV+cY z*LKl$a*9pbW4C6~%Pv=P=kU;=|4s?HxcvR{Xyqka zc1-xEYku(R!4f5Z7~RDua?di0d&X0(I3!F_Zsq9PlPg)eOYGx@rYl9(;PF?Zf8xog zuV&tkk&?*bednG$J?lQ{x*K7B_L%#m;dF}CbT?STD!yI6a&Kk-$e>k{ggCbry(_qj zo(e)By(WP5P7k7y>=TS(<%?Uq@KVkgcgHUpV4um5SCaU~(;MGLL3`(e^`;r+gw!OS zWWf<`bJRmt=b#%gru|eC15;e+C*dw3e9n>VM4l%cZ;9?-!idcuCD6gob?*hWb;p1} z1y{;^<&8r8n6#o9El~j+p#Le)XMa|sQ=X+`0*KKMysgJf|NZH4zTd-Df#N@fY8-nW$7(lte z#9rw`KNJqjGdn>nMN@pW4NRz4dM-q4MH9;dFOwiv%dGgLDWapO7ZbwLNi_O7!BBjQ zG--@e97bZ?2spaH*E&S=TgFL}!iVF*0Prp{ONmF!MO}29Z>WSw3C7a`#b1<^Vhl%d z{JBD`qUo9}EYq|sqpT>L3PVW6I?N}YNCnp;K_TJ9qhJODyhqgV#c#BbaSTX-R0w!< zB>*BnwqnP1OvekvteTptD!j!zj73HiMV7!vB8wNKSQB2Hi*p-=vC|QMG#Yv70XYi* zG1y0C8Az6F$wPpn&-t`*O2gPnsX zI>U*jOpzP7fwX9|0BF2p`v#Mv442HxfF#6LQoYzJ$C|uJ70Uwo(j~6cE}E#glL$Pa z1Tu_l3{KQXOnJ+o7)iBAHmV#Bqf`q{n988QOO*fzZd?wnEKFjo9GhH78VZV>ED4y1 zL^-^x0HDb1dJL=6i5|Fv!y}52Y>ZX#hK^*5%-qbCOv+vQHK*9WATdnVJjGZ+Na{ML z$ifL}!-_x{uxIqUDzv!9tjOaK1EnCj$3O;f(8ez4I1U5O${2+hXp4iH@AG5oiTo zyA@fxl=zg89=HK|*gvSV&Y6(Tzf_IROcDFciT%`$>}*goOe}@VvQU#F3!J-ITF(H8 zKi!lkJk&u9MGd-S36r~(!F&v(z6_T#6fY3Kk6j z7xhb?2o$>*1rR6#J@_*LJq`y=(iLnX_KBpJq@D>ivG6<`cGAL*Fa+3gvV#)~e>$ z(@i~z1^tN{{nL~XuBxv;h1FQrHs8; zED2edMXjB5C@L!1qpDDA|HPPXQPCXz(^yTTmL;T~RhO78TCt*?wMp9E(K(uIEBne> z8*$lPNmZ$JIitPWfO?6VW2+2mD2Yv3ZTVS#qS~sx4y--ftZfN&G}vdosoxQl9%wna z*s+!MR{0=FjY3dc8^2Xrg{rQwblYkj5FL3mH1#v=@HmS_3&ah88z@qzs9T~S z+pa^Np4MUhn&><>b>=mX1 z;@srzUca4G=2e{l|KvLUJ3inI7EUgfo#!c8D`fw1B5my={wKWzz39Dzo2-I91b8VI&40_plj)5Fr_!;n|gm)}`I^jbbEAVl2*L(6VB1tYR)E5iJg5G2VzT z{=$7N<2e~)HD2S0K;uFIVK>ecHlAZT7L_NybV@mI@{DgBNCrvb^MQtmG@9WKeDyPu^mJ)q=C} zg1}LU#wz7V9_2$xWLWlJSN4d4RGYI&8AJ_|@Bansq`6 zEl}l`z%FB6L1L!jK%QnWj%JHMW|mN8s0o~IPMc;{<^bqrY(_w9&Jj^Q=jAD9C7PX? z=;o7v=cgIx!101_hT#QfXOL276-j4)4pe<^2r?Mwd0ypa?qzzGXN4YTRF3C$vgaze zyMTtdf0os1uIN3U=!Ga~R0d~)E@+P?=*9|ZaMtCAR-0AEJB^k!jK1cJZfX2j>4eCG zv*~DY|1Riw{^*7FW}SX$n+|7GhGv;gGMEO)VJ>Q|8|s4iXl_owcs6LLX6C4NXsV`X zwIKt_RO+xoY6uWsv0)9RBT>$#q1kw)ve zE^EC;347-2hF0ai=C`#*Ex6{7w=Qf!Q|gqUYmu(&k+y4+hHS}xY|0KCi5~33N)@L; z1ZLLA%9sFbxRk2tgz`m|!@i7}g$zjBY?zVjw5e%lu4{(&Yue81+MaFO&h4ibY|I8K zT;c3*n1Cz*23->Y5%7mDE$x&L0c?1SHbKp#=mzz5>!ZdF=8os3+g?0{i3wQWw%7vR z|GNp!Mv3OO2_kNZ>rTO;?wpQBXr|t4-41Nro^Sc?YiCB~R?g?%9w<}^gb5%7aF{$J z*h`wg1`QZ)2a8s4z=rZ>3U+}VdGUh@a0TM7iKd$dnQ(`cfL&wVXdzkf=H@U#_-_NW zk1D9}wAkgE@NWP=2@#kFoQT$)3xM$sOa_nTm0s-nhHv`j@g2Wy`F@($=I#9!s8qS- z?T!h0FmBbP3}8?RZ*XVmRtgN)@FWM1(`F3g_J=8H9z%HY>l5xPSBmpai6&}tr3h~` zS?{#w>-hF@I0y2*zHhVE?;=;?Ea`4n(7h(=ZcU=`sT6J$w}i2sAp^i@D}#5sxXZg6})^p==_L3naX*MI=52Y>Dro3w|M54Mb9mQnA!mup zzVmLP5~nc&Ey#j%$DL%KR1z2PXh@qcho*IB8%qZv7q?-54*-9N2I}E-mB0q0)Pgw( z02C*uf6w>E;yokqf|_QNDH-S~5rMVocT%VE;M)Q@sDgO2a`k>`2x%-W|4{K8pY-v5 z;rbZp01$S+oPa49_NU?D1u=Q*F?pz&fKhjE5diaPM+tWh0C#?Jq(Ah@J9EOD2SZ@> zl%Rs7mw4HpfC#G`es}nKg<(sW?f_`;LIC+@FOik5n#C4!_jU?yo^AQI_p}uf&Mt&n z0EbmriCU2OzF&!f7W(x)yjtM)CkF;YSN4+_e3U>0V9)^#00wEFvls7s;%4}iko=RV zad@1aXs88N-%HSTaSa%JU~uPX0Fz;lhQOcrm1u@%nDw1dc3j7h>JE-e3GI`B@lFl> z7X$}H&~#uRcM9jY30UQ`X_Lhl{82}Q!5?q?5OaFi`a$@6&_nZE|Iq@luLZ)Reqi8i zEx3LFpYBWf28o}N=QkhBk9>?j8$^f(o==HY=*uY)eyE8sfB+X@Ai;tL46cylV%s#dQKoIohrz$yh_9t@;a-~@!q~IoL^tr1v}tUyf(}g;r0CM7PahH-U|uv4BIX5%4EXKa0Koh}|BeiKLY7(r3u%r0wRYCT z0Spr)3~chnBhkVa@e6w zg>=`JE1d*DMM3QWLeOLd1wv4J&%rfNEdx<;7cjid7#ng0UAG!ImJpQ3iY40E9*D;^ zl-d@bP2eCnWW;#T1g>lm+(4Usg^Y(=a)}UWU4m)aUY2lm*F2cL)+1~K9Z6dNMm)#X zTPAV^Cz}X0NzgZJ^`@4dZS|(z7QoQMNr?lhIMZ97|B<2HU3YzR$XMD9MBIrQY!MkE zXn|7~IGp4uS$nua*2xxxY_ZC9uMMO_TUEdqrmR+JSgWnK;)-ErJTo2m~Y>z}vQ+cP^!g zidf5IHGXqr#S7VM@RgqjfTieWC(JPE9`-!4p_I?lV?JDo(6d%?wb#~(I`Ia~yWli~ zqzD?+@>w)HAE{myy0`1#Kr15(FPxJ-hnDf|+L&2fFV(h1tkICSwv6;>2X1QK8l+}k zmY#0**z2?3zELpsdbRSt`b717!|nwCKt8$OzyJROFo5k#9{~|^q$)V*5a5W0dJHld zPTYuGwXxQE$OEptY#}}aX<&#rSgE$G|7&wz_`~m})fUWnkYDj4)LohspS~;!WZO}m zZO|70LLfvZ1M`Xw)`XR5T*glYM9qeVh@jp*Br-g-z;XPs9oUeoG2n39yt)F6=VS7V2lW|V1(`LD|fnyMr2l|LF{P22)-&v7;QI@Rg}dQL*iHf_5v2P zyzoycL{g3@Q>Fk+jxyvro)H@+z(+#ze+~3XfG8=Tu4JZu!7`22DrvR-@k9%Nh$JaX zY06Wg5-E*bC1S(_nFiu$GR)J$a#CXiY{<)wP@8}Tpz{q5_N9d)*vdiX!9fvdp%%{( z2rPTUl?LWzUj!MHSvXj`6Bz_c|JSr#6{Hy&go$jG^(mq_OM^s+)TWmc!&YLT8J!Qd zrI#Q=%R2SeDh*fy9O(1rKrC?`f!K*gM#Co56f!yH`G_FESe-6yvxSM3QHBG$i+Xs8 zP(%*IhnOs9v{LEOkK*bpyb@3)3rdrnY!Wp1lbY5J0@9blG^R2QAV-IIE^0-?QbC&$ zTF{cyoW5mUAM;*8qESdaeFGcOxXX1ggM|S2qk18#R5Ws0khkb2FF-noZh!%dTC9yy z0s#g#*5nQ>Iq`GYz(zBmA)jd^vZgmNom{OFRGXZoHpeTgiq^K)8b+q7X%a}hR1V&0d>YrAW)hHopfi<+$!*VkjrxFCLSFEe7 zX6oA4J|#h5DUc=Smr0Zst16wuT3@oZHP`|-xWXOogjkC-@geOqQ#A;b4yD4-kb#Dn zTAWSJn$7D50Aa>;BsobpP|+yWCR%9jLp~GEk#%WIV!hT)zS|J@I`B9jn}AQ0yWK7k zH^2HN$sqN&HTjuTlerbI{#d#{P|73^hS)EI8|>f*hvdEr`^z8%0$;OvC9a~0VqFL~ z6%A{bAl5yN=OpY9x|RT4K%&1*i1cYj>L8+?3(HkGCZ6$@AZ%j?=K+By&E$X&LO=lqb}_T;~!?hB<$ZR%4aE7Gc7bwWy98hUs` z#ac#mprz?#R^vL?4?#7r-4_6_c;(Vk0~V(>ZS0gLq|PhVNl6#h>t{nd+V;^kwX0nf zR|lHbfrjjL9Bu7y=UUq0W?E%}#USXgnLzkkcbnTiH8|tBAfJpkfHye4apODR`X&ju z`|a;)Yg@~;vGTtQuK((NBm7o<>=kcEdf?gm*1+t>G=Vye?)+IA*)B~u$2;C}gM&O; zgL;JhcG+qpzJ%nJKb!C6rS^{Uwx%B@A}t^X7ZHpGVE*5UDo41 z!tx0{=||1?ssBB^=cCVB?u&1H_Sru940$|#;Og?XL)wPQpZ=MTZ~coPi1(x4e(86- zHQILH3)de%`JYAo^PgY#5?(a=+uxz`m%nP<_x

    9(bA?8UFg;e}>%e|1G)0H$)zm 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

    g=a_ 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 ck#N`deW!A7 z=l6dHc#-3}ck1eaK!mW{+insVA^Y@F__>J!+A(Sh2 z8%}jsVjwpx_f(H;vEYe66OL#3mVeS$clnr?dG(kTBO8eT$b!(-)LEYKegbqmiYS{9 z3!%@8O?Y>ezrLAQ`lWv>m}mN@XL?zIG!(}27kfIWHV8T3l1%@kN+{2QC@96Up6UsE zr2liMNBfx1__SyHjh{3PTOj5R4W*LRtgd~$DTFem&7S^U)J z_rzEI)<^cZy0zW<>Lu#~rBRJ)@hGeI{QsW^`w04>>plIqb^YOgeWxe>;}7;!6!^|( zzwG*Yzkm4NKL~-z3$Y4*;Ya@LS9asq{_ekYbQO3GtPem(d7zJeu@8Jl82sy}5bt;Y z@8|yamw!D6|D$C7?Aj2~_kHR2`vn4sPXPjWN;S~az=Q)?GLPhphAZdEo$^A z(xf^&DsAfYDb%P^r&6tIb!g12F|YpA>h&wwuwp&hL@03J*@OszrfoZLEZn$q=hCff z_b%SNdiPf3>h~|;z=8)8rpmN1;{U{o7c*{*l&@XK11t4`Z22-~kTlb(oyj@jr?mk- z2^4*k^uRz4GqY~(`Zes>vS(wKZ2LCu+`6M4-tGH0aE0H3+oqjWxAEl4VJZ}D`ZK|z zYeUamZv8s;?Ao_;S4sRk`0(Pd_8xEkd_oRAyd@lt{(X0oxGTr6AAhsL(w=KlM|<#e zPWA~nAb|xMcwkHKDM%E10kGF#g9}RNS9ud!c$<4vWq6@s2x^3!has}IM@^=ch?I#6 zk+_7*QX)9l0c2IO&%kf1;h0 zB2Y|Pc_o%vYH3lCJ>DoJjsMSd85ECWni&+3gk=cknL=5)8+}{KdD3Q7HpkVRdFr_* zpV_skU^6p)2^2Gg6|L4@kve@t zgP=EUQY=Jdl{};UuKK)DmlMP&9xu)2%f{j4W5rba2AJ2`w8y!sq(CE=@N$ zNaVj9)6_A;G%?wblK{-> zu2RjQ%rmGNL2c9xCEH|mP|rLal)(7bbm`VX(sXREQ!jO7a5~RB zNlzPf++Y)2QIJQmOFanwYtT*_mbT9)Zb`sE48}bGl3!sC^hZDI`6$_*0<0GQ#=q04~y(` zO(uX)dg2NR8N5PA_muR~Ow%O2O?of_eE=rllr+=}n?7~`RU_)JQQk+TJ@gwxzVA%M zmp=eMK)}EJaMja0zRLft`v$k~FedrgbZl}etFVqBq6okN2a>xJC8U22d|+JgM>KCh z>wNI*8Tm4J!S;Y-IGj_64b;gVM5 zDG0V`CT8hLs-hzmKH$w|2}$5gP@%vhCh!tP{bcuw!h}!wiQ+Me~S{itM35 zG?*n6H#jdm-f&MAaj3r?O7TFh(}`QIvOBdoLMORX;_2F`$31=|Wxslw$T~0(_1y#+ zGtt)wgjW?dY3(V<8wjH?c9TbTsf$|-zzAp+uc2%xjeY^-@GeQY80pYAd>l~f+QzLE zc`GTBK!napNfZAi-Z3Gdpyf>XcgkJzQX--h6tpzyte?2&Zd#O(Cv#*;Dkjo`MCn+D zj7gJCKr4(s;a*Z~_{*NGk&V6NPU@V~836qyXR@^2K=2rV1*Wr|1JUI-04^r z(xK+fbOTM)@fgEVMGjOcO)cF?6LJHK@+_j`V^7tZh1G6ERX6UT33y?)`Zn{Xm3 zb{Gi9z`*}gTsXnyI(ur%oXioecIBM}4^}})b=9Ghv#RO7_%QORuts3r=CEWG0F~b6 zt}f-w0KL`9r_^;^wo~UU=c+(jV)iYL{cPMw2u+Y8)+fyv#bJvCSj%~%gD7;Zj{3Sy zv#JiXd=sYt4?-%yv{hS4T;gWqxJY-xL&Y#}!`r5xZm z3G)d+R2Ez`$*DTSTNAkEvbyHo7If3Q#nq~}y_=HmZ35BV^Va4!YatiQep{2A{uGzQ z1u7BgE8so3x4@QTuYnV+TKG!lQv=4#9x9;;Rbh8Z7Pd-S=4;c-2KT3OU0`uFt4<9+ zxWxaRQZS0mh2Rvk*g_|6nTaiv$|o^KG$d|SDSfJ6oer13?pz>CV=QDCwYbQQTCtIn zJf0WVcF22SEPrT*U8l6=#;j5ZP}2!w56d;Pz^#sx!`wq9letGoE;E`*JWzBAU==E;XtPW2H^ZxiCQv^;k3kS#q}eD9>rGcNOyA z;C9)an%3;4KMm_)r#02aPMe{Rt?b+|sSio2HDQPymp({>);ICQZX=Ctp-@^O;#&U^ zJ3)-jadTR~c$7A}UApXbH#XVqez#(*#OB0UIo-^uM7CoV>8ad$GKhG!s8~kaS&IA1 z9GJlr3d^$67f=qeaLFx8PFn(tddwS(N9rwU{twgnhz3yMh zI^K8W?!5EeO+A+kEbSf>mQgy}G=cU`j-GJ00{f0Y1**y$pIE_D9xS+Vc;EjodG*X! zT}y4Q{7VABim{U&W~7huqfKsE9^aDSzJ7e%UA|b*)7~qbzkOHsj{DtfJxh%7dhJ=V zhd(%H?1PylWT*G1b5hF$u}4xK@o?o9uns5peDJ)iUO znr7vcD6C)+&Y*>?0{$6A!T4Zf^q?04$8aXn5FzELZw&9yk- z3wjx5S>YO1poQ?l4mQt%K!{OT6NdmGh*Zvn5Yi<9Vo?a;#AcL_(-DEgkd8d#VXo}BN|R0ex9D> zfhL3)51L|0d+xA7FeEhY#y7MgPv~G$ zkd(0q1w4+VQNRJlL?UhwBD1mMZy@497DgAaA1yA%7NX?vR6_OOA2!)UC2T^lXpl0k zossmGM)r~G&{&+M-b^$^0ijbAMxGR2q!JyaFPaff@Pbk(qC(k3LDobM;KTC4hidwxl6m#Ww&6RVl<*-b6`?JBu$)U07w!~ zP>Nb21tYr4H}qt9gbLhv)RhGo=cw8G2ATid&DD?l26e43!FajQ4CiUe;BFzvr zA_X{*p=88BK23%CL{3*3(qH}sCqzgi4NOuA1{kVIPPBqw)`S_zPf=_RRp=&F%%)!* zg>5?0V7NzbPQ{fy1Wg8oWtt{tl1KrPQ!yq4b@>h@R^)g67iDN?9$E-Unuki}L|g`i zaPmZ3%I8qT=TGEiP`u}0{ALVsl0O!Se)dFw`hCBB?OK}r#|9|OZr87`b0@8=s*fZUnc)lPSB{0_9#xw;$Ot(l!X7U8u-GouHjvxepy&zGE@)&8JD2j>_BaA?SMk!$cWKSUG zU*PFY6poJYC-3no4gF|(jmr>f=;#tHV?6SNpdM4`xMpDLlOEgv2zZYhG>ALyguslzUZQ0~y{ei9 zO;q4#CC+73fK9I+sci^rQ2^#`4g?Lj>ao&Gtj=lx)apRo0VetCP2}pXf`_uMO7*d1 zQ6LwD{zRG5gtN5~DH2hDs9vXHVQ}cnQzifBO+^E4Gh`3-E*%huS_ z;tWTkG)us8Ou{-3sfy(X5^Gct2}o^7!v@8?>>zvO>oZ}HzYf;oEbPxn?8Ne`a2PC- zhEhP1>@6~@yVBkzD3PNc#irWUKu{vgP-;Q|T`xHV`Egwzl+wFi<&@sfZ~}(L4wGYy zr5K@1A4;r-L@Us+EY!WC(T2y-ZtUei?XsdOR28j{CdSn+WbO5oD8#H%%->Lv=@Gs~ zX-d(f-W5;yfl~&pT>O&MT5XRECJ{pH`olz?Exy7 z?K+xRP-*V*(uP#5NcIlk^G=N!{zm0~FN+BtiBK<)cCY$U?Xa%zNEjC23S~CwEBOi= z`|3!8nhRIr1Z+-CgxpB~Qi$CV2JX5q>T(758gNK79C|ct#qQqt`bhjr#RKa^1uMn{ z@5BJB8ulviW#XI%Yetb6X>hQB@Z9Q6BmrV(Fi|wasS>1taz(d}tMkexVW=>&j>oag z@CHHa4Xr3n4L2=~nEg#>n zVszw8C@FCIqrI-^hUlqRlFxer$$>r%%G9GmRIz)!W&Zw*5qCkyG!xPivQCgyA4W0r zV69LX@uy~MjQ*!^VBY@jsKXA#VtNd-z$j4|=C|N+VkFQXN3Iit@Gh4NaJ(l9BSir7 zYIwZo3umJrDnxE3#BL5mjT$j6&jcJS4^UjPPKabuh_XUxvqH2bkYubgJ@a1-^B}$Q z6{|7*4uu`3>`o}}KzRRiPFS-~6!RU=94`JcoGykwTOeZy%oj6oSbbGa$a7N&2|PDN zozhSe^c5N5=Wu9pPMGsn`mj@Q^V8BZu9|SAxN`&hh$$y%Pnd8!BOop_MgjNp6)r|f z8?a+QOEMn?3s3N%&P0GN)l7u+japGtm?321rNH(?#oh>9@+lkpQ2=RHM=&(Hw1Jln~hG70w@2(s_IpZKCo4LWBUSf zZje>*9P(n&sL;6RPAoJ&5(O7S%CP`$)D;I;gZ0=thVU}6ZM0(8thG z%*YBQG=VC}I4!{Pv{RvMNx6rSO7`DsD>!=WHXrp<5Oi5vaB87TO~3NNf^2%9tUiH> z#KIPPXbRG@hd%_yzYL}vkGB}bwY@DGaj&5qNQN9pU{~A54uvuxLfYWp>j$p$)V1qx*!$1sp)e6f=OGZmKctS3X1SJko_q7v!EJs`0&m- zoujd#@5Gbm`ItjFlureh%jLqMf>yT~l?S@Q?FJTH`nx9j&UrcC>3N^OxQx5_zq~k& zFOoAu%FHE6rGFu&3%YLTtUgP~Qy;W-Lj{HlMk~v2`j`%=2iA%E#FL}Co+reapE(8( zd-^W8&02b_*WZKdMucm$%#fC`{?+-IM7ns8`J$(}nfEoTXB?alhJ!`B z_<8@JZp;CpAMkH*^v?*1I4Y~S4}_p@;jQO{f~q7A2gSDnyiW`~dB+>L`^LW$27pfr zPwe?j$aoE;bpT*bpGU>M!}`MY@^HI*G>Y$!1BXu1FHR0=8~?;l&%{p#C1dz@uBTbB zHwDbw1_)s5Pt^QWm;wm6&^%0oP2l`MNc>_jdvGuKx<4$)*Pb8z26tJzcKXI{7bZK~ zgf%q-3EOo-VCyAJeN)(SPS-MTd#%>*M8l`@*;_rJ1M`A_{ct|;^3MxV!;%l_ zlV(kuHb>^v2~t-cL^pv36*`n?QKLtZCRMtWX;Y_9p`P5clWNtEQL$##x|RPcQ>$OW zh7~(jr?;FQ$u2}2(rj9{Z_mO7J5z35k!wMs9buQRMZ6dF7>xF}??`3~6E@KZ?cv+N z0R|VA%&==^%a@Bz{>mr-X3w8NhZa4Wbm`BU-+>Q6qguY}58)vOFwo*}{{q$Lc_|-SQXpjG;R^Mvn&FS2QOA5fe zIvI{w;)!piGgpc+RxRO;IqsNGcs*t)nvga7Mqh%kG$^4WmwmQnWec{}-esXR2xen# zmIzdASSC56jKOV`k3N7d+UTSEv-nX#h(_A!t$dDJ>Z!41l|<_1oy<9 z_^?qm3!4C~C8^=1jZ?Xs;6=ig#TmM+UVd95YkbtXGp>=1t$EZQ+E_O!&XJBgQDGf@ z=8Z9Kt#JW6;0=)#Lj+31U^WXL4ig8sIFiOki**0wBNg;UNIDIVm9!+A{&z`@P*G9H zLR>)(h8TQ}l3rgN-wXliy#(%yldYR%XtJZGSI&}_OF^QeyIaG+EE=wIDT=8ne0KhLud1zW* zaYa?ZQqyWN#cG+3h0LzjE=xC@mH`(R!?6xN)Jk0GpJ)gqE~y zER#x0t16fF=v1@f>tLf(SlC{xqB+ANDys#hiEI^>KE3T%5f|Ky6t$=UAg)`9mD;0t zE-jovt#YAzL5STox@~f4YcawJt89)hDw^%vGWlJZp3IQ9#VsI}$&hHi%prI6$Mz{z)`r-}veRe0^z-}+CDqfN3fF2JP zOAJvdzR1Oli1En+n z6@)?h@ZSK$8p^A-wcTbd>*fFCLBO{jbRBmyY0v$|8$@OZGprnvBva;c#Cz9XNwkoj zDMZ+vK@M>ypo0KJ+aL}e3AVL;5hHYi+x=@aFDs~n5G`Z^1}?Wk3>^R?MC0AfP7!lY zWES{MpbF3ct|3>6Q5t5Hfdo9;S#g?hf)KaFnl)}`g{R^Zr_0wtaZqe4p7FZrdNOi& zwZ?zlL@M0dAVx4YhlUhyg8(DZMiBNYs4VZ3dn4v@NYu0Q0FHQrp%2J0xkAPS>25oD z0$U)1wgW+Ig^@6@v+kn{X$nu~C zrU#%3EDyj0p#Gv;s$Bmed7y$3@cE5Eh@`7Gbe!!DcxIe>*irzCc&ZJeS%(X?AP#qU zaXU&6ChG^}mACu_Io=mPTpmGBtT4+0v72N=N;gC%dSw2A5hr(p+zsMHv`2sQj@%sI z-q3b5D(s!udv_tP2S*_SkaR?OgZ15TG7@0_4bXSPz)l87gT`$TC(nL`=hkHLz;PiF zSa|~LHi#hT5pT25+#?qXf%g#%dT+=t5Jhsb$RyvV2?&tLX8)dUFM4Lh5u9099Ee6Y z1>#stzC5aqY;1!|iBKP~<1)3_=5sqeDEREh3{qk}m2515pwM15;*1KJdm&r24k* zyKGNTz9HreLJMlp$&7$}jwA||1OvN4yI?T(0)PzSPb0LTsIcKMkicJdradxkcxnf} z`Yu|U$y&hYOWG>MvZS~cZ@%UM0TaR|Ho;R1rf|Fb2Wl)nKq^qHO{iL_;*{fh4M;t|Sfjh`hw6vWV#-9M3)A5b~@g0oSY) zFD4MBaT@=DMlRq;&!`bJ@JI`yf$k=a1xwB%x&az3;`LP7)R390TAR z5d|%7F!*%v`(8~+kV6Z)!Ptl`8b)RUwk;3ysR(C_M5yc=vJEZ9Z8x4!3Ke4SNRRf2 z?8ZJ32?HPn^HCoeV%(_ZFm?!KV5ki(g0e)(lkCMkz{jH=uO&T&;f$-e{NeE!FmJN) zCUG)ou2C$?B`48Hio9Xu8icRBfdy6MvtDoS$e>7!av~-0#xmp^M(@Uyk|`+>GPG?K zJR%c;PWFEQ)0 zB(-r~_6zhNEOZQBU(HIdBw5(OO8QZ|Dx z*@}cA9p$I!;_Vv(v*}iIE!L0h z&<1BbNrEKEvW6*;GD3@DM<&;-quikzXp+7P!ZO_qH1%^o<>U`RqSyEnmZT`#+%5^j zE%;_{6$B*|MFKmu(jyL3_*(NK@}L_mvf8wbWhxXxBa-?cr6b@+1Pmhdc926>v*iCA zkwC-o5Cs$8o>R733P;dFAPoZQ=z*`QAW?!+Ar3(aF+xS-j79g#+ZbXrms3!lPa&${ zFtp6Qe8-(Y$!3ZPpVVn08n5w;tHhvWtzs!}4eK5KGT*z{PDHCaJt5I^)DAu=smWK93^B1xPC zIYv&{NQ4tQFbY{j_6!UgcI1gN^`JV!yQuXZ_tZ{3aqr+KNzPSF?qx6`>fj2Aj8IZ- zq$!JRh#}aD@woI3I}61gZy_d5Sd{f(5jJ6~@rf1<3hRPFbpknF>p;H;MtHFFoYI$| zQ=F*C!ou}M_|i|Ug7_dufz-2)EDh5hieMb#xGwXoQq3}F@;;4A@!Ui4ipmbBp&KSI zg%tK@fi`HRs)-14GO~|V-J(dkf!;dRP(-#)8Es>ulPs>SXE~~(@`a*Mb}>KcQbHv? zI%)y^RbXc}OXcf6X%@vI&tQeNZQb^5--KBSr(E~6M2Z%nq;_hNWIF$Lqh1Zja_~i8 z749M!ZeZ&x0cD1`hH6W9>u!A(ZXq{vC6_<+)@K|SQ}g!v{N~K2#HQ@&qS(-Kc8jeT z?$iR-)PyT-0TwfVWpZ8jbzv7XD)-xF_r;>Db~^^dHcW8sIRDjOCaw zzIcw?MP~{4cY=4mOf4A~ZeWWQV&Hg?30X6{IEL*Q8&e{YujYmiu!DowS`d&jR}94( zE)GqyjSG2`Ik^uR`IE(>jzO7MOp=oM)n5@8K6fi^v-F37xRYTymMd?RX}KzpxR!}U zX2-VFO4r0x#_`VB#O!e4u0@uO`Iv1D-Eg^A)F^(JxmW*KmtawOjQKT#%{C6bG>tE% z)PALzk@=g!xxZ%ZnaLTGq~p)ZIal-uCf!MGcgI@VDoYn{fPIEhzAxUnMQp;1443EigeR>?{B(0oM9?qA9wf2?Qk801}|cq9rga{NTU- zYFS4D6HuX{Kcb{jnxs`arCGY61I?mgI;LehlDv4Pr?VZMC)BwD(sSNf-Ynxzfe zrir?!je5D1N|E`w)tXKu_8<^UM}Gn!sDZkwsrssan#YcMtGT+Xw`n=JnyHyBP5D5l zMJJ7v+N;%it!)~>tTC-&+MGMctqTJ{+4`>Wx~>1|+AZuluQ|vdP+B2GfuseSunl6c zANtM=Td@-xu?yQX{W`KGyP}n9vOQ=aR@xvkI{-Gjvjdy6KYO!>+OkQzv|U?BX?vMQ&gS?afpIK;R2qLb303BQ-- zp5}AH>luIb^M3Sk|5(YnwLGHo!QtvL-Q+#QLt1sCE!eU1yK;J=KmGZWi@*OWx%~BEKRb>b+10=P zee&LcUHDHOk?G(50fLXffdmU0Jcux%!i5YQI(!H*qQr?5D_XpWF{8$f96Nga2r{I| zkt87oIEgZ)%9Sizx_ti$Gp5X$G;7+ti8H6pojiN`{0VfW$)H4w8a;|M>9>>K0xV4m zHL6sbC97J!iZ!d&tz5f${R%d$*s)~Gnmvm)ty+&$+q!)VH?G{dbcH^wi#M;{y?py7 zT?;s{;K76o8$OIUvEs#y8#^xRH?ri(lq=62YB{s!&73>4d<;6Y=+UH0n?8*?wd&P} zJ-dDlJGNZRvTNJEjk_l6-MoAI{tZ01@ZrRZ=RS@+`EA&B*x487_)T>*+jy=0} z*3i3q{|=Wq`0?b+>)eh$z54a++q-|C-Mswy^k0gvk3au+`273({|{h*0uH#5eg+;0 z-F*ly$lz-PJ_!F|gc43jVTGkNh+&4FDX3wG9_D3Xh$4c6DA9*1u80?hEWQY1 zQ7FzxV~sZ6h@*8e?#QEzcl8KlkU8Z@WRXT5iDZ&z3CU!WF}dhult3y;WtCQ5iDi}t zNy%lDP<9EXidv3IW|?N5`Cyo8^7!SNZZ3#soN~@dXPv&iiRXuH=E>)LcK!)ypn?vH zR-cB#`QxFA;zwwsjy?)$q!lenDSdlZis^BZZpvw=o&uU_sQV=+;yqfH6q&7<*GD03pZMD{3 zD;k50#J2y6H_z<&t#`jdOKy1Bo{Mg}>XOA^C9(0sY`JZX8>%<)qSTaf_wKlEzyAIU za7VS}YalNH=ppZ0^a4vTa{I;@aKsW%Off-6>2X3$4X4zMT-z2wQxX|7Rg?+%#?{8Wh;B(FWy%`@$rHrqcr4OF;A(VddoCv6K9 z4U8a6ncZ*2pc39N8QxOZJ!Q>!jTF?JAdV8$QWAExarFtnx{_`?SizFU25z;wyvy6Xh)V{LRDT^R#^cW}xpA z?mp3e6yG@MZxqad3@7djz~$fo0-X#{XOw9I@usIcl{{sBG`RuQ7DyA2H6>p7GYa-T z2*MC%sa-5d-(0}vIiz$@DoZilOIQF7qWEqnNswOgpu#`D+%P9KWC;dm!i=3X40r+X z-%9cYzyQSXC~(M`0{gOwvt^DZQQV6NuZYDg+6YG92x3Z1_&}n7@q4NBV)&dw6ZZc_ zhJsB=;*g%jyQw5?jQP8l6g37D;TGWE7bJ9O)DVxB$3OH&$9) zB5#SyNGWA&Mrp-MOm+eqjACOmd2uZo!gn31npUjvAN}dQUW%2Y$Gsc+2aKc4uv!rE`P(p?M z<#M0|wUZdDS;EZV?TDmAH0 zUFsHKl8}?^Apixfs#S|LuyU4kJo>arS0$6oi9%42BaO;efg-H{g!QaPd8tt2#n!pR zwJmeCPphKRhk*pZ5q7<*U;iqitR6IBld0=@%8DPuYL%mewHcA(@(E3(0w#F{tYt4d zAluY*tdvr$TQ-Z793}^}jTKjpAUj!@L^c3|z^rRu>mJRP)=;9It!=4-qmwjsw@&4) zZ+p8`oqXaXl|7SPV2j*U-FCUu8|P%M$=pTVXeGa`E^w{uTbXDg0NejXiJ49U3RGYM z6e=lha?dL%+>o|Gg1c>PYtvq6#n(ey3;UQjRsy#&@}ed@Z* zmU?zUNZj0lW%6E~tT#^y?oKHsOx77$X1l5AZc6qrLQ#04zX9kkfWsSJOauVL1#YpR zqWhu=cP7Fx)+S+Tl$q5Im&5rrWR?CaiW8$Ez(gi5Rb7naI>AQ9k=dwBibeoLeoO?;xmC+Ti}S*)Z0%-Iu0MhPkc5ac-vI{GPk7AIjJ8_HVS2-2B394{W4EpIZeA)MwsO0t(y?cB2aGf$Rv6KZ>PuF^aAO2FYjw@fhQDXHJS zQ6dI#xLhVG7pQY#^K6tn<6(!!%LzOlwVJoY-n)!oPB8ztlqS#(Jv+pI zUE*Kc8TD~*J(6UzS-Ks~tt1U)x&i4I)J0U;EjoNqF#i*H-3$28wh8g)!oGXCS|uZJ zs90h8DsR6e>p+qIt1Wbr>uMjn+wXpLRgz|~b2k8~mAkIrJaJ&3*v@fAMIl`e|J=r9 z6d)+VpGVMx8Xslx$=-hvtxgn|W=}0)cP{Zs^%4JOF~NT{;Yp7)KCxtdMsYbWkw6EC zJeEcx0Ofrf$bB8SeJ$|`pqCP_cM?H%5=6Fkbv9?~H&yZHf;d)tFwp}|u?Z{166=LN z%U2iivN1H!e2m3&mGKlb&;+VOO)7D7it~3jA%He`6MEn@Ch&upGXhKKgcw#)MQ zYbgeCDmIS|DIxj@kvx%!5ouNz<|O~9Teql@x#)o@p>PQ&kbHHHIpPRy2MTydXUS-c zCirW_=#VcdAQUN+xRs4F*%|iO9D&7=8_AO$_!8ZfS|d4K#3+X6B3Adq6^UPe~ae(g(Kzb|J`Jm2m$ExdvYAcY?r1ac$>gOlg+yAtXmgl^YV3J(4mp zF<)z0R2FiTvf*K}Mr+<>enshDC_!h;~k$s^uln2`}8_SKV*xs%@) zdc3$2sW5gV_+P_Tj4I||g6Wxqi6@1*m^3kw`SEz8Sr>GP8*KO)=64Cj$P$6@nX{=L zU1Ehx!)d_x8bBo_r3oI?L@;m3QWyA8a?ww~w=*q)o3N-S*q0mP*kiTS2Q7#aN=cj7 zxgBdFnyBed4WS|?*AkVm36+)-U_%#isZg;}A$4gRBB+;R(g$^EUf4;W$g(0&ku?5x zh3nK41c#q-v9Uj&{SMVu?4JOavDH_<&h(K{#sG7j1xjd>g%76?LF zpB?I23$lPIfplv5HcgR0H7FMzmo&{seJ(*T=*cStN*}@*7cu7+)HifN(V{HkoH^;C zKYCvJVWH5|G5YBeFM~Txgc40~1G5A)bXR~fai9S}cXI)TK3bDpVxDucXgFb^MFbU3 zp`;K)A?$f7K#HcX^&$xxrfSf0+evRN(F$PH1cU<f zp330}ZbMK=GyeA)}4;gEQeSRr(eri$vRf8`;n769#37u^ta zdT~~PMqhdwBUf5?_|*Rsn>Z5yh*E_kHmf=zW||v|aS~Y&0BaBcR`9F7N^E8MsmH2k zJYtEf$Tn~_iW@VHE8z~)5IgTgjnSis?}l#7$sm&o9fJ504YM+e1Fp<8sJSC>IT3Wx z%2MCZsL0Wynt~W|fCVQ3tX6QZYhVRafD&?m2*!%60lQzpqN!t~sR!b%38PQ!`bBP1 zsJOwE9J&@Ofv>>|tiM{Zy(+N+%dvO0T+_;(P?@D6+jP=ZA-E9;^EntRp|381uP-}- z9V@e#NwO4~ur+%q>v@Ke37I|nTN|qua=;QT>$1NZvonjd=!mlsd9zE)Cnswg0X2Gr z5v(kcu@-BwSNs36Ymf(snzUbAA$jlzYM60O%e1U&w&e1#8OgJ4>$7AFv}S=47hAP- zYqS|lv?+nFV9U1>;(0AWjA?t1O)I!-f}yf8wSs{X!iu#gA*^}3xGk%%_exxSYqyVpdJDOb z%d%OEu_>Xl#7n-@p%PhuyvO^x*f71eCB5w%Bq1UQNEHCsYrohfx@r*pmJFaf(C93>%~67EpKE}_6ZoTu^Xz(cH& ziMtk4HMGFX!v7n-E)l~s48^EH6T}*&YQn=Pp~FCok5Q$?(1MW;va(j&#H1U(62HF3a-lEry(uwC-QU5u+b`@lqOT@kFpb4$V8i?wGAtdmQ|j=KhIjK~)< z6UwV7S8T^mg0_!LTa8)6nxV#LAqS6Jw~?#Kf*ie>N6fCT!EX=&R$SMI2n1m;e{K|kE$;lo4f&lya=taCxOf=GF2P;6hjRYAs`bv@Dd>~4{tF6^pHL} zpx1LFI`4`b7Y!S29d2QoQ9vz;dHpcNm3W*ZM23A zzM&idqYTbtP1;p)l|q}mXnoecI>(%mFMN*e6YkK?C!+-o6#x`Xjo&aw zWID1jW=OF;Mk3JHwK-V#WsaR~=I7C(Y;|)kqDuV4A@16e=xm^?V z4Gg|*t9;>-bVH2r-LOvG4bsgdCla61tQg_V#3x|^Ye2MTY{4qg%UDaw=1t;RAqfA9 z+6tWoPX%HaaxW8L1WM!$3%(RPE#bwz5;`yeI&jwkfTsa~1nW%OJ1nj1EY|@r4enqt zN8S=fKo4yc02A;J4Ko2`&@lOg5)%-LE73YheG*LW66G`5!@VFc9^>FJCC=$qag~UWS9y@K7Ly;uC<0^<<17KU;IRLp8=5cd8vucj zFFKIkFq1DxV7w>Q?!>MgPAt?qoV zF6+Qv>#T^>T2S8yjuLRI5`_fU#6|^1;1Bek5^K!|z8z2}G36-n>`(FVFw^bNuGoT( z5@fIia!~Hho$3yLRCK)sOVKeCum#Z`>+TLBC{p3kp%{o@ju>9)9S+`iThe^o={q0O zr5@|uKo7n%0fZzV(9dJuA^yiM~tB~B5 z0`EEb^Fg2JL~r!5juOb5^g6TjjP3LRKnDH*4jVP}uObNf5{%Oh6VdSGEOGfL5sVFP zFW=BH$W7pewBIVxI zET5V*Le=KN7y;nRo^8nA?a3k1QKUz|adH`e-a;)YOM>hcC;*?12Sa8hX)-`J9T@|B`*<*FfT>2xmW|m` zAsKpH6`oBivgyH@PHQ%7`R&_Esw4Awo4VyH!2nDL$_;?ct>1%>8OKB%$Zb;0nKknq z+S&7G(4j?-CSBU}>Cb)oY-T+W^=sIu_YAZWU`_1+R@T_oJ@EGJ+&8T}(qJ3W!Pv=_ zFK6D|`E%&erBA0`-THOx*|l%y-rf86?5)L*H|qb5XdZ`vX@1KJZnjmfRW2{2LUY=| zQ32`?G`R5oeT8HQz#RYs2!WCT4+KPbsR_sdO~a|B%;vjCaH{EapuD0E1`qKhMb}IxqJ{xuPQAi_|bW%zywe(UUQ6l8;AcG3rqrU)n34m5x1=&BO*8i8884wTmXpsy%4gCm$)Aj2tJb{}#x-FjQjNmH1UTWz&^VC3jXr z#-_3d0^`kT@*}gAVRa2)!)iqcx!V8B3}K3jvYJUry_Ed&D@=ukyr~w~PKVx?~;(BZPs2g3TiXfg(TTgAAFJpe=K;g~5T zz?pBNwsfp758v+!K)%_uMvES+s12!2oBV2VrLb0!>CMhNf0}8U;r(7H-*5|bA>eL$ z#P5OT*kjM}w?qiioM6iP!5y6KhXAzJX#$q1=M_7Q-4aOm16$jBleUR{t z~SrdK&K_>BzRcnSYyF1J5=%?pW9>)-#_!?=MAuz==5piY{E7ATU0CXsWC zV(=2V03Jk&P)tc!bbvw$sYNH@G8h0&K(fCK?;?kTM9wj`lMR{a-57Eps_A1R9r;K|MpBZKq~9Ll^^Gh_ZiCCK7FPg67^av|LMtK* z@vh~C0636L!BWV97BWE_3?x4X0mIn{^gvejCojOd7D<}-5%$m|D+w%^2}@*|RT$w~ zB2W@0o6<4j?QJ0_+JY;;;zJ{H&yt-|;x;dtNpX>>3Y|R2C%qBMHubV((Wu-&N+iyO zZIW=4!%6Mvxyf;+gpJ?;h7Q>OcaY-%LUEUIT>_!lv5zf;apT;HTkM$^_F09F-lP#F zDfP!2!S8-PqYe51brAFI1`!&yXh}_aQk14tr7QgmMFB|=h(rTqi}41BOqLaINF^l> z5gtXr!Md@mWFGUtWv`CqjtbRkAqP^b1373^UPaJ?_VI^Oht)!{ki?UO;fgoF!55H) z&M#i2M>J|D6~+LyAv6&YI5gsxoYK^#fZW_%xe3#d%@i#)Rh%Z{8dsjWr6EA=$Wsif zRSp48FDn7rd>l&_xbpR;a_I;^N!N{u*wZ6V>FHio!k&&~C2=j`O5#l96t|2jGGzHk zGp=aYIkA*as(Dyz(AY@-pCxioy3JY7Mk-t223NSlC2nyg1zR~0W}-PU8BLqZL7enQ zgu9w*O&r*b{uDKpBgCTyGs4VmHPm9`UD$IqV_M|atgh+JlX5fjT;*mgN|)&i#D;ZR z^d`j!Dm;io)Z!5OlD8%SKAB?kixc8ijlD85?srrZKGr-kC%pYx&<2u)-!@po8|H9_ zJ?!BH%P2&glCFQCYh4~{VS*7FA$M=GiD-ZmiOUUfY)Mn&8;#hdE17^6G;3eh1OhOB z0A`A4#IT#JQU_sh)ArFGc%cN@?Ws?TL4_W9aNN-acD_j8(k0y1gO?_%qcc;;FMRZLb=wi)0Fwd)| zbxrzQYg`+dMqFiX_Xe74&7`_I0l+9pS9%pe7{U*8xWb=$?M;SG;;!24O}iH}p=8UJ|5A71j4r+nq< z4fz^J@a&l9W8XRddB_Xi@}nnx=}iw}(BD1v7I*yWT|afvpI-K}r+w`s`TFW1QR=v- z>u7lY`?b5v_QNNB@r|!=;3Ltg$>$WjZ_Iq?^BWRtn11zfk_TfSU;EqVe)l&8;pF2z z`hGtD4Y9|6{%rSQ4uZsq_uc<~_{Tpvt~vK?bODOYrQ7oKmw&=%Lw)KC07$rr*y}$7 zJU|3Yz-gNaPzZ$vd_V_`KnR>b39LX0yub(?2$vf(1?)f%+XFb+jV`MZ4=h0wJV6v> zse!;i7PLSYY(WSNfJ(4~6s*DGnvDU>L7Nah8|* zbh8a71VHNx03KXIDy%{)yu#}^40u68EbK!3k-|L~1OcpxJn%v@JVP{0!_QbHIhjH= ze8Z&?vpZM_tAIm0yhA)pLD$F=oXJB!v>BJ177fh4KP*H;JjCMLlN>=rMr=e!d_+k9 z#6uc!!$_<|OT0u(%tZK;!tv-sP5eYq3`J2KMZ=Rq@G}aWISf)j>xbJa8<>5EI8->_&B5$BjEhb9_hSnkmz8M|iA9ijqfm z%tw7ZID72J5F1C(@JE3}E`Z!egFHxU8%TwusMcFZhwMCrj7W(Twuh|9?rBJi%*Y#= zNRI5tN7G1D-Yn$LvSOoJ`96xX8Ro$h=H=tW3@njXdxJRH_L<$V<(% z$;>Rxa_mgiB%g)Ag6k7Yszl9~JWbg|ztvRDPCE)pl*-y%N!skq=)29z%mYp{$*u%W zohSl-{7vLcKH;2986m`b%b96ihL5&G85XuC&O-tBG0vLkjDp2^yfD zo+wYOW6tcvz3miC9GSGK#Jy%3|*Ke=>n-(h%V3)P0~mS#Y;G%P^82?2vvy4Sx^70l5xoiI0yhUz>FwqhPP3O z^CYbRC{QZ_KKBI2A&sN9>`}OcNS}nhtZ*%!sILIUi2y}377Y>?9f(zk3vq!@7%j;$ z4NEUY%XlfsnS4AGz>&+yirvczJzxYqC<@!z(1TdhjwsNUc$@M6^t&wmy&V0|))>@6 zCCNKI%Xo1g{@c@xJW~@{ye8?0D7}e}NKXRu&w;3f0GLtEi&Wqf15hnfO3lbj^-4ML z&OE^aEXad8>`CZr(18$E@d8wrDOH4xkBF9o8le+^R10annP zFEmZjHsiJRB-f+>8IuiFl(oBxeZ7^v(gIr#nC#f3#GKv#?8th2RF~q}y?fdBj69p2 z&W}yHoR9-D_ydd!+Pj=nrv1jDCEB2)xjgs-Enujd$O5dDgF}$cb9-8c1X{5DyQu9} zoQ*jlS)MB3w6$Fxw!JV;Qiz;Q$gxdKr>$G)D_ffE96eoHbE$%HdRw>++_+`i;v@+0 zoHx7`NU>d9w$t0a#T+jfO^%}ptvv|A4S=`JT+9uC(3CgF)lT0F%9eFY$dyU&**H01 zsJ2}Q!o}Lnbz31}TQBfhiVWR)Y+T%J&(lT8)Qv^8J>1sSTyr5_*o|A+r3w1Hw%y&# zyKP?A>s^%0gF|8>;GNyHy48CBdEZvgS zV2)%%ZZ)_QeqRNaUxq5q65e4R&H^t0gt={$h0p{@NQEUHKuu6$O<>haf#Dby4JyuC z8NnwQ~&Iu;2gv6bcD`r0Pjbql!Vu}3SfvX8J z24C)lTjV_ohU#MP7zj;3id0~fNTFjaeO@~M1xq`=V*)NG{3T)t+0_T$&}<7?i6Zl;cdC})E% zXOt#ra_-bR*&k(wXuj)Tm=;=z&PPQ5*kVp8xZ1jjkIrS|_2{3T<)H58s05FJNa%qG zW`tI0rWWUuZeoW)Vwaw2lapzxPQ02nNS7hHgKYxe2+z1%lu=$klX zq%LWB5oIKL>8qY+E1qk{%j$h}nd2b1E%1UpPFq{hGM7-{JE=B8e3CVuM1 zhH7DUYhnxNxK>MM@L8LC=w!A^yw2--vFLMK4Oxz5EUTnu^ zY=YK6%7*Awn2bKH7tFp$ceZOzc?S0C?0gi-g4^fD*+p{leh23V2?yfP-wrgRbiS-UK;T~=rZoGoqvU^T$ z6Si+5Uf|;vVdK>T#BL6nQ0khPZku51HKG&kc9|R4yv+G-Z!*{R#+)r(v-FOa@*ck; zxNv@ZZ+(Q9BW7NlXzmg2)=Cr;gX_|QA#Rli<9_Vu3grY<6fJ*{Z&50i1 z3VhXYB6XRTP}rmxPw{rsm+o=c%5Ar^>iyYR8c}i?aq^w>@c@AGbHxcMFA6JXUk=Yl zik6cV|Js}&W8|)7GH-65K68EcT(xfN0FP{(AaI-rbL_CI$pC=^rHL)q3^+hhxu}F0 zZ4xRKk1Yr9g#d4IHCJT+C~rXD(WE%<95Hn4uIpU;2H@riNw2Xz=agBcqfOUQoA?Ml zkn2qMlrCS#uT%%B6KgMs_)n+{w-uVrL4}XpVUx?REX@f3gtut!&YKcId zksde*a9vNSVYHc5(Vn2O`B1P39|(TEiDWm}W=FD}aB?bVZ5d`ShOLQ=6)&DB_9nA( z8y)gq8?bV3wNKZgIO%rdJoRqGs2Q0FVl23OW^Us?bHgTWtQF&ck9F~dG}jj97>{l1 zmhl1X>S0?jC~t2eZw;RX)nmU2GhhMrbeR~CcZIm^OA^o^@$qWM$=+U~0ZoYxJ<#2b z_=A{uJrje@T8MG~zloZUns;|}nY}oAUq>7%1iv+N`Q_Y9HtYIc^I4B|`V9ahcKGE0 z=A004s6T3iPlZBmX^4*(NYD0t1<;*PW<#G0Ctvh>K~{M224daFlaKVxaEK)MhCJVl zoZqvQNOzS1}R>WRF+ z36JM;z*omTY>od0aeUU~#GmGB#$`1}aZZk4=(Y}?cx=jN>h=?Mpo9HHrB$0R@;JS^ z0KHUZS9WgyV*Nd0Sn4mi9${N-{SG(vjU!G3ChUAhZGEP7 zS#Mo{$WkQ$!GePLAWW!mA;X3a6Bdk^Dq?_%sUT^hsBt650DB&S3@K7!$C4&bqD=WM zB}PP8cLIzcxH2M$mKuAo?80*AP>nTx1|YieDAJ}Bl}21iBPT_sC1nO! zRg;lZibZ6qfYY_)NQh(4qSdIUAl3jit!`{<5^CDIcB4@=(^W1~x)GVJ^lO*kRD#U{ z=z&X-1;xaJ9}_Gqc{1h7mM>$@ta&r%&YnMm4lQ~#>C&c8qfV`QHRk|%=2W@D@*wNl zwoeED+E^t^!QBK0@(uv}cYxr+g99H(P~b`C1oO~FP0=Exix=f#tghXmKGTwer(YZkVoD~5pu=#wxpB98Rr{E&XpukaH>_*kvsup6w#JmatR)H z-f8#Ij4?i@A3-sw31WaHqUk1zb+s9%gLck^Cr1_P+2@>)b?B#{Q8`s-06?W>s85Lh z#)+h(k3t%$q?1xwsil{eMqU7Y5JVoQn1Z^QMp6#<+mpgEiE5OqqMGV)O;U7TYJGG` z(UzKMvYmFdYT1z{h?HmEpu-kLtg*);o2*ZKD%-5H&q5omo}p4(t+m%;o2|Cna@(z? z5lP9Ua!R(!n{wukYMi*~!s-QUoZ8A|u40Bquf322zzQNXN=skA{{kGaz#1_-u))s; zda%NFdfTwW4?`TW#1m6o@olY|s;X}5a;z@6=(vdNDV|}l6 zfCMWn&jITdBVb>rH8$BsN_Mu|YqM>v(Qm^Yx7>5nT{pHGDa|y~8t?t;Nl<^h>AMHH96%%mUX$FD>b3H=Lg#zy6B^mUb^Y0mt``z92cke za!@v%vFz;bIe0-9QG~1FTGu;>jh~~{Lk}VX%!{G}vpJUWp5weW+jHN&_t?r&>Tu&a2lA=3`;C47&z@GM$Ow9 znbUiLe(Igu?Ufx`*>)fFI`Q08Nt~pImIy6H z)@6!*3_v6)$-o|t;0~8amL_E;##5p)m8x8&w_37BHL~iAvGg0vNXSBtm{4)>%MN!u zlB9nD5+ziD45K{sNRIrkE>^@74j(f>0Tn6$m?WYS3uv%oTBwizNGT4M7c1N8&UeByo;Z5lt2#K#SK@P)0f>&*2zL_RO(aMCn&uvR;*;cc zBt#uC0}CdU9VMY=RECE3YDjY$xK4oaRv9V$_aYE)~cF`r4jCsT#vhdi|KWGuPm z?K)x#JO1)no4AuxG-?;1V9~-A`UT$jpNxGxsfA<;~fhB5MJ>*t1yGL%MFG} z7oim7B{30aQvwT@m3)OGW6fcJTXF&#jH0G8IYAHqTJa{IEH)xF=4x33klOTkLk%jh z3sVMU-jw{eUtPX!L1@r9gZc2AR~aWqg85UQEE&m6$;t4xNwy8+InR3DGo@BnVh=-i z#DONU;C7T7ITi$K@jZl(p7kIq|LHw-^(|LxJ7WQFGf+@@G=VNXV2bKQq%&3&d)j1` z0tYxnsNP7JbM|En^@r219?MENM7>!@(Wo6LQ(|TY5+_Tk&hg_+7n+n%LCc0h(l}DEtl!cou%=c<2>gVUTzwr z_HkGywF>^8)Is(cRY%plSrt$E!eGwyr$fCyI-fe#t1i?O2hN^9?>OTm8+ARQl_gjH zdfS{%_OqjXSyo>=+uIJt=rRXnf%7=sg;QBo)|`=Pe~jMo)c2g3eeZ%Fd_>%C_`@S! zdlu|)vg^)uRuVofUQVE?mmYbwH{qI+TT9+tp!z%_y+Zk4^AE|JABVMza$_?LP~mylxL)$BGw1kY)y2ML4MP` zyil{vrr-MYw@v&}U-uDU0U98Z@LcOy$!;*9$HWuQ-(AogM20lMG|!eGoa;0!Jxb6JVsoS0DA4196KY+>L= znFKhTnNP6a2Mz{PJj1NH-;;He{)ph^$;C6t1WU1mwh5U=B;iJkT}nLD7P270#9$bT z;TVcWN-W?Rl1vWj;KKYMLF8Belqm}q-Pc(_lwB<$vdm%aA=MWWS)W)TOAH|&4h9(# zVj&tLWqjXp(BLB8;EN4aa>OwGQ9&%6 zPF!MC1eYLE$RWDoE52euaLq=bp_OQXFEy6_7$HS4!U#Cog`A?FOc;8s7gFRG7wsV{ zBFiswgjjR|M$w5f#tJM_V>J?@PpDy0olU~v*tVgB^93U|`4}(ylrRY(OK4MvKt+}r zALW2z)Cq>6gjE(r#2*GzoQ;Av;KeycL^n#KH3DQn!eDs>z!u1X7ifg}J=Ovt2!w2e zZ52o~xJRvZltZdSNHtOaH)W!((HuosBt@nm&AemOt%#-Nk*Lv{r}c$29-TnCRWl4V&MO<1Dkoqc6mvZaNYH*I66S^Y@92AL+d}e8WQD>qi!#rkcvSwPCrq>mO7szG+Xu&|prWPRNZ`@)_ zzGiQ3Piq3F)U9U!a1v)!`eqk#1VZ8_LEK_)GABoj&~Zwq=@e&my5?|NXLhpMbViU# z+$PD;CPg@B?Qmy#(v5baXA6o%da@^JnrHW%L~T~VZvI|#=B5|aCRx1aeilu8`sZt{ z=YI-l;qhnoa9nNHCVoc5a^@#~j*CWISAj~X#0+SKQlLXv=zvZrb(+L{+9qwz(=$rB>jJ0WTuIZn4WuN}cLC$HI-szJ5 zUWqQKm?r9@KIftm1Q{?Ip<1dyv0^jq;z@C3pay0l_Sj1B(58OfrBaNfhG%Xz=X27j zqK@dHrYft->WJDYK~$=#;%dx%;Rqdt5v*XGJlszFDklV`r$&x#{c1Vp5w9Z1Sa1`m zT3oI=3{=ggY{KfQrmAu-DYkN}q;@N$qYGACgCI2y1=l06nmWC@x4^8td68 z6d9CYL7Yb(0H#pI`F4%t{K>+-NrgfydG7!18((hU8s_v$8((1!rtG80D zs-o)utWKlCYHVom;Zg9xQB=V(i2%D&S^)IVMo46%xJJrO60t(-=fo=>u`I#3M!t3= zHW(os5|OM}MIIT!1f1+c?$pde-NtH59&9VgI4Hy}>cfWXZC328erv@tZJ`YR>i&cm+?Gdw`Krx!AVKJW zMN+QiLR?COu0?i4A~HoDx@@d)#ArqYf}I`PlG0fi0XWnYTtvfpTpvO35E+=@1W*D0 zN}>g*23$dOt@14e&w`o>ek?~!0O(fk8Zx01|! z^5*rLFG<910Ehq?RPIK&uI1)NZX5+R)KN|7Zu?G>5om!95JcD(#QO#S|8h+p5Uob! z0Tmd5KeTQ{+!r65ED~%16$F5VJv7tv5``dq1HckT0Mzh)%yBnVZZp{~McA=H&;$MkN;J^Jd=xT4yb;~H z2vJ~hmj(dN*2ME#E>v+sO^Iy)z);$*#MvT&6`d{G$?oi?1P|#zG<3s!h=@pi!+8{f zzmf+5M+FZ_a7}2zH|V1#VzM_#Z&ut_P3$XF1%e`PEvOj*Hf-QgtipZqYC#aI4hO~} z6J#FrDmK_|<>31o*m}ybF%U>L4X6mGDXN1gr-n6Qzivd3@jF( z!~{T9JRSu&+^ZA=Em8RMq)K#=ZL$t5wjYTw>=pzu6a*MdLn)hZ=1ts>p)Ad!D?w23 z1%3k!uL#b@vS~*|9~ZUdQe+5-h+gCJ@lKKu_l88SHipQyKzF47M!2?4{$tzO?1=Fy<6001Dno^W@GD8}9?`Qx=Lrp;y zR1q=0a#|6;_XIn%4)e1Wom&46Z0&Mx0CD%fl{I+MXv7+M(;hj~D*4ksEvz!9(mH35 z-*fuzb1((7A;`5u^|0F-Gh9H`Ztg_Emeo-dLP2QiP9=r^*iLim9)%&$Lt;vIB0HZg;Qas&|iY3PU*NMuZ56IYCTw6+{D{ zYsA1J0zD)jq?-s|4=lG6#We3IrHi}z7Oc2?b3uafiPHzQS5%7+cnXh#D074yR4x)I za2&(5=Bxz&>HrvTM8Bsnod5T|S8kEahY5nJMu36;VQ4x1hWI!1ZqUL75>SB%@NqYQ z1Gi7SC)X4wh%Nv2!F&++r`t8XvoP1v1yndS%Q?HR3wy9HuB+k+CyYPP#`^U z-MlT77{FmdZ{93UTu3iKw*Vh64*Z5vW6OjyY1XuP6K77HJ9*mV^Al)Lp+kulHF^|j zQl(3oHg)+r=GEj>buwKOWG@J9QKrdPbW_9}(Zd|!@>DIM- z7jIs@d-?YDJM?T|!Gj4G7TM7-&4n2M71w0!^07c9E(I=5EHK2(Zjo<(gXk_HXqwXk zh`xLpb!weWRkwDH`s3h{lsV7jZ24?RPO$@2!CiTx&ELOA%1rqCc=F&0CzEmLnfGhy z(`){h8=r;eAxiz)1zOn-n`cUYXb=4ejk5+{rma%_x~Tj zs;C=~K%4{#Adiq*tIaT#CeR|9#~3tgf`L{EfQkTCp(i?=Nb_)}4+|V|MAk?=QKtz- z3ZO!P3~^0G*I1%yv$G9ZT?-J%AcY(f$eOx3a=iJH3~#>j;Cs?N_TGz9 zz4^qekH{>w+>*;Kz5EhPp?nw;PJY|yEWHIHG zO82Z(az10Ny%yVSwH-FtZoOUbG}F$S)i83u4eZu))m^vNR@tR)+j!-*_9n6@Wp>FW zoz)b|XQ7nuJ+`o%m*9d8J{aL+-(8sDh8;F6-9($A&fSMCzSzfvHQqSLt!lbA-}R`y zcVB4zm6krT1o|qF062dC8RnQ}z82$}ZPrQLo9~Qx&u*{@02^R{)|u#{7oHjEr1>)P zuy5GMAmvUN_r|o0!#1&thah~sf9P-H98hcfU*p6Gn%LNj8^SFc7 zT=LLGpS2voj9B<~OpPqVSlfNE&c6U!y=%0^|`@*$<7d~j9Bg`Isi>qJX;Bx_1wmkOb zpP&9M)4w18RJY&%llbA~RvUAxVB6)=R=)flP+j#?p!K}vq_Vs&dJB}`1S$9z1YS@z z=AzCt++?%;-AO^j8=!?W_cq~i4udJ|RRvktC~uMHJ!`3glUi6q8{SYrD%9Z)PxC=L zc>^J8(;N!hHo}?wjBY$sqS$cQL?Lwxg7%uB49ns~D_-%6OVr{PS3@~mDM)Uh^IzJ& z2S6}#5sd@#1{T@KpKf_2id0;SSlU=eJ1Q`Zc}$}hzc{p?5s@ZF6dBSTjIwO8H}msf-2DtD+!>d>iXPT6I!B2zxlX)cMpROU(|m`i6~r4M*ZQe3dP6@~015@xmOeu!eS8ApmO#06a-IPJ7;SYk3&i zBSnU$+7c3`64-kHk!yp9G%{`=(p&8XE!mKn+flbYz z{w%0SLkiH63e;*FRq0AuN-zYikd1pl#7k4!Qk&kiRO#%^m9m*SpZ4@=I2GzpiOMKk z-SM17Rq9fom=J6})tWzrsZj=1SHH3~0HHYQUkO{-TsCYAku>aL8M{)4HWspx zeQ3kFxmL+u7PFKTh$B?Bhooi}w4t3|cno(g(ViByZ0uX2_K;fFzP5BT2}_mA30vFV zwtlq2?QVHHGkwZ%UA-0VaAhY4QK*Wz$yF}R0zig$Ef>1cMVd~9Wm)N77rR>uL|VR; z2ZUx9yx}$H9!9Yz&x#kl=?#$f)Z1S7I!YjnxYK#xTVMNX^;`GV?|!p%CSB;$y8IRJ zfTwb<$`TmC3BHbJ?WAA_KbVib`YwbiT;aHkNuL(hu!g&}VGn?QT{NB0IXIiUpc%hdvcSN%w;AU`O7O7M2@jsW(a#0$kJ)8nb~~ZB8R!lVD|Ey z*Ybxp-?_rnSu>vbOk8PFleQ?DaGwdy-`AWsz=c+{Xw@Rev*xv$c{s$PDZSrHI3mqG ztn{W+n};ow71TdXG^a@&-9GGr!I?J8CK6)hQomZVHqq;>14o8#!W!4%MRfowVU`ll z#MQYLHe+L)GLw=N*-5JCu$k><25;pO@|6T5ru}Q5jJny`1}oxZ3+Ye?;MUsz7I$(n zs@4FYde<&fH%-Rf?!JEd*zq2yL9y*_d*2)1`K}#xzdhAL=o{bx7x=&%y5ip872X6_ z_`(_9@Qtn3snrx%9?0x)i(ee$85dRT1|kQr1V9d3$O9SvErQR?^&Ib~7 zc?19wS7$obAvyKF?!@b17rWl7E|9G=J?lcZ<=DTP6Hf@_?Z9UH-07aMfym?RS2ugp z2SW6^6P3X@`NTj5&+e!H9r1||Q$o}(5KUBF=y->+;%zFB!NR-S6Fr-#ILMo4lSTv0r`RaV~xHpIfkd{+-YI)dwH_0#E?0A^c&^<2>TWUuKw zkOph8DJr4#u21dsj_Y9!O17OmwFbK1-CoZt~5`yz&PZBDQ z4D&DmTc##}PYUtS%}@iucA^5ia0~OV`NS^~2~P#}&=JqCCa~}7YA(A1?+ppe2>CA) z>yIZiuMX)D^8x`7ACVM6kGj_H4Dk*SwC^&6(7p(f3lGr?w@~@~j~2hMo=Q;{Yp)Mp zB=?$*9p+&6lq)qbu@mJm{OE8XJkb-utqFG#8dI-6?86naOb`dG7E$B%@DCRWPy7Hd z8pBZ>F{>B{?D1p~9V-wgWUug;Q5jop7{^f_=h3aok-sYc!TSU+6N}LQ_Ky=?PZ5WX z9tV;j7Yj8g@xA)M5fIT38FC$I0{L#@6ZxtjE7Bt0O4InO56dr}~g5-YP3oLZ|e8ZjZwA|FRG12ZooZSnO+Q7h9@ zE$!$JY=tDHD-P%I9mA6FvJnhxaV_&wFB8cv1q>(U5+XzLALTL(EsqhT6N7R(fpaqdPxid88pG2)x#Kn76W7k- zCsA`cRTKQYQa<++zu<*GdoBr)ureWYIIYqO{}Vwis6g~HL9whiA(J@m^EC0(CRnpU zFZ9Argh4S>$ULSzClWpN(JoK3CJu~4PZTcB1Vyn-3+odZ#}YM>aRXPBMz6w7+OtMI zOrN0ADk)PU)ssYNLKC1fM~l>l2E<5*Yz`Oyu@+x+D6tbKDgiN*luFG_Pd2nlvrrRz zG(x`*NUIYka5PJeR9MFJ!yxVm3(+j|G43cdCoI%V$#hlX)WQM}L_zcxZLvTHv^wZC zPHV+a>CZq5kx9EzBwx=-VRK3Yl|uoQHW}3#snaFZaU>OSPlpdu$y7R1MNBi*y&jVt zl`tSR{2Q|0urPPHkm)H=PLN5SVRxq+aNir;Fv1lvwNfWR&EQxez<#S`2xUn z3wpCxdq1#Y{KHqOcYDJ(dm9FJ_aayzSA5eKdhynKy+wWB7k-0+Vfuq?YXyAc7k?)g zZujF`_BDV1H+2Iyc{B7|?iYX$*lpR@jn2h@kFHM<7=r1CD}!r*-(p>Yx9)a=t138x zJNRWB?^`Ot5E_^+*oA~w&Q?5Fg=J?Ce)n4_sz2C8XySMl7>C=nhnv`TT)11%*L>e)hj*?fZa5*7Sc`MGx!Tr=znG&~ zwZnjzy~OS&xR{7-LWsebjfuruqC7 zgA>*{-FJc6g?615=?3D6Gh~ZrA`RfJi}6^CXIPMzCnwg}kk7Y}v!Y?jE$V234DJ|^ z`{slV3TU%p#YE zwhvBjRf;l(8HSOK&XMJoyB?bwb3APTksi&Z(8 zEqa$eT5B>=awphMD6UAv*rbuJZ>Tw=1tO?9+PSuQmG5{~X}YEpqnPD&D~xwchD4%$ zx|(e`n1z~`<=Kz5TBs?Rrj+_>O1d#qx3$bLs)@v5{F$54_?L@#CNvsDUV0$*SQNq9 zcI;84Q3MhS0gkm}s*es$**S|V8L8uXAheo`xvHt{x>QiZL_b#`6j#p{8!yxcu=SXu zyI7tDd#JJh8jF88GB<6(so~NKo;4f>mrm_TiZOQqMb{&=USjsS)j3cqbK>Q z6PmO+rhWzE2W@vioLNf(fN<4?t0uaVF*}d*c()bH3U5(WYF!i3*`IO%HQ)3DjIqdTTCgsZ!{ z>(m=&m{|g|*R%S74|gKIE8!4M?KRg$C$Zbz>bJRrd7Ux(sy{owXIj8N7z?)-)qo;Q z5l6x|w~QxRrms1kH=37MTC-W)wLAPvY&v}}3RI}ESWf)AX9C`Ao0360x&e8&OI*ge z+n$U6{F%?im5mOz;d!}t+nR^EsBc)5ksM)`Ii#&TKfuktqi!pZx}sa$x#d~5Eu5ow zQp-6;tidkE)BGvag~;vgr)?OOi+Z4!xS%zMZ|Z9&Pkc0!+XlJ8N9vt(wQqKMqNJ$JyA6M-cY*5 zGds)&JFES>5CYuPubkTi!qY~s4coldo5JIIWuH@S&c~bB_dJWE0Jjrj)>XaNDc5D} z<3O5SKVYs`m>u7K`l7}An)`bo?0~nW9o$uGg{=MF`sUM`yNg@8m$!Yq$sp#)J>28} z{d86(xc_dMPj207BD@FUMQB~#{~e2HWwdEH0PLW!g*ls@t7`on;2R!?P~Arce(tb* zrqX~6^81wCec`{|;XA&C(n$`Yc1%1@;_J?17TykUQy^3x-aX#sk!I%Z#D$>`)vNsF zZ@z^#lT75m4j$X~(h2B+9_NR?iDnKEfEGpSn&_7vfY*FFTAt~no@5~j;@Mm3uReeA z1najxeurthxgPAZzGp8(pP+s(1AyAXl-D1TCN{wY*FNmW=~MDx4yr)p$J$Ry_2WSeBNTK2AVP%-KrjFwgM1Py zT*$DY!-o(fN}NcsqQ#3CGiuz(v7^V2AVZ2ANwT0xNbF3ighVi<%9Afi%A85Frp=o; zbL!m5v!~CWK!XY$O0=laqezn~UCOkn)2BBb6&(1os@0de1YU*8wX4_vuVA5GeJPJ+ z0G9<>${X7!tlPJ6)vC>oFKP^m96k*mXi@hc&d-fiJs&nh!ZL#WF$&!6X zCK#L(@8igmD__pMx%21HqYHgG@U?5$Rc9B}UQoLC?{lpNpG@2(B+TQ<6U6N2Hu(4O z}Db zSRM8sfE^a(UWO!A_+UYpsfXf9Emc+Gi!jD08pSl*``(Oq{rrw1);?fD~JdQ>#x8D zE9|hue$~fRq^261C|a3{?596^7(k@~v{mf2JgM1YVJX7I+8o%1EAF`DmTRsdzlBHJ21kuvRu^XqX^2%4rL53W1lmYY0G0zOM%r4(-bIm#P zOf$|u?+mogJrC_O(Lon2^wLTs5a;`>vR7bk}_$2OVJ00p5CZ!1vyG`@MJHc>`X!;D{66_uq&IuDIcg z8_vPwf=8bC-iI?DxZ{{dZaL?gXD&JApQ=N*jTgB%%aYFK~o3Z_Qcgr+KQY^~c} zFd~3p_cqA18cCV%#20V;e0X=>LE*~l&Ajt}H^039^UyQ@H}%smAN}>&TkpNz*nh8m z;o(2JzML$R(>;ttVH(>6S@q z<)9P|@PQDF;BnT6KIgG7d)Tw!`@o06$60y5MuOI3ka0Nh0tGEt>#8$y`?B}go7ZR>XiL=h5)NW>;O@rixG-1|B>#p;2u zd)~Vu^Rg&IELyRPTl}I4(?`YdadC`i93dICs5cwFYENIXip2CIt+EwE4TK>RBXS4D zV6{ypWeM0x*rpz{Ai`RGOr##kUbxO;C<2KUNUz*S8L`audES_k+AxMf-JKyLFUR^ z2mF{Rm7E=)ki0FL(1L2D-U{T9yb^FTg;>m+A4q_=X=XBv**iiPv^YvH zsK9+=0GB{$zii?Awh6`fCn?C$a=tU1X2if4RQka! zs33XHE5ZoWIMR`t6sM5vY4}Vk#Wuc@nRo&y|6$Z+D1g3;mj#U&0QZzpOa)*NzFQ!0 z9=fe*DYU9!4XaoaQq2x;&YrqZZ)aUzOG}AD42Tp1b5&va#?XW+epIb3 zWZ?=+=+?8w)P--gXI`hrI;7?)0DJ5YFYBnwQmG+c0;w36JamaYP<5<$8W_Ncqd*2u z%1wL(t!h`>%=O9P4lrART=O6YFm%8Va`*!nH~E4ZXtUwT#_cwu=zATAg{*#QlV0cKzrL-M8|2kClH4z%5E z;ru|&Blva<-P1r5t~=ch=s>(27{Pzr|C?M4EVl#Utt|-jmWAs|7kpWWZgBH91u*>9 zc?S+56@C?fOH5@T194bo33^A1HOmc@1zSSx#acS`;SWx75FwWoQIjl_#VyXUj-6Ok zIrIPoE2snsI#3CGv7hVB%mH|$8hI7llca0 z#sC)7KmriJp$E>pvYOeP-ad2L$Ce(!4f1^EAp^Dpk{;|90@$gO-H$)o@sOAXimJmz z%p|Yb<7am)CnGMy74^j@{3T%6VN@!1=9k4dGdm!IDoZxyO;OGe$u>zn@`Pz76 zH@BBZ0!Cn4%NU6FP5-unZA+U2`aZZ1c8lkZzBk$-bR~{m_aRjwl(J*`ryYXiYc%Og zRyEf3F~8dJlABy%gX{B4wM=Ze4%wx6TLB~Rb^|@=q2AeRF5jBF)^~D1=0Bf#-VR)5 zJs5m>3zq{Uw(ZTrb>XDj9GuKuZtt_!{N_2AL*Yum2qh0Z>Usli%M>43f z>_s^=6jryXWe%k^OAqk})vwV5948L>8!d^DNMzF>+E4%bU#r>DmMNC+;DZs6QxE+| zPIBM_cgBAlKmuxIZF>d=kJNJUMqXyse<}cgc#~cvFoBR}RvCC!aBu|51#Zf)vsFl5O$fFqy+ zcTfZ>ScFG-fC(rEEbxOF1OimxSc-yoJH%>aVmq}n76Wk`>t}z3cM!q@QG=vR6$OT9 zn1+{vL2)H%%~e|*V1#hEbaFs}9pFuMm;^qEUF^m;2?tkhP=}O+Hz05XuQg#9&;!JF zR<%b1C8cz7Fa~;4h%j>p!Nma}a9(iW1K?9|3Wi;)hi7>hVAJG-=VghE4#0Jv6gAc#PihuJ1y?=xytm?&Q|HU=diDn=;9a2Wi-24AR#HPQ!& zFbDe5Ex+T8|JHboc2ZXGvpJX(IM3FN-l#Z_mONtDbKNLD-k3a$BSkoEv{Adwu|kyElh=Ob9d7e0S80@tQDCV5HDGm`HpJsf0jFqBQC#9K9ZPVDqJ z9bkw{cLS*-2inC!!gqr!DU|4Ak~_6R764bNMR6~YM+fy`w*ys#Vh2Ftkv38-7^xC$ z0tvLRkyiPYU@0R-wMHcAY=ZSdEg4GIgiY6kKK52kYGjid6j)dkJy}FX=OmY81V&`Q zPX~DwupA;bwgY0H;!A!RmM>Beec%VIfOrPfn1~sfl6fKb2uA87lPg7%^>mX{BzkPA zS0vSzYw1O936esUSSNHrRB)Jghb-k6AWiv{1OW#gwqldHFnn+_eNYG$$qIAeo2K9g zh`<@QS)9gcA1OCdba|Ta)0}82Px3@Tm1$4ygHDHqnrwMbpgERxl|GFXjImTEku_N< zCMvTr5FWO1uymXyViJM?p6nT)@|hd~0RSQS1O*BJ0RSuj0001f2j2q#2>$>J2pm`- zfWdl?vZc$HFk{M` zNwcQSn>cgo+{v@2&!0ep3LQ$csL`WHlPX=xw5ijlP@_tnO0}xht5~yY-O9DASDq!q zTBN73+lY{A+)S)!kV7|w5qO-jvbA6)9^}_Qj9@oPLeYuAj5m51ltgy{#`v9hVsvqyN9yIL5VvjbadF#y|We6=O6sU=TwyHyBij9vpzc%YhAQ#MuqJq!s`~97qD%CU9`j z!6l$wndO!NK=4loY>5E}iXfQq44DSeFiHe620Ei(=;26{9wR0?B1=X#>gc18Mk?u~ zlvZl#r82oOoza5s8aflAiYCe^rqotz?X}outL?Vjc6-wSnu@Wcr=JEG z9d>V-@RmX9^6Dis$Qp!EG{~ITu0hkGdyom!7zC_AX55KTbT^dJ3JnIaPzs>RHjI~e zUD#OYZ8`bn?Zp^ptntPickJ=Uh8+M0J^#Sb6sV%!V4A5H1~7!G4w}G7B?e6i(G8{k z)3O;J>TJ-x(zZu#YyXRi6?oNILe*kOY=%_Yz9GTT9LG!vq(dj>GgSsZ**tQpVH z!;HWIxPi=uTn3Q(>e#i?O^KxifB`hccGE!&ywF2Tp^Q5&eRGs|ZvFMxXRrPC+;=|{ z%F`d@mY5NkP%B#pL7+$3;kU1sPAhpY|NQjVZ~y)HL;9qs?KgppM3rDh?+{Xo61ney zcgddu7s$W{I`DxI6b=F>NWltzB!U>spawU{!47)xCl(B$2t)Y65}NRYC`_RWt#^PC zy6}bJ1KkQ|NW&W1@P;@vQVU_o!yYC}hB*wP5dVir#3C9|G70dZ5|`+m1RU{+P>iA! zr$|MjF!735d|wr}$i*&t@ryZ3Viw0p#*jD>jA%@w8rR51@9nURaEv2EfY`=5+VPHf z%%ir%D91kfF#u=OqaX)K$U+)&J_kquAOFZmI4Y8khm52oCrQal9;T6)%%mna$;nQ7 z@{^zpr6@;9%2Jy0l&DOlDp$$MR=V<)u#BZFXGzOi+VYmT%%v`O$;)2)@|VC2rZ9&| z%wihzn8-|~GMCBBW;*kk(2S-ur%BCfTJxIN%%(QC$<1zh^PAudr#Qz+&T^Xboajua zI@ih0cDnPO@QkNC=Sk0c+Vh_H%%?v0$^XxO`tzRv4X8i|O3;ED^q>e$s6rRY(1tqn zp%9IzL?=qoidyud7|p0gH_FkDdi0|p4XH>+O45>=^rR?FsY+MM(w4gPr7(@DOlL~d zn%eZHIL)a}cgoYA`t+wj4XRLwO4Onn^{7Zqs#2HA)TTQ1sZfooRHsVSs#^7`Sk0>M$4lPwn)kfuO|N>_%ii|7SC!s) zuYBiA-}>73zWB|re)r4Y{`&X701mK#2Tb4s8~DHoPOyR(%-{w)_`wj4u!JW};R;*$ z!Who5hBwUN4tuza-!MosLhMH)U?vYAPO*w@>kTjjU=<|pBND)IkaZC)#vh6Aig(Q8 z)qdm03DGebY0MB8lSIfpPO_3aD+hz%m}q@mMF6N^V=9m&4@iC?d6vxOE+^H-0SG}t zzG32pkbxdZ7V|>X9Dorsg#Qy^5%L6HQ)YzVILilNvzPbG=f-k^9!CBK83JHrUOZU` zRz6641rg*n8^j1rR!Et}Ok{;1AciEd*c>y#F$Y!JWyAd%OJQ+}r|*BmIp)8$=LK9{IUHzK}{IB-#zJxVga* zZeuTG+eQEfjc43&gc$kU1K-2C2JQ`V4;tkl-#Iib4)d;iJ(&6qKp^-qcA#hb$KOzP zj7iJ~G%rNip5DXL=j`qPMElZXKY1W9o@c&ed&VLGcL2m~5HM`p8v|GN$w~h1X|Fo# zUyr=xo$3v5%(?QISBRi_^>M7z8zG)nJj8YI;usHH-p0mrrD=Tn%1?yrDhJ29Nc@eD z>o^a?Hav-Cj&jjY{`bH)Dm@fW1tgpv5HxCh8w}fPY!_*w2*Lv$ic*Y=sOkI zY~xKE!P65cKL4g2?sL!|gbHOSyv2<`Wro;T2b(?!)(ukq;M@QHI1M<+Pal9eH>1S~ zA!ZXm26&cmwUTf^c66(N1arm^->?d#XAr9p4MaD1-B5lGVFdKBbQPEob#{Ak_j&j? zf+ToT(SUi^rfA+k5AK$L>J|;(Ko7`P2c5{ZFq-xn1_0}hkV$Fe)xxg7>I&s5F)q`Z#amEn23tF zh>X~Xj{o?GkQj-QIEj>4iI#YYn3##0xQU#oRw(z0Cl`vKIEtcJillgorkIM;!w0C? zimB*|u=t9y7>l$xi?&#cxOj`Yn2Wr)i@w;4!1#;87>vX?jK)}u$asv(n2gN0jLz7M z(D;nf7>(38jn-I=*m#ZFn2p@Hi@ql#-58GBIF902j^ucb=9rG?xQ^=Bj_ml3?ii2o zIFIsJkMwwQe830F<^@&2kN((?0O<2bquvxsVFkkPP{d4jGXUIgt`s zkra864!JaYV38b&ksjHRAo-CZ8ImM9k|tS_D0z~)*f?Z>2r8M9F!_=)8Iv?QlQvnC zIRAN*I+>F^DLosxlR(*%LK&1qIh00Olt_7$;b@Whs4RfMib|Q3QW=$0Ih9sfl~{R| zTA7sx`Hcu&av7I&IhS@>mv|Y9tC*L3 zd6$0Kmw@?~f*F|EXm@Pc9C;uJggKauxtNaGn2`CHk_nm9Ly&vGijkR_lew9i*_oUv znIVaV%`tYL>6xZkny7i2s+pRssf_Id2w=IIve}xnIh(dwo25CLQHPti*_*uio4y&G zz!{SWgqg!BoXB~c%9)(Z$&nk0nR*eM=C}}kU=Y&ToY=XX+L@i)xt&INnC;`8-TyhB z;#r>L8I;7CkKj2GC>97CDUTj;o#r{8=SiRTS)cd`jCp_s#d&h~`Jee2pa43cznGH) z2c0NZod}_9?&+WR7!nOipb{#e6gr_6Dw`E#nH9RB7uum5`k_MElzT^t{%MaQv6~>8 zq8_@UD%zrCxt|s!pDsG1ELx*9dZS@k5;AI|Jc^?}+M_@!jUyDKMEavfTBIaNpOv5q zpP-}0d8AN^q)|GhQtFB#^rTfvrCOS$T)K_(xp$h73Yws!TUw?E`4L!ZrfS-yIXOZo z%BE`?r*Jx_E&2&e3JIw|rgXZe#;Buw`loX0kwY4&fLf@8dZ@s85P2E^cK?bHseq@S z0I6XbrU#J+ePF1VT8ev-shor(yc7%1Wt{a0v8jqis2`_UftYSwRN7um{Vq^@y*I`U(3gso$Eacgm=czzWvc zu!kBbc_6JE>#$S$LLe)$CL6LS8;=l+5UDD$l)9?1imb+(vgc?JoB!|!mEZ@P@UiKd znu5}jCmXaB>OyZyvqF2cMoW$mo2Q>(r{Aiok(vs|`ltagrkVf=lAxGLn~VyPs#S{+ zG=rQDgS2GJo$^|;Tbs6JyS6^6s#I&I$||l;+p14Hvt`S&Rr{zDyQrv&5ZBt5OM|v- zJGh?7pF=9RYMZ!(ySPKEsA1ZwW6HO1>#dL5r0$8djw-IhdbJq4uK}>A@>aBf+c=2J zxTb5D$#%AjD+3Fmqc!vCwcSF5+0E4ANCya7-N zx;wg#v%boQnyS((75c}Gs{pz^C>$N94u2qYt<@>nW3%S25zF*s_0n5J0 zTfYvhmexwY5-g=fzda6-dv0tmdaNDn%aJdr95CcrUzFWS(8@}Rd zcX;WQOXI!|T*FhDuoPs&I&7K>;Ry`k!x+4)9E^?h$$xRXy^v}U;ku|BOs%lmw~Cs= z1U#uMJjI}EopD*5JG{k6=|U*h#6uj$ubU7)ybzdR5U3CUo{+|pK*M4jrx+}zuzRei z`m{~D31IBRbbQ6c%c{OhyPrG0EzHM#tC(4QK+4O-ivN6(@d~PMyvUrH5UAk8lZ**z zj0t6I#+fXCkX)gCaC*h6#GMPbkZ_s*3%)AMva>6|tQyGT>b2xswPX6leu=}R+sU{* zlJgfrx!lWEc@U^@#%rv`lf1^Bu*PSs#>q^~8QimvJhFTM2$V~#`P-`M>&ynL#QeIq z1kB0_ET)_5&3o+4?dq2kq_w`B&TDDQ?UTsQtj<+=5SEP0^qj`^902o-%oAbC?_8iD zy2}8Z5PRUp3`@se8@#i-w=BH5axBW@oWAIcx)YSg{S4148EL$1(HgCi330}g{Lz?f z&%tcW_dLuDA;b>MJ=CU{)Xf{nRO_~GJEj|830va)Nfta-MSF^T*<{u)st)xA&t*_?bMd+w{~6FzpTvw z%(><&zTAw!IlamOjlO??&0<+W1P#}ioud}?u`iw3o*kGL!Pmzu+EXpqfnC*4oyG!} z(x2V9s{FFpJIGe-!u)#HVm-Whs@7)7){R5ahW*>2*vL5y+{CSoA2HITecF0G+NRCi zRm})T-P*-{lz9-Tt{lr$+p4b|)3j~VR{yKrj*Gw&O+nE;-O_E`6O;$$z246F5qX{3 z&b{34UD~Max1Y`4@|XvJkOzOD-|D@N0l*4S?A77x)#IDO-JQx-8^8eUySg2f7Ibyz zjoKW_6V8r3o-8{KOJ2sA?AsG;wMD8A(z%Z!t)s_09#HQwNYoZvS8wSx@4guUA$WaZ{PLFFCfbS|Xw z+}Fqr-&GybPwnJPj?aSbPKnDgRbQGJnLn=)Wa;*BF)T$o$G!5 zbBqq_s+a}dq3G!9)ro!6UQ62n47z|E#h@GG-aY1Kx!JBB>~DQQ#*Xd6UW<4Q)wQn7 z2I0fY4CS?M=uvL&OrhDiFZ{yE{=y5MxsiSCR%!EsLd#2E^tWi? zOXCM3F~Qdf)d`5bw&X%~tEXXua}yYt}0)$1yL~c)GVV z&*LHV;BY_r2Wj?q5ovW_=M<#RbWi!7e~NR!pHmO@q(AzmkMBjUsmqSO-h9xZ;myf2l-Z}P*h{GZ77;!pnQpOF?c*yB&QYhVz0AOCzW|9wyY zagK^cOxq0Y`Y|rN01+Snq*SRi5hNHulR$lSVRwsQl# ztr|EjTEc@37v6~Y@M6V{89%-$Sn_0*F=_H)7!l>cRFDD~^bAo`XU(5EYktVk^TLHF zQwem87&B$sRR6cYj15_LZr;0n_Xhr3cyOD1@&v$A`=w>#!<{#W{#<%=>eHuV_N!c_ zU4fXiuiR5zymjc~&67ue9%BG2SJ=C6|6YE4`ta?~zh58!`#E_?s)k6Qggy&R6V3>7 zD53%f6wNiyJY(r1^^h{|I}2T!FvIjV)UZPiKlG5Jj4o6uL=aCjF+~(tRM9bk`pWCC zx!Q^mFB)mIQ7^Xs5-2VU!(%bX7K<9PNFk2|i9{s@nqmqsu3|1DKKwAPGt&UHvOv-j z9IZeBNkfSsh#qn%fGCX=aY;4LM6yjZ-GuW^Ie!bUq6_PcGtW8q)U!`MvyucQ9A_j{ z#ux|9i~l>hJgJMWA1(URPY@%eR8qyJP*X`OU=yiJ)Lu()%*+rRNFXas9j!G~Q~PqX zLM*-PxJ(1PEhQ=hx(e1xch$95UdJ2FKD6lZbxgq z>#k55EtG^hn|)Keiu$%-`^YK=>8{}k3-<=7NA zU~&f*SlpQGd?^n>jN6xBhYNl7=KY zy8r48Q3A~3xfZQ!P)2o>@oPod>N-2hB&0fRo2+KrO+)m9J3qPii<|Db<73FbkXB0Q zA)QAR)unp{+H?(F;$>c#q5%$(0FdiWqVOF93|B${TcS5QEJ$A{jLzXDGl+92FRG9%as1=0u@Bce& z`Vy&Zrx1FzZ+C(jTvZ+is|tBALKuYM24xsS8qUy$HN*)+swXY3MJs}0<00967&0LK zuu(?zVT@LILnh)-TrO0fhywJWE2%0W2T|2aeAki`$^j9UQx@!m#t@A|A1hL+oL-?x@E+^3YLiB$pcn3B@ROq*Ytu9f5Afo~opfek(cL zAk4HYlgw(2EkT4umI%l}c9N5y6d0$52TD>J$yuM1paUaF!Bv)Rg058ME2C9PUwKlE zgtQQ5(6zXmd1_q<8cn@Y$dWA%?tb_~i62JEr%INwe|bnAEo~`HYEDx{vj6JQCp^)b zO>(%A&HoYkG{fh?BiOl3M*89r}zsGV!XCCt*4#Ztu&e#m5G3hReYh8*sGDJ<6F z0zd_VG7g&2d}l(xsZfTNB>=*39HbUHQRgq$Rq7XIGLv8n2Z!&H}%v9i>5DeB?YQUgE~~22;>te(dJS8w@jfn zRWa8Zr^h5{L5{*womOocQMKw+sH8NP5=C2;N{ApZ&D15oqf82+$y7NB^ks+;#9Fz! zRl3gAuImYaQdQ!Kr1G_7bp>os_~)^UKCq)yy{b8{$`H2R)vD9CC%~Pzg zXD|t;lF4osB^G$Z6kO)W`svh2COOU}zeBV`1fUPPd~9qXD_fHym9K#@Ep2BT07vx2 zdp+uyM-A&r#DX-je5piZXB#fv{*Q2XgRb4ATQ}-PcT)iCAZHX}PkQ06aAy6T%N}8Y zH5e|m=<{r6l?y9eRfV9=q{?x?xZLK_*S^rBO;TmG-usf&qQ2cra6k4@tP*#li3Q(C z>pPwPO0K7rt&PdtdPP*7&$H=G;drfpRx6;_exUU*N^*Nzo({Ass$B_B=eytrx0uD4 zlT2Te3Y!tXIH=SkosTLuHski#z&ysPi-kNM`rFPwyUSU>Q%HF;3;5J;ieabgtCV8!73?X8Z@HfwUzI3KdNf{Y$)6$rp zW&%rBsE}5b&IJY;jX-@;O{dMz+R@uc?35r9;?tNZd4vWO(a$Hd=aE*vsTuj~+DHrJ zRSan@@@l+lX1BW8g?UM*rR`TyI=jAbY~UZW=A9ky_>V91%d^cQZeB_D)sw+g)c%98 zLHE<$gR{V{ak^P2Ly{pN_BGXFA{ES(m?1osE4i&LaQ}k)W=YE)_%~fkJw6Jt;dGQ% zMt>-AB2s)&8BKV>1$b`SPzlRPs)&IWGTMAI^1S~1uDfY!*7LfXZBcUVr!zitjniB= z7t$t;Yd%dz>}Y{>>?0C?Zt-h9kXn0L@y=}?ImvG73QmW50G=-Os88MMRmVEjwSM)j zXZ`9^kONR8S}H}`^n6yn@X8T=;SSF`06$%N+?76eB40?&3Z}aojb2<-1Np#W^ICE7 zE_cFj4iHn|dg8sl_^vlz@s3~olYU~En5KlhXh$pk^vOsKgx2jVcXZwhKYGy{@H>mF zcIjzZF>-e2+qKEq#d^fCjm>@&Nhf_%RNtaKnE#vOfw_Vo@__L0+Irr@oUFab3saN; zLgu^AJ@ln-tUgl);nQzj2W3y#m2kWDJn}a8X)}22v-5x|bmQ+OV+AkbekNEr=05M| zPy1QfUQe#~q6hc<==Z<;|2Gykv#n0cE7rph|7)sC=$OEWJplu*+Uqvgi$ES3KK=v1 z>7kA56Q9|ruIaL_5A;9}`~wRNfGkKk&HFsVnXFusH_sXz(P};fyg(VGL8)PgX_L09 zaK9R)rA6_JZPT{dn+)%BK#l2=VqmtveAzPr|0o(Kc^0K>ylkz}h3fGpvct5r`&~3=NDNC=?6}+=G^x zsqhN6Lu)ztb2~+&h%iJ%IZVXnAeo%YrZ!AMMI4~&%M>E4JyBZ3k6Rsci9`(4!3y1>C3G{2VwMugcu<6#7gtihM-pN+&Jk9?t&p*NNAyh6ku^Ps4l8lr58Z1lrb+%WVaxoT9&Q|z?>2)L~5 z%D9}$JU~5%RI`+TJGpcnY3ZKfI<>##j+=x_O*BU+VoB)%60uyWr04_M`NKs!zE#Yp z4RbOobHQHJ%fZY{?AuCE+|2Tci9BGGY|$Y-vZJ8eqlgpC&#V+&^2v!&OkCVYMoLNM zGc+jMwfdo-*VN43WK9{IjsHNYNn!g<%?S@iK$M5GBceOH9k~Q_Nk|)vC)0~f#^RtR z3xM$|yL#)Vkvt}ua=zm9&GF2|5wT0g0!#5s7KWI}IXWVYf~p;ng!ClOW1&Q#;Z1N! z%-D=de%q(-ia*@cBH>Zb`YceTgDu=JP+D3@_AI*igg8vJtHn$a>5L~Ybe{WswX86X zn)*Cxv=_BBy8!5q9X!wlJyB4bik^zY6y3x+$;ffC7I6X@2~{YUILtPqtXiSZfa%QN zqfd+QI7nK9%UcN$JDkHYQ50oT7nQ#JY*PIC(Uw3$!mKSP43VSpGblwCR>I0~K@9D1Fm7g;QObQnySOhN@HIB(p8uIHG(luE>bv7|BJ` zO_gIb#<~hQRn$3M)J7GK*{F!wSVp2cn=JK82rD2B^(wNM4X1RZw5vRZ7{NVAR7N#b zM@7|B4Zt_MvGGHamC;n=;nIlFL#%X){ZvzH^sFUK)m~LqUj^2L(++ojF!rV{;aw|WJjqs=>fiMI>0EFYf40ny!gPqreMc6j0*Od5H z8){g0G(Ch7)BlPEQ@6}f;BXp+<=BPo*pHrp?-|-9(^e5tR|zU}U54h|!lRglgs5wAI?RRokt)laj@tur(K<+#ss` zBDTfbw$0nUjYYY=+B4!fGO*Pe+RsdpgVy3zzExbkW!%Mmu^+kDq>)=S*@CM4(DmF83ERXa4zLvnEikg%fZGgGR+sPs=R*X}9o^b>4$SB-s`y*m z<=usa-2eJA4%OvXg~Joc@PYt1w%%1<=Li5+$X&r0+~t*CzI|N^9a)as-OS0+JQ!G% zkb^u(*{PV`@x3{fcwQY8-Sj12w4e<(nAFGJQY-bONp(~8wcovTgzg%q!SRf?Oo>g1 z-uo5c_05Tst%?eK)vy%Tp=#X$c3=-_iSrE;QzXy_wqOFji3E+W0B$IY5MIjS;0qSv z--M$PO$iBBjR?Wu5oY0!FkqG_)!j0(243MBrl>hEI~3;MmGIxAwc#IDRSjkce&fBg zDO(lHeW25YuhpDYg^i$XP1B3@M~xKr*toT;eZwGaOD~zIh20{zxpw z+W#p@9j=vMH__4~4oWnZ9rI>;Uu18ZGDso*S2g`zy=yOb+b6xyrKvF0|m=zE^ru?*>!7HJ#$(LCgls(X)QU)M5dACVYAvSH&6)$jA>$~9DIf~sfO#_+sa0xHeU63PdSG zQ$wW+qOLr>a_DQu>&lVR>c!E*R&1q9WQ{iKsw<=CvSa3!Kw#BYy)AU@%*Ls-{6kRX?Ekos=lP`U zP$OH(*e}`+?ge`ZHFa!rwqxrx?wS(}-;t@z-n{L6Y<^VX$NlC;?rIK);$VG@IDR`ttR7PdR|Bf@YlX@g0sqh#Ljn9r1V3zzX!MQPV2=Nt~zL4eK~heTX=nI=<{+{C) zXL6)I`ZvO$l>6t>|N}yXR zhuJ}ea8@+F2;binkEwRv@(`wFGUxMyjVXH3aIBVWetO`t6ws8}NkUv0lOz&2;eDGZLa<$uSNoRF+4r@}+bXY%8 zG>7d^r|V_zE@6&!MU3z51Z)#8X|tB_B0uOeXDlU`^<7VP6;1OSFYq+ia7$PA=F;m# zW7Ctqc2#F}Vt4hG@QNR5_nNw(t4E4{E}${KY4I zg9LQDe|FSQ`m93s(k~^acXvYLYetX!eq`{r?{ZM3eE%vQ{oe0Ci}>m>zUoi~_}3nFW^ZEf1v>FP&*X~JPA!+dSOZfqyXbq0L5 zSMkl_=^4lV_bWnk0L{o9BHy7%9AQnvRvu1CCryHW73?d(Pqt@ICtvo$@8br zpg@NTElTvL(WFS1Ds9U2sne)Xr%J6#^{UmZ7AX#7*!3$|NL~ku{aUu-9;|A$vR&)8 zE!?+q!~et#2QMs?wk~A5kta*8?9(!4%9=BC z-t4(E=+B}8i$MZ8q>x1td8Cm^BDtiJ zyHV7KA2}+QBZ)sY$t0FnYKb2obL{w^f&a$o2-aW&*(h9@dM&h|f&9|9rL@3eCG5oK4k<$LhH38!`iu2Y8jm%{^|OD?zq3cJP}@hZGA z!X0z`vBx2UJn~aN27u~KB&#ek%l}UC;gzkc9_TBB$&Cr1x(HrsCUq_QyfV-~3mr7k zLmOQ*(nli;BZ3hJD5kCe;u>zcBSOk9m>MgcG}l{u-8I->iyb!EW1Fp0AAu+)ZkaR+ zR^Zh(>q<46{^_iB*?FJ6x88m8{kPwN13oyB4p!`Tau9>d*ty^~$2dcO#3wlAY7u^U z<(Xr?x#pd7em1aAx6K^Imd4F)!=+0%_2-_u-a72B%N{%Jv)i7pfH8`m`*ix2+z<&%HD`R3y| zuJ*hWCOr3)PQTb3lcyg(`v3XgzrX(d^Z$R@9wR-cRquLzQOxdg_AABsL0SNOp#LHW z!3j#Rf)u=9hN#3gyE%$iUV$I`wl_QslF))CEFlU{sKOMoP$?AY&y2X&E7UDcQrf%P z%?h@{9k#HCJp7>#g9yYST1{roL6-=3#KB$_C_X}b;t-)I#VAs7idIZm0OSBQr%4ZM zhr^x-foG8KU9pT+Jfj)Yh{iRlkwbg{*uyx8F4d9AZjCaR{n)6-Htw;HeEg#y1Bs#d zkw``d>|p%n2eT4+L6CX_BqSXf$w^AGl0pidxk@xg8pcp(aGRJwu)qm2S`uWGJY^|S zsmfHc(n&DdhWGTyn^emV` z`z170X2fYyvzpetCN{IF&24hCo8J5;IKwH+DNb{$)1AJv8GEo& zNbJeep%S&IMm4HWfjTd+cmfrkAQ2-rfvTRk&4AXMTl*Y_M6}Gbt2O;4V~iXfAMl}>&2>d4$G*ufHZtuHAoViT*_ zoZ1VVsBlxXViH*_r0%L(jZ_gS6;Qy2?601k)nY>{+R-BPu%taLYE%2orK*#pIB^gy zc~!2_o#}mRFoaS$+Z9Q$!Vx1K6km1wSwO0`xW+v$iA;;!d95RvX z%4-%85!@?kbGyR*?kbxr-tj6{x#T@Bdez#kc;?n50;KM2;hV{^Om&w6d8#!(F%Up- zb-3UKYqFNhI^ z;1QG9q5lOgF^W^nP6eNJC+{^#ZGp8ey`+Vrs3Z}71CZk#55yCx_=JxI;s~kUDa1z3 zlZun9Fr>;uPo+0I@rsi>1tWY1*OVlMSG5uNA&q{2{x)v8|1EIB zrkkVr_V1aQYT;~ZyDPi)(7q#Ho`F+bv;nU;#>-mpjXoUNy&iLL;@#tSe_SA`D7M5? z?z)Wglou?w=EYwwbDn}qrw8ZvN2rNyZf;oHNm4n`t26U##-Ib<5J=IFKF&%6zzjoQ zRm`6*^=X6F<~VQ4G~umid4oLUB$D>cgD!S&O`V$GU;zi{5rDI&eVpH51Wu8G;Ha0q z?hLKD%_V+cQ={6~-{gBmI)3%Bx6bY|Q@TJ9Pfn(b!SGc+iQ^-m;8zLtrj;=E;Qy)m z6>V$1$6N=zqEW41J!hWq=aYOx;qG*fHiOwPK|SnE{3@dA{VSwi_&YBgcHfx3_nL{l zLYZy?fpj1Myiokm=?;9N%DvK?syt%_AL3PYe($2!xv&Sq;L>wnh|AX|9JC2{)GPn{ zI+f@^ny_{_KYyr1{&u!;uTCBhufN^De+tcS&E{*9@d5#d;gz5Mou8y}%1|)^=CK-y zRG*H09`@N@a7Z70_#Op927-m(O|@P^sots}UsrU%D0twq31E6@URxDkUk6<0jqAiN!OO_B#P9!ltB|N#oq?L-a6%|v$BrNh>EBf66x?-XM9*cB_ zNh0Mz27oP6*-0kkSIE^x9+^~1WK_mwJE7k&&K@eVnzndlPm&qu1teHTp~7)k4vNf3 zp5;Jvqf)Y^J*H&+*mgGW@X?`H7icS-Jo~d&-=b^F^qr%>srmCgM=|*V+iaMH5iKlt;sg??mZ@2msXc1usd^u)Hfkp>YdRjGcG}oQK59=^YPCM0aLQqqI-$1_O(qJe zgSMtwqA6<@YqGMbv)0|Jrt6%2iatJ6Y_8JFv|NtKF?D!qyqAOoTL^mBDg^uMsFDcBP^H8omOjnQ>>WEvaJ)tg)(Sxh84E zF5$vXox+~1la0zMYLusrV1O)TOM@!dkJw z5k_&W4mv`&<*8R@DVLULu=VK-B4?TgYgsnuS&pnqcI&xLQPIvE(T44%HA{j?93NCd zXg(s4V(fvg3V+2blI1Ky4s1!br>TBzC6O&|E-K&VXR^GjJV}D28mx4k*~Q|jwSwig zdY*a|Zb(u_C1~M;BBr3K$6CTDIPxga_W$AEVyEAJ?uDAIMQvZoZGtqmtt%#Ow%%gZ zTEfxq5`sZZqSM@;5IAo3Y@G6Zaeit9=xmq_Cd8Qt~F+9!#!2wZYo$f zf_Bl1)#@x;08l`$zt%17BBbVu0s_jd-qz0VhE^+x0UBDJ??&GEmT!$3i}0$GBT$&` z`bi*^nm-y5t&V8xx^B@#&&ML>^=gySey<847BASPz}!^n7=C%Li&kAt*cvo)bGT zi@g&Gv)SPm>BR(@_8&v|V>hM>F<$3nxCd4ydesJwl@(8~%cg68H zZ__t)0Z{=UM^$k_%M3h9GgyECSadKbkAOv`vP3d;H$ik;xq(H;Q@v2DH_7NTuSCCW zf+nyo7=x#X`ka=K1`@g_Gc%?@qw8RmuO?fXMt_A0Pct`hb64!NLbbx`H55jdRZ*vx zP!B{;Ba}816;XpU$Gl7zr$RArAwOHjCKP93^3X(FB|c-=4)^A@RdSP*hCcrDuq1Iw ze;4moGd3$03bTMhXIeO`Y!5Va3j>YU^)h;nP>aAE1=%l4dvQ2sNojzZgNg8RV&Ns& zMoka2g;sMbH-S7h)MW>UWe5KURR%ySm>@SjwLoz6Mc*}9!9h|Z6dY7SQ!f;0gA-+^ zUuO5M$BghhqYAFEb%byeGUv~zjdi~IW~%84Zv@A{;@K9rwUh1?2+%Rb-2q#%fJC3x zQ7c;yfc7{+cU#eb`i0gZgHu)`%{vn_Z{~A&qsDJgM74+lDzG<2l5}M6?XpUgH^i_- z)pu8T132tAb({8BdDM8>w>QzZfOFJ0)b(QZp%zkZH|YY5?9X^pUvL-pdEbjjbH$e` zcNc~)7FQI9k1nAJ1cQsy|456SHe$h4IKHr8asy5^L9T{V#fSS_b8kfg;e2@C8mHJq{wpuy2Misb#rMg2=;i_|DA>7@ovlToJglMDJ zuwxT0I}|&w%CYBE6<0ZUW4by$yICQ)MkV`KQ2S69mA13fvO^a+UX&e3m#53QZozqr znfqKRmR>vAw_pFJwAIJ~~GP8B?^F!*0MbWZ-QRtun>|N;w9zkA-~WA4vHV)4c5z84PW`akhr33t zfwZ3MzW&)6)|4an33Z~j~zjNTo{t1$dV^brbM|? zWy^$l0>qR#lV(kuH*w~?xrb#>mp^?51v*q{(V;q#CRMr=X^{Zlm`0U4m1aACp* z=h(e!nDODpj~x$IY_>({jc*B;#Ua8x|`Y!}a+ z{CM-_(Vr&?Q##M|@72SXUmC=isBU6l55Q=Y7@Z*e!wSHw+&DqM1Hp<8!JG=TE;*|n zTu{Q};;T?Tme?Dvk3GV>&_fGB{7}RYN&M&!>q<;<#1$0;&A_RgvJ5O{zJV}+N|^tf zP{$oj^N2^OY80)(A497oNFu#rQArgmI&Q-bnOsuJD5;#%%C@AF1Cc7ZywbxRdkEl2 z9=izuga9@X5Y4H;Suh3@SO}mQB$sND$2OH}rI|Bh8bwArXKDq|s02-7P@1|-w7j{R z+hapTA@wrSNhL+EkLx(3RMSfJ+A+q=!W?r;(j5-9-?N=iT{S7$Xfd&5tt~!DW zCRpKye!Nzz7D7I+?=B&-QrjG*%87CT?DtFjj#OlvI`&4^(R|*ST8xE`>Sxiu>A2#gb5eS^oN@klH-w{x8eFTZR*BJr z2g2Ix`HI#o&alZwis-V@P8+kY(S7>Stl5@1Zme&f8*VGr&Rg%x&feSazq7kL?x*c8 zd`ZJ;9#-(hbq1U)pPhDG-oEK%SMtjs{UT z*JTQQbJ9we-S*RQw@~HVd3Ou;-(9DCUEqlqtM^Syqx$&VnO|NuOgsN~zWLXRuO4vW ztsP=KZ`(aYZh5eUKA94vzWGvEcMML-O0P)8M%RJTA=3FS%9gC!(k z=lXX-6J~^iEo7AjT^Pfo+!;ZlUd;~&=;M?pg8je|5~<^V~sC{E^ndQAUh9v>MsIZ!B)COqUN z>sUxkZt_-@Ocxl@(nwD_l9bx&Lm!hT#!-5bldTM;CS4gzXO&Vri#*FEXt_#=+Ql2k1V%J~5rA)k zGib_$FJOt8aNe3#3ZIIC1qFH$r+fS^`7wqgH4$6j}Z)D2=piuPA*Cl zF!;188yzY*``HPkl5>M2WnTz(69Q>THL6q9Dpgy^*5_5_d}WoaSur~vCSG=??fXVe zY4QzT{G$K?000R7(aoFWH7Tr}ia#~#SB(Orrqlr_SC`sCJmnN8NQenvv*HbK+%+cH z2mn@Pa@DK`V5EO)}gApq6{4!UpzBVLw=OW85A-FvnT%}a9r!NFzI4nkWKSy|b@Q$`AFi5n9zL?gk>g{*NOd|bv#6|0c7>Qs+=XE#?>ZzfKUxhxxE zLHl{Kc8Ttw=O{Tc=tRpmN%1#iTv`B38UP1q#VWuIT272=#Wg}KOjx|-0Bm8@Np^Hi zr~rmC0Am#~F#@Rv;?ddyqrl~@$)age(-;5GL^pZR0vXbrw=NS82~==$00yiZBM{lu z4$T9wy&(r%2LK7u!u3q%qJwFRIsjytq-$dm0x!fw1b3!tH=fGeJWI8>>$XXPmCNp| z!r7|qj`#KuP3U}&NZ*i&$wNcgZ__!hOx_@Jkf_iN))qLlGClyMDLnu^(Bm04N%+xZ zL=SL;IuG=4ayPyaY?)kK$+wkLG}uE}{`#{St)@xA4_@9N+hi5pQ26z7dqH5cwt}~-^K)}FJ~G~LLdTW7`IQ@s575qg7tJ~UBWbJE>w@pXRWr| zvD0{}*{3nNeG8oJKxsQ;UV5vyd+z^ou(~Tf4la{!>>3!}NLr+2GU@cXK~6o73EqF( zXqyau={C80-V6Ut#NIX!s{Lp;IJ)1S>V4NCM{GCdxwm2e?&d1ifq!=$lWHG6)Itxl zOc(_@W~(?4GtUVU;GEnG!+YNI9(zqH9Q>{~R?bhQch+N)cI3Ai?aEKp+@Zhrk3`=v z4owVL*u!1wudMDk0mrs~3tACy!taotF--(-sCU`q`(@Ti<+}me#_xD0?Vrgw{cG}& z@}j7-*ZlmCq(<6Au?FyRMsGBl>N!T`8zunRq(aN+YyRjV_Qb^27%wKkt=(v1?CPz- z@C>g6LFWs~X?dHMk=w$n3Q2H2R2K}NB!b1+$U@Lw}m28lX zE@w`RAn4{#PR69XIAH=pU=@<4<4Wr$0H8R^OIB=8CK8Y)jKi*Sq6%qZqb4t;YR@th z%cuf?3i2Qi5bFYCLJ0i<{OV6yGO%b45O!+B<3vCWXF}v;!VvgC2POay*^uclkS2b{ z)No>|pdtAl%g)@)&E}1Ip9HZk2?uoETZ2TgJ9 zPBAa|=u&!cF|>~cg9vkiBoJf;*sf*o_$diRpc}9O4E_PVpzyT*p&7oR>1u*q>ZkH# z;v0OcqWYi-bz)A!>Dd4F38|v28LBb!HZK4IK@SH}Oax*s;&6a+PJq~>Oyo-k7R&Eq zq8sIJCM*L_)G$c)fVdWJP7==XqOKW!1}3IT!Y0fFp{l}=ixg!-1OouwCb6nqFzeDz z6azpL)$J2u5cw){LQ)YHsV^g2CiKk42Q|`*Bxl)T0_5(h*c1=gYAPDAK@KK>3b4Tm zae~rN>luzl@pNF(@Fd}wY~qCwe+%s2u8CvYtSqY~u8%?PRhvA&Wi*hBm#EZrJQ!RYL<4l?Y*4laey z5{nD#QZN*qPZR$oavCU-1s{?V!Ok5@F(f$>G2Kovsl>yKaSZ@24+_D<81s(+r_p4^ zy<&n7d@S6g1skG41SWtNeBu0T0^)|T@L&%rl1VcSA`kQs;8b%nk475FFaR1b)>I4} zz6luoE6f_r8^=s0^1v$d1uIpj8T{`XRj;XZ!^(6(Mle$*jt!}3A_QP+&JqmWau50J47u>K6SvO!)NMW4O%u~DJ{1fUjT1By^FGZAKl|c*dZ>H=tum#@ zaXRLvWLk%SyHh3r^GRpI?4IfcQ!q>6(@Q^*UjVdB^>ZT5 zG)=R~fUM5yM(YV@A_9^C4k}?2HsK$5K~S!6CZ4XyH0~<$i&4rzCV;`#I12y%&nBpF z9J#C<@#z~VFf6W4Cd{qPpj1j749-l@!GLcfzwRPaP^unMRF#hfFOf{uG*v4}RarHX zE~rgM3m9Xf0Q6v|e4!LZVH6zU4nj>Sh!iUTD#a$i2x?BnHma!ZD=Db*b!60#7?moV zbT0n~GP&q%x$v?PkqO33C&bPX!~AxnQ)!%rsq7wN=^GGG8@8=>bj? zU>cqQ4(`AdTEQ1uff=SkM4ti=siL2bX}C-j7{I}=$Y~t=6n2`GY@U^yz~V+1%-!;G z`0|WO!R|oI?ghJaCZekOC{+akGh0)zW9LC4HxX6Ub!GLlU0pVfFv#jk;U6L(CJq1^ zS^*B))apb7w!8sR$LUa|47XT_q@2n-?NZ&Um1OCX_^x$JyHp}Y_3ZqTFWJ)tmCrC= z5MbTtWxZC1zBX)WXoIk!6^Nk}24Dcp_6~NIXL}Z~JSu353brE4QH}O2C{fSMt@r;% zuq}-XtJss>K(%DYRa8SYR8Kal4D%pCF>rT>Wg(X%#ddNfh=Z_!5txA<{s9@-wrw#M zH1Z2*#p!1g^}p=)Zp*?O^i1C5EmL=|J@*W2IaWPEF(UmzBF`>m4VN&nmfh5iETM;T zg*SYLw|J+gg^bG)^q_M+w=@J6MEgz~m8xh_7c`>E68RQWkqg6Q7Y1MOYDw|z1hZp5 zmUctcFpunUjdy(+hkf0b?p%l&67f2EGj2mFpiZ}X6+=^dZ_a!dDH7LqldpE+b6dGp zBBgdPUr=||H*y`=2H!V=??!*i0-27eVJSE({J|axmt$quaL*1CM>RhGGI9UmQxQF} zu}T#%xwcUr_=O=DhGlsBGMIufxQ4r;8@ktn*RyuLcU;d-aA(4U2lG9#bRHHqg(P^1 zS%!(77zlCrZf`h>%|d(+5F137Sm4;0q^3BKyCR6iH*mewi-DMqx9WR~xQhk1 zg(-=TDVb9$`I0Mzknw4dH90H3)e}2-FEbXB!8rMlkCDIFafA1dSy{s{`IYgelL=~# zWqC5fmw_?y1+$oAPuX|P?k`8xcm3g9GYOWBncrGDnZ>J?n`wk1f|>t^!-_faTVYUY zPuL-KS#du$`BZpJ4YF>IIh>VwoY@zehl!J!B&51Xh@q^~>s zbKK*694G*`%cU!+LvE;^N_(?qJGN;%g0`ZhaeJuUTBK+DJff?yn+RQdyS9mYxQ#no zv_iLYd$(ifxYZPqNZYxKd%C5&x^3nvmOHy4ArD~Nx^2+4840t!o4Uz+yv-XnK3k}n zd%4fLyxqI{F6PkAd%oYhzUf;iri(15D7x*tzd<6s1Bbuw+rOJ#&Nv1c|tZ}I>%)Yyk|nMrCJoGddNKi6GXui=;Fweyv1?a$8S8! zq1>-k+oW?Fx8u5`lVZwq8o*mTCZsw5^18XDyv(`$%*|XRWFoY8FeZvxq;DF+(;Sv( z+{1<3&cD2C=o-!We9rxxZrFz&o|a^ri|Q~oeGz&lR)G@&DbbrFiT{@-l<%(8xjGiT z&oTYaHQgs1{g9aEl(M?hv190JwH-6+zH{_)p<;mFO3k%^{o>@OW=8Hn*%lzd}UWja7fN0){cRtE<-nm@<=P{?} zmzd~HT<8NR;*nl{WWMQp!ssP<>ETD|rG8nUUc;~ceXE{*sGfM9ftX9m>BasevOdGN zUg3lOiaMZ{%YJ>^p2E*Qd%8aE4`}S|{=fh4KJQDO?in`i`QCl>KJa%*@CiR5{+@W| z{&Loy7l<8o-@bSspTJ3qR%$r$(`N0#eq@X`@`ac4``eKCRVk1mkS*VFdbN{1e{xg5 zz7HRGN}p^pKZ;jBY-iuSTmN@nAA@55;A`Jyd!M{-zwh^+ig%xge&6>CKlzV+mJ^2`=y=zYZ&~YAO1aC{;iw-0V06FfdmU0Jcux%!i5YQI(%4gPXLJ&Ct9osF{8$f z96Nga2vXy>Zy`&XJc%;p#)>R2x@`Xm^QFv~G-ukZiSwq;oIH2>>G2+FFVK!zA`LX24lqXxRjQO(SzMMOI{tPfywT8y{Zxxbo%9o5Kwb{kQb! z)Tdjo-WB`x?b^F@|L(oF^YP@%n{Sx>y!!R*m4^op|GoV9^yfphUq3(o{rdZRZx3LA z0)Cd>fCe6jU|akx=wE{jKIs1-Vh~OkA%zxJh+%;VZpdMW8ENQYh$4=dVTl)>Xkv;F zqR66(F184xZzRr0V}c;oh+~fB#mM7}KK2MCU_lP~W06E2X_SsiF4^3TOg;&vXh=?J zWR+ANd8L(FX31rWQGN*~fuw~=W|??(iRPASrYRqrYrg4boHCk8XPs<0X=k2#jwWZH zaQ^9MV}Sxn=%9v7+ziT4|=3Zt4=Go_dPsr=rryX{nl? zYN)BIqROh9qrR$VsIboZua(WBFn6@&hl4m zv`P*uZM8N&i*2*oa<~6&x7mIhoVDVPNbR`hvgd8O;I8|cy6&E@rEGJ~Z^_K_%#!vDw9mNeOf+XW7mc)EK`$*d znoK|4G@(gPZP(FMU(ILKP;YG|*IsK~CDy!r(~Lu{Kzg=nR%2~)+9rJsH`px3O}E@7 zju#C!Y{RS!J!Z^*!VGm z7zV6DJdO9wwE9p zoZvVI>>vWE;=vGp&?)WNU;v_#Am$zCdd7L4dt9d{3&zC^4549M`iB=5MoEM`93fD8 z2t*(1ggwU#037sYy9WvjhIxvf5}m`fflxspTcAP}1F(fWkRbqk;6uoS7(}Lo5sYM% zms$X57ZCrPupn?41-_c##$rYAiKc-gnV4pfEl9D7dYmF31(Js{3KBPCEF>A*Lj!@x zPaqVe6u#iFp-$#ArK*Br88X#&Cso49=;)r;|zAmr?-Ilt=Fu$uEABsFLNfVw$(CZrO{ zbfr??`Z4M$lAgeDg&v+EIf>3vky*XgKKuGWiWNkxgaxZ%#d;8e4z;Y2ief@QfeJ^! zb+RgjmwNU(Q-Q2Pewf7}Uz;HaQ|vv0XYys4Z9*=+B!#6B$uV?`QN ze~NOy*%ioj_uJp>%9gqTuJM2aHXsy|1fvRJq40Rj5g;$;Z$ieUh3$07;#^O`YSBZH z4dUYfJXs{*j2ab1jAarpmmtxVuUYc|v^~HW#-Iqme`VX>fuMpB_4VP6Z9GCS(UeTz zz+k>Ydm0CiXcOPWahCtA<-r~(Nop$C!&FY@dXEY7c^%e-GRLpIWV*xLV_FYR5` zP@vH!(QSPirk_pVFeN#?slhW_^n8#^B5m4=+7~iUqsKxITD1rj zt(gP8NKj94Rczek%d^QLOiqO1`sZlw0yj-iBgKZIU`z~%!NHa>kCw|#F z*SN;@NDHc1fuxqUF_TCDCS7cFWB1Jb)i@xU2}XH~{7-m^V*qYIj{umQL#;qzBMl;I zyu@G(2sf9tRYH;^>`&WJ8n|3ophoba?vgy`+ftP?@w1v0p(u5kK{Wra-vEqx(&}vU z$gd7>`0NF^s2M`dYsuse-MlSax0pA;QQm@-{UBa{l)Jvo@VGa;lw|#NSwB$;VXx2P zQtvoy2ZG&zkE4KKWWil zzo@LgN9@ag#0?oL&(ay=!TYz&`DJ(d(GP%r(t52R1_8G+?dSh~paL@A<{YUdP=h82 z!!}z|r)(V;dfhij|EGZ%l7K{mfE`kF9SA`SIDaMRa4Gf)numN_bzl0|VlQ@I#}|Mz zxPe_Ff;sbnH<&2ul6@waWmrULrX_JI7H0ynf5G>6uvLRJ$b>eig9rF;PdHj1!+<{6 zgCsU+1r-tICo|v^eC{R+o)7?FwuEKagp?tLDwBijK|EsMbDxxkA>l0oVTE-_SPyqv z0pNSp6$qC=ab^gJXJ|%`(qU}@0>XnA(}rPgaX#G^g=E4Aqu^P3wuqfkiBIG$-*$y| z$Xr~ed&g&K$g~HFCWxgNh(R(dVYeP{s8x?xC#pCQtjPZs|C121m^pI@U*myLk`RHW zC^x6*i-!`2Y@tT=Mr%GXD5QM<1SdBn(j@GDt;HWTb=t#;aE#eq|&*+Tfgb;$Dj`^65X4ruSSP=d85i6G` z?np=e7>{v5PL`;c`yfF zum)Bj5GRQR0U!r`z=aa&8WAazm=TAtC=kLlE*MEH)W0uI6g+UpV1=0VD?;!_DnUY|s5Gxp!5>l2kiHcS^ zh3KW0_2QG`(M3(E9V=-NMv0UOL6&8RmS-6@bmR`*u#w!@mbCJg81o(np_F!M1t+-% z2$7e3xtEcdI8QVU0pJdS$(DnelUP|Ey?2-g!I+B~0HP_D2Jw=537M(6Dgq$~fRG2C z$xoFz4Ls?Af~lFWB8~v{lOIWsM5T3gDVCxcmW_E3uqm0xshZL;5LGY`S)fOJ_f3_V zPgQxF#)6Pq`H{xC9YskH!WotYL7I#?5T}`($Z4LDflPFn1p!b6>&cv1kOzwwoq*{` z)LEV6f{?Yxn^q`;)e%TWxtJ)~m;pMPN9q5a;#rzRc%BPdo{$j$?kN!MNf6ArM*MV{ zeP)~Yses!;9$mSeh^d&wX`lk3l-@~S#3nOm!UB_dsGOQ|1l7D zIhu+&qT>0T+_|C!6r?Je8!zgjS>U2pw_os2nJ&8%ZgmIVzx8T9jET zq6F%jL^`Bl${@Nap^P^T^f^EF`J`3Gk;@^H)qxNPs-*~O5OeyaVmcLIs-kjG5M`QP zOBz^Cx~9j-p&hxOJ=mY4W1s`ds0GTSIvSo>dX$Mqih7Etmg*#aI;h*2nVV{K-lCh> zNspYT9ZA`pjryG-3YK>{q6&JcOM(BXuR4g$8KDnatDY)Hf{LqV0)NWkp>TSX+)0`} zI;R5asKaWMrx>f}c@Sm#o&o`*Sv3t8syS-9s{x`eQc4_DTBKgOqu+`UJPMuzI--wx zc*`oQhhY#f>a4eVt)GgF?+UNmqOGdok~nIjk*cpf8m!)Gmx%_5=z5$|N}=ouqtW_V zOlmRKDzEGDcE`c3vcZ)73aO16so*J`cZrndny!w~u^)RP;|QVb8Ldg$u6xf0uppa2wTiH1ilp_Vuqs<442!g^maTk{ zi>C3i(IF61aH>_iuXSprR)GJKO3ATT_p_3z5F@L0t;Yv8c``~{Gi6&d-Zqbiie-sf z9bXBe;|i{(`mFsiAp?1Vrg~unc7nDq6cX~O8i{KV zc>s&Li4fgzv^v5sE&Cb2x*dEl2Pw%AifI9Q8?#fJvlQ32tLwLdLJ)u}w(N-wX1hbT zgam5`7t(2&34#RRkh$!YvN*B_wu`$95dt6)0%_+Nv}+I~VFcZvipjf!LNkWDDXM>_ zO7Wqat171kY6Xu#q6@*L0L!{N(YobJ6>-U)whFD!I*%v)}5iiwd4y)V1iV!slBQeb5JdYO=MO2MYWp6ro^?1Q8+-0MJ0K2qBOH z;ST$|Gw_=c->|p=K*P~OzzZ`GH>?qz=DQ35xp!f}3_-aN!MsrFGvnyCTb4L`aF^kk zrQfRoYe1YPDznAvsx`>MEBq8LjE@GP1%RL<$Lqiralir4yVa9F&^Zt~8~{Qr5YW&N zhdU64><~dr31 ztH)6cz(c7Ks!0C^o*XFzk;Mu&0R~aZn0pX*Y{xXbc(+u_0xMRfmZNPvT0wS;q z-GD;U%UiZ|$q7vm_$%3Cn7|g-Y4ZC^2uu)Y3YhK;44{qDok}Biz1nHV2YRi?d_8@j z1P=57+c9n13C#)6%VVhg+rT}Z3f2vvtpm~U4ZmI4lI;!W{5Q015SiW7soV{b!_d_u zpV1IVYMUd*19q1oVJR zS4`2OebTbDd0}V70RWh{6bLjj%6DPkl8oQ_-Mjof5dO_ucl|dZpaYX*<4Igi4DQ?q z5#-+{xqwMz0f6C{oKVx}I7T4aEUwJAl)$%i+ywF20d1O}%%&}6G!631IXax{J?5cF zoF|Erl@_t{?dGXTBGp4L-!RS@vCsvf-wPp~`|IHDAOayE;1IFrZuHm$Q3_*g!9t!t zC^X#!0ZJlH&h!EX-w+KNjmj5H&gw)DQ7wvIE+8>J7jiCTbq*1CP7qQ3Hxr-(LN5Oh zag09&;pv$^5UQBy%}WsW&E)xA-3eXFwVuU1Cf>qpH=G+6QJWv%29}Yk)*n2q0jiXP zkiC5S=FB?VvMvxHz1lWR(f=E6Tu#J{EYIMs&k7-(i9HabP?;X_=hwbXD?GMfE zkGy#YK@us4&Q5A25QYrcx{CzU;6RtXVds7j`F!F_J?pkq%eG|qJRJa>@Zv@Q z4w745d2rzcam5IHOW*Ln34OcVeb>8|>3Q(+A_D4o5%mE4H-SL)H)+AEbxT=KHaBUx z(O}VXUip{L;tkRHT#fOS?+sP2<)Cfh-tgI~_%|Lc@XqTGxDLP)@ohRz5HybvX9vhR zAL}0FIqDl1_*iKZ(g#Cttm1j46idRPX`p(itW9tI9!w)4U<6uF%0(RjzdgU$Fl$Wa z$nIP{U|>q-9su!tnFfBm1U(R9?*eH0)dTUD^ZdsG;YRLG_OlKU6OjJ|AV@C{!2nc{ z@F5r?K(qh@n|<4ZkRU{YWWX5l_6?!803gA5q#}n5!EYb~fb(eaBN&$iRmP-Q)8JpFw#JC0f+z(S}B}I7C=6fRTX#=(z-G1RKVX7Rv;H*)nD{r%(%iYZicM zPB}HhMx{qFKpvGdEviJq(xun{-MoSnxtFHOZs3U0<777BS)_>-FV-p4F;K{lB}b+_ z*>Yvfmo;bRyxBA40DKrS#wmzTo<5&dcV@kMa!e}$)>yGJ+v1u4wPRbXo%_!o*R6#I zCqCSGapcF9CuhFgd2{H`rAMbe-FkKG*R^NozTJCw@ZYtQCSU*lC}NowkzkF9?OUIS z>_5^3)6HE&f=HkdoaPdm!~R4F;68*x(BeJ-2}B5qY3AvVp!@nm#;^b;&>@=91j~vh zBLY|@Ca-|%rWqXwvQWeH5b7o-x&-Tn3jFAiYeTrqv++i?jKgt99&18x!~Ep4$UXSh z`zwHEqC&_cxr{_ezzO64CMP4hk*i7hW<e|U0m<01` z0)jXZvPU{e3-3hMxZwUro{mehoGN9e!0-!Cw2@Rlxy&<<&t2qY5z^Z?f%ZC70|{v88XOq9@k5WXjf~snS!a zC2v+lF{O`IOm$wQKBMKk{2XgzX3PhbNhu=R&3h}@vLYJmI_D} z7pBUkXt0$RAuv%2ms>Hl?G_wwLn0Zc5GA%~T!K~!5nq^dGIeHBPYo>(Dz?aZXPtZI z`DdTEKq$4EMeUO}+YpuKH`fH(L7MmtM19*8c zm3aylT3!*7mI`Qno#sJlkA*O&R)9HiY`*(8&tCt(11G5LvlUBwM=cjC14fHs_}r0?nRn$m+;zu2=;5>Dz(o(ih_af88F%h=RP0aNr`}^U z-5xa`=|n#dzkBv=Z~y)E>J<+I4}JnZ)2p9 z3#?{Uyuf(_Tiy^1^$yr0`ehI@sq`_(t7LBVBN$V`7e2M?B_HkN9I_ zAKTa*g1{#L9TZJ!z~@Iq{!x*MWaJ_p*~m@pv5r8jlN=>^$(!5}lbhsZC!zO9P(m`4 zqZB38N=Zsoo>G;mw3#PesTjSya+8{rWi4%aON*>hm#o}nFTv(ZVDeI!!NeRcjd>Gy z4Dpyqq~$W5`Aj1ToRNCYwHoe(RJV6tha};M1n^{hDrc-+rIa*BA z2hMNC6Q1*wXFch8PkYu*o%`gc^3MO+Pk;vWUZTq+J`I}BgC-QA3sq=C%_vY$QWB#6 z^xi}*dQmz-LS_!-C__Dp#*T*cqa$r3MlUH*lS1yFDP?I(b+WWfl2oQ5rD-T@defQa z^oB0QBRP30xRw4?s4j)X8j5<<97#&2OT}qZnfg?xMir{b8!8f$xxlLOEvQ-LYD!`1 z5~WI2tWzCpSjl?UvZ7U(T`i(A*_x`YzE!RO)u>w1dRMjH6t8>DYhS}TR|2{fuowi& zW(IrMiGGzJau5Vx9qU)fK31}krEGT)tGxdB^s;q|>t;Q>$vH3zscQMtWKCOH)Sgzg zsZH%PKdU>ezScY+QjKe6dt3iZu7VRr9g103`&-r`61c++Ze&XvRi}s|6hO6NacAdS z=SH`iqBW{wi#kQ=W|z1L-L7`M+ueB*Efk;_Z+XdkUh}3Gz3WwPd(YdH=7RUT^BrrS z>RMMu1)#3bm2ZFP`(OVC7{CL*ljuajUIe>W!3kb(QwmF92n)EOd>BLvEmBw9NI1e9 z)^LY8{9zA=4I#^=mrp_rVia2wAsS_Ib-|iq7@t_iF{bg1k-I$`*I37p98G9pieD4& zSjaiP9Fd27WFu4REw<$>lAWAnc>*U8`n8B0s`+FrLs`pN=JJ+l7aHJPf=-Nhu$Q}h zX7-u*CuK%+o6XFfH@p8?&T$qo1_8z2Ip>+qwo{$z^!(>N6DQDsCNz-?g(Ex)=w5?rj zY+qa3+2;1Pz1?kae_Pz)Cil3_U2b%rTiv6^ZM$>JiPgAU-g?ySy6v5AeBV1c^X4}k zCFZ$D6SUt0Z>vWDuJ3%O7U2tDD8U_$#Aa~1HB5eZ#qk$$T{B$c!`yhsy=iff6VNjP z4|&Or=W&$tGUfj(N2SSKe)5XGT;_gbdCebbbDS%3<~!GF%z6Iv5+^+8svNq|p9*xO z&#SXZXSyAYUUW!8{pmSvdesr_bE{{4F;eHc{JQ@2@M&G_TgUp?&CV0Br@eJ*XZzRA z{`TIM9qx1Q$<*6U#=G0So^gWsm!3BS|82Y%^^ulnH)&+*4QUW1GW z{NgE}bje3Pm6_+fcP`I&%YXjoohSWtN`HFih<@&&SN+UU|9V1c9`;{u{n=Sxdza5% z_G;99?=gdWv)>;0A@_ap*=&5>4`1xUSANBjzx%Gm86jy|>>Lh4>*u$x`B`WF_6eSS z=zHDQ?F|3G6|O*#^Dhb^84tfhz2A7bzklc8pFNcGkcAW$WyiSt9L!96#~i4NG7p~ypoATu+N zt1bWJ!?z;Dy92~jF+Zo7K}y6FxXVHu@kGX`ga8l&NZiAa6FhMFH0V9Js5E?J22^e}Tdwe)^EXbe2$9SxwKO?{G$+NczNH9W(iOhn4jETk) zGK6fyUfjq&Tu6l+xps8NG2}9etOA0l0x&|!06@u!R7sb}gFpzu|MJL>>`0pwK#>33 z$tMc1edHVVJFT3^0+(z_oG`|;%1M4ZNTtN6kmSkMu(ZCJgZRR-p(sk2j0u(u4NV-d zrqoHC{K|WT%BPeJsVpFq9Ez7j2$*Eal>{R#2ucVOO9!k+x@4=eB+Jb3ueYckt0an| z3;?2}$dnY!m?+7by34T)OU1mbyu>-a1gfq~N~7e7l_boqWJ|$xNtB#Pr)td4VoK2D zr^nRGpoq-CIf%(J3b(Ax%QVc*qzTWQ%hBvXyR6NGG)>)np8%7C`~ylbBS@lnP2w!f z!6b;|Bub=|ONGKs$GlDFY^l<$IsfW|JlGAiq)CxF3ams<*Zj`bG|E3v&948MGMq@Y z_=(QL5>54lrs>?x$iz;q63*c?PVn^3*38c;ct`Dw#hlOtjcP2dY0tT0PXb-0_gp#D zq&babP1tP7wTw{vG|Gt_oTyQZ{uGo0rAEfw(4k6D(+n_@Tb{}Mu%dWPmnhEs#Lv2; znZ;mEI`mMkGEf<{B@jiqvmBe{#H++O&e%lGt@KeL<;*IG6l9DfgaFV~fP^J&QYL*; zCv{Ry5IlXV(Q<@ND+Q<-6+;E*NZAwe0?Ky z1xanG1!`D zOAxx6H6>M5J=KyWSw*c^Kq%3_;-?$XEN4>1lABnH4FG2Fp??2eI1TfhXcgESLD8F# z(Dh^4@6gyaHCdc!*qB06ekuY8QdXL!SUtemi>=z|E3|3F5yV8X07#4Qgiy^q)3p@a z!X#U;)yfLpN*K+KHtp76tqE?GQb;&bk?p4*n3KZ-#aB&MD+R`M%Gr2~L*M*BI^iq$ z@B*Ms$%@R(Yqf~R)y$Vn+iLw$gtdscB~?Vl+(R|e7~Q9rMJzOQi4n-2o6uWyirR?1 z1F8jFd-E`!g{!sQunE;l{FGd_v|Yu8+}z#W%4Eq5l@6Uy*qf-=^JLm%3f-7+gA&XM z5V*xufdjLVB-14i5ST{fxk56#x(;=iwkC6De!i0c3CIo5T<5c3>4AY2=f14HW5 z2n|x2_|w3gU)ZEy!=&HEb+DF23OLhT_L9+shPEuKeA2W8stkz1&~*Uv3?R&fTXTcw>+w zT?dZiAa5;zM>~X&z)jUQtx8;+hEn;#Jz1 zh*E;!=A;EHGSk)pwRW9XIp6RpyO-?CBWuwgHeQQ%+1=h<2=Wo{L zZ_UVYCg*YrX`}{e3U2C|F|#roA9&Mf;wtExzUte}>azXOw+!2OC0cFNSpW6bRiz1H zO@+&a>3LmhaZ+lw)*X}?dvHSypQeee0yw zWW?U1xQ4*Su8vjC%yxayf__k*wvGUh1pd`#v%U!>zLdpIY#&7_=tFf0;uTiw4ggd5Yys{kr=7(b4R7&Y*Yozm;Z|)ZK3jt(Vfaq&%?wD{?rXoM z38k&&ni$p>rc%(hsJrc<2u?@E9`GhQaBCy+@i>U~Ugi5_@0|8n&&KYe`AJ9~?3B%K z>h5MP*56*8sO|2XI(CU4H>U5tBmo!kZY*#l#|{(Ux5It!<#uo~ePXPJVLbt5oS1Hi z)@F-#Qvto#bedy?P~I{QfFV|lAjhRMC*lwfsPVn6@U5jKkGweV>6(D@=7#T7uHTy8 z(T!eVT9##?u5S(R@1|vy3;J-%D{>?M-`F{~yGAb#ufFpqm-N^SWG0qSF9;MMBsuz? z=p~Jb`yPY{r*k}VgAuq9H0JKUF?HP;atR*os=C2VkhfNt5Kc~VTz{QhU#b+PbUQcG z6zA%k2J1=@4OVDjq6YL*6-8qWC;~p9G)D?B$D2oloM^`*MOR8-FllDm^%i_|;H?t} z?{zO2^+xN1JI)CUzi~-0gvglN3|$?KAvVM^P`wyo>O!XD1xa)tXgaZOui8{o^=idc=`kP z;&90FaB;`Y0zox*;s9mb&T{?#;?|QL1;f^DfZ}#KdFOZ*MdHbcEJWa%h=DQS`4oip zvH9i8V&}0#`KN>W;HVRpU%#m716o*A3`f~0-S3>wZpi6qi27)rAo~SebEX?ZdjSOW^qLVWEZCuaJdJ(frzqSHx8uKzgujKRPAB7XDgqLnVJZCm4#wPOF@WPteGg(&7MDlPU+@E=$42}qh2g{^x{Z*IPl`W3#Lx)^jk{ip+^L8uH10rO4BuWzOH?{ zPv()igAZTyxbgDj&znz=e!cp*?BBPCFF$_YJ%5%0CO&^ZS@Hf~e$&N&0}cg3OO_ER zRdxw7*r00T$$?0dOj_ilmEjo(6fZko z>EMiAf|=D-VUo!il~ba5rkZK8nNyo@t{LZke9)N(QGLL7V%F_smh8EeD zpLXRDtg|lW%}Sw;N^5eMcG@eizxt{du)_vRY(tu|B*!0i;#n-Gy8f3dv^#wx?X)iy zdabtGmT9cB-h%tBxCDh;?zrb77eF8CqNJ|5@NpZjykTZLuf6vU_%6Ep>f3LL{sPRe zzyaTeufYfVS1-Z~Gwd0`14I0<#KJ~g@x&Kfyj#N?bG(_t9fKV5QW%q*amfImY_iHJ zvn;a9FEjNq%rn#MQp-25oHM~V^Q^Pa=GGjv(Adg4w9!WQob=C2|EjdpOhfJH(Notf zGu2oBBmA`1QFGno)?arW_J>%L4YJi`qs=YYYl{tH+ib)Aw%lWM zg8sScCAs8<76EvXk``5{q($bL2S9rOd5oC4@2dl^^y$OzSvv8@HyXV1zkAA(>nc?- zdjPMyu6yh&UGK#f0m#Js^51*^F!JO3XgvAnYZpHFz`Hbi?6be_eEiZspVIXz<=3_P z`>&re`u{h`d;lCEX!vJ9h_S?a)3cxJFlW2z(e8fPd*A)=cfbqwFN0DeAP2!Czz%}{ za4Q%bp}1b+zSv2ydhcr=>?l~hlmr508k`{sg%`pbrsjh<+@VuyNW-PUWQ8q+9R%N} zJLfI1h3;cX?|$aPC;pIEJe;D(9!JG1iZF^=#Mw@=7eCom!H7nL-TZV%J=qZve*=Lc z8@K32-C!|}ez9VWY$Zpey)lm%>xmaz7&|ip@_rALoggc?Mm-|3jf#ZL9UCbeIyy3v zM_D8#Np!i|;VySzMC0|&xWG=PF#s_vS0ziCNHs|^m6L>|DqHCjQ^Ha`eu&-}>qo{T z?gWsW+~p@X3B%3BGMJ}4pesw`4Qts=hjBClJ?!X8VM0@5RiK^pikQS&e$kfyFHDIU z3t2l{s$`TaizYdT*`B%u2Px4Q5E23aj$9#=YRWuV9-QeDBY=h;>T z4nE40n4Z;aO=T*16zaes4zi#@UE)v)O234z#FL)no>KSfSiTwwZ3J=uCN_q$PIpS{ z8~Ai*azZKxyB5Z+*DC9@qLI<8uB4tnVU9(Uw}-X%1RO?~fIfS2S(-sMw-#v^Kyeq? z-4dFX zRWgB}bmO#k)GlZ>XHV#YL9{6eL8@}fN_(-l8`9YkPFZr>@~(uuF8QxYoOxcGwWJIH zUWtBRB3)5(i3~)8Ni#v(5;9cyWz5|$vWR%SZesGAyc{kO3wzk$g7S8;69h2%#o-q> zHzF?60TlxLBqMA!C6iT&XaOe(wyp#TR1iT*-l-EDuLPfR$O9SwY8kxQ1{t9UbZv4k!&hho6-JQNi*IV0ujK~CD%8b+PN^00=w) zRE6QFZ2E67r$h(B{w__YjlbG*q8r`-hE}U}@lxA5xCLz!N%mFDxbaRl?O8EfW5{y8i*joT- z-AX)zoooW!%>yx%8{83vJDeQdty#S6!QK6YJ$c~nWLUnjmRHFal&w^k0SLnd09=LO zfZ1P>*%5Cc*b=%#cEJ#j@n5qA0BRut_N~PKW${5X>6gic7jyXEosrcAS|IBb(et?6 z1DYOjnHbC|AOj{48Yxlq#9oWN$sX$ABl4lhgp@P=TOd54TKSsh^cYKc6t?N$(>0u0 zIfo@~)@nhSC%V}vwqJQ6Q=(y)_w9~LNFPiPVd!bZ!sq}!c$5tE9f7IVzKPaKh1-sR zLzn^Bj(}I}MV>vOUxr~Aqh%PwWmcUoG1mwsI>$`VxK^4pGr`Hf=ONft_Va%Q9)gC0=*Sp0MwUDY*j5TOx=MAUb%!0 zz@q(eA2@gvOL7_bsf45HS)f52$xT3{bzDk(-b#Gg`ze}pq!cqb$4<6WPjX{7h?f4P zgo1IIbd+LN0S=Nu+@B#DP;36f*Eag!N=Tn8 zro>p8NuS%zLuCd)y3L|3Itju=Lo|59;uWYf+0ne&5p#&0R7D%9 zaTV^ZlZ8eDyX_i>I)_ww16-X{OI+5Dc*BU^n|bbK;4ML?%}Hk1BBRM?y5p>~#19=IcMQKyLsqH!4_Shik3#iLk0kXz;n zol@x|T4{dzhY`?VRp1jd$mvTos7g%g^xfjd>_Ig8h%Vr@jIP6-2HnrncuqvPmDm4-6xGXEJVo))G_XV zLNO_z5?2zDi;H1vy`EaQYKL3-s$pQMmw+q2hHJSZs_U)myRz%S8tl6s5jNFXtP1SI z+Nf=R%I={!U_{80EH_rmpEiZuP?2scO^o`ou(@D`)- zkj9Ar?o`1UckvsivCM+;Bl5wz08X>SF&yjhOwb99C|4c>@*eLacT|$Q1o9!jWsOKN z5eqURJMxl%$09fKBtLQ?p9t-c0gF{KC3A9>ZbuPsawvDQ&+N`0-f4JDVCIgpD6_KL zXvY_W@+!-6D~~P#|8584@(1Vg`_%6&`!X%dZu0D?ZzEJ~Q+}JG3ofv#%KRL?5*O zrwH_}L^MTfbVa8}A7f8LD|ATLvq9?#ddBda2F6YqweeMox=RzhrlK(_V<| zL0$vJaF7djZ})nOcUK=Zd&75oU&!u^2u$oj9`x@XsJDFccTJx*RxEdX3wVETc6}!| zdLu-D6Zn8LxQsE`J&$yR9~2n?f@&}Lg)cUGH~5BQI88J*NK3eLYWU7@c!_toiKF;N z8$=gcE`-Ni{EWERocN5Vc#YFI6#chI-_H-v&5hgmkNbF#H<#?S1dHeJ;INF4D>;xe z`I1X7o%BJJ_W@u?Ib*@eN|3QzdxeuXd6#qfm(x&pOOYlJ4Vas`m!tXrnd5ItKtd`& zLQ1%K0HgvfxUrh2`JLN&o^$jjyg8f8gPaF|JoGtZ;d!A~guQu!BYc7gYPq31x}KAD zo4@&=zj>UKc`8uamOr|H4+Y>@_@+ZTsBid6wE0S4I;97|CYZFStGG&dLI5nfN-Vl3 z#5%Tw`mM7%R0n`2KmwWbI;Kl{091OPM>nn~?W@=NtGk4<+qbdjy0dF+cT#$s|2ee_ zdki%?M?kwCuLQEEM6$wu#@?pv-_!+dYjYvs*7^}mh*&yGr6k^{Jwtz zvx_^dvqZSRy5?m&!jt^Lr}4VS`Khxxpfh|W9D=5=xlk1RdVhSk^ZT+7Jgo;nzhCXl zn|#Rweb}g=eaA=rN`(8_2S6{>dEHAs-D@sN0K1^Kd(pG}9I&)a#QZ)HejL~O;=euI zBmS)CJlZpRu}^;MQ@&E`rJG;=n^$_Ezd3O~vdA9=e*d`t>A!^HOT9~gyvGy&##ci3 zx_5ul&f%g<1=~%u7F~KYi0{|KX#);kP}{yZR_Czw(nm%C-d8t3)7Z00HC;gp>*rt3d(wFm%`u;zNlM zDN>wx(PBo78#Q+1_|aoXkRwHwBze+gN|Y-HwsiRtX3RY*X|Ak!(`HVbJ9YNt`P1h} zn4bm`ZRyD+(f~@C67}TN=s>0~l?Lbo6l>6|TeWuO`qgV#uw%uRC41IvTC{7`wq^U) zZCto><<_Np*KS_Cd-Vq8Qz(EyO#xW33J_>!U%rd~6*p#VGG=7SFB3m@eA#kl%o1Tv zb@>UYQPD*shbA2WH0q*CD{t2P+I4K$vt`$&ecN_!+`D!6=Kb4uaNxsj&YanYPoO}2 z7>*_WTzGVglc~dO4IR65?ap70E-e-O(#TH3AN{=PYIgP8*{^rs9{zjz@#)XEUmyQ{ z{cf-4_rIUG>I599KLGzbkhjjHf@&%698~bM)I_Vwk*oMH5JLnt%#gzkJ?s#~4@C@- z#1T#0s>BpE9FRIW5K|Gx6T=#6GS6&8&9tO+YzZ{bu97jvAb}iG$Rde6(#Rx{TvEx$ znq*SCJRn(P$||Y6(#kBcoWnRMpEQyWNuFx|P&^4k%ke?g%=?T?9AT=&%Q(N3lg>Hq ztP{^W^~{scJ^l0#%Rsdpbd7=XXs*vhJLDrO7H<@<%+=gn)6LWf{1a0}HO-XMO+D=t z)K5j-Db!Mf3QW*c2X(2yQC$s?#Rz3pv#RkRWfM_Tb$ylBU488p*k6SW)~;cVy(kY= zmDO^P%8q@uu2|Q6RH;T|s+P=JTZNWeV!Qp;+i<}hSKRo>{nemlQ+4UkbIVE`fmFNcn7;pXi)e93_-nS+%%2n&Gn zIV2h6k1iko9P+P_<6|M1wB&_(WJ!E6~so3t@wmYuw{W4aLZ+*Q||_uU(lwVi0QUn7_XG?6Kb-FW_V1Jaz5u6Mw|8x&Jv{v(u&* zO-U!D5nsy4pZ|RN?bq6S>XyI%pa1&(4^dYq?sKE@L(1a=E(*&gj5f#1=g)t0~01IfiTh+{L zWUEw56emL;!Vri*^b`xdS3)5M5s62PO~hz6ulDUwga`c2CnRy99wrfsO3dOG?NSMW z5HVJjK+F}jSVk_Mu`kafpl2urGaaUFDKA3f9L-oqI>IF|U<96F2xLd$+|iHP>)_#L z#iI^}Fj6Q35u>nCXy^9 zrL&9=)))ZWuY}lxo#FL*1?dL)>cF$BY6ru|4m^#gELdgSIuJz7$brO$m4%F#cMGSKOrbd#STNP_pO?AU! z5)h-YTaYAPG*-9&lJ%{0`f4Nt5!aBw6|eNECJ4_qIF=qJt9ccyT?u;@XU!{ z>EqK)QWSozGwNa~%h<|#1hJ6}tYtZSS@;OEJ8cr^wFo)V&Ysq@sXbL@A1Wlj<+Na` zl`UqZ=Db=_bWGQ*O2RffTi>FVv8>f2b9$>Cgn3~=ZROT)nVZHpPSu8}>tl1N3*4Ft zSF;@&h!?JK1!>8_5X-ghc=z-WN2CI`ty5cd*-Kvc?hU)WG*H4Oh=l-D0h#Y=?|##Q zvgZQyAqeH~fZyxZ_=Qk49ai zDZOY*R~pHGmULtUjmJ$FxlWkA^r%H$>IiOHUeG}`Y`aEkSDPBvuLh>3S1sL6w}sY0 z26e1?E$d$Y8kn^f5OHuU>{|mH*}hhGvhTP{VM}H>f_OGVcD-zCGyB@vmh)k!&8li! zrrX;87B{xZJ?<}u8z$f0Ot@`q?sC7|-SO@yoY2jx-*9`{dV=@9<=t<7bBJ~Iws&;u zt?m@@nO$N1x4#*_aFEvfpuiD0wH>b4n2^CG3UBzvE8g)H)f+L<33$Pe{U0$)OXD6_ zImcNZpiR`6`Q#r6Z;D%Xf5vIZu%Hc{03qRS_0$Bj{yhJ1J`eYI`9A&CppfHOIYpe=1);zY(N~3 z{4Q|(_75hgAo?)Q^+xamqlJk2E(arO^F)jlyzDueZ_~bS+=$R6q%Q!XVEAfH`jl%5*HA_9i4Dh#A+%7q zoW} zg1jIP5b+j6sRWs75RzvVZEyZqXBcgfB_xpwERg(gkNT3({Os-!1Q8i~5iL^DCA5GH zPO%zCF#(0_4%H**CNA}?aU&#y49l+!spA>b&>T5&An=QYcCicJF_4V^N}R&yKxhvF z!!RaFuM8ir?wFAo=^F?8+EM_ktD^& z@#4x4|7dal`VSfvG9q_wBPVSkt&7E^Z6GA!izcIT0>HsCQZe{I4yF#``cd14?;kgi z5+M)zUa=)$aQ`?kC0mj!z>z84iw^_===R_bD!~u@pb`eb4_>Y#_rv?1GBa4G77cPJ z>#i(ga42aI7F%Ky!O|@g!~tVP7vHkLsBN;0Wa4;{F5hkz5AyUb@c)cb9bW<^_tG$% zQpgU|Fze0m3iAMMk|;5b5@|34{V*V3P$1RP=oE7)Q^LhYj5FW=5+t7u?>h4%$gdXF zkqlo@21QaG?cfkha}aYb&16&cdJ+uH?fIHe|0pmCoe<-MvNy>95kk{8JrgKn1tN`; zDU(y}j`JM2< z10$~)p|3{$Fbxbr5HwUpe{@By>>IgkRF(|rp0NA?vmgimq7b?aNS`!FdvHkO%MTPO zO7XJ~iqj=>bT`ScI8pRU!xTza&MLeh3+T)kF~!O*gmvtoIIr_1v=k`ER7~si>{^tU zI3>)IhYz%LAl%dr2=h=rluirPPSNg2n=Am;)aTeVc@h=qkYiAB(oie)P*W>WX^a~! zH6&JS#fv?6Ua3&a z0`_-eVkQGXX*4QV36^0|!p2y~%NWLmq%~kv)O$o>;2M?~BcmX`=wKE0UPBgRL{@@; zM;BURT{D(sv!fynb|4a#9gsB~SC(cqmax1h6moW5Pu4nY7A#-_5(*+FVm6O})<4X<;v9W2G1d0%KjGY@uyt z!4{HQ!eb3WXg^jmSm6)CmTm1;sRSex%2pKm7H#_$YNd8>{nkqFwr%0oCAJnOI@WC! z7ZMB?Z<7f+tD_i1A!P~ICHyvJb(U~N0djHwqHz=VCBBGgXVP&q?sFses3uokpLS{k z;B_VAaz()r^f7fCav-*LCJ&ZqxmIy=S7C2gb%mF1ssn0W!f9WkX{k1O1K<*DcX&h3 zZ6~95<(6wjHvp_QdWm;?uV+A(HvmpHcBK|3Dwor^7k3vok9t>Yr*|OO;cX4V6qXZx zySIJ~=2v6)Z((;Jcs6Ix7Iw*(CGQvC<|)Yo*~*CnbqCQKoF@7IBI34*J`f6cdVF*hbswt0<4dtKF;KGuK{I5J}P zfKm5^P1uJ+Mt_m_bu~ALi@^<$cYj&`rf_?>ELuWn*%yS@H+OB=Al8$Ke;A7ihAX8- ze3>?gGuYl{_=HE7j89jD$ryNh_KMe7a*MSODlAq2xFrObT{m|!78Y+^;$Xk%ccEB; zsiPj*SdgAk4Ra)tGyoIF0o<0HFATefML77IV&ZkUM#iOw)J; z7lyqUgIywL#TSNoi+J~-WnTh*(KwCeb|znWfeG1@wFs1DH-=GGg598q<@j&mz&OOX zQiIN8e|JFo7<&nW8g1E`eqIto0aCte2fjM(0jhMSvk}DY|diRFgcb22rAXvfB zw)vUUc?Fxbj!ijd)3zl5cxrk7)^c6I-w6bp&Oc^7y6+kTA>}fq9fX(A$p=SnxZxOqBk0&LmH$<`lCzQqffe{Q<|e! zTBJ?dpfOH^5{8al7i!NIk)76ml{cpYV1hAL6zl+xf%Ko9c$}BHC7@Vnx3+f)dZ1AV z0-hQI3P7r-nyRbXs;?TWvs$aSnyb6otG^no!&ZJqD^F-~h(jun!xt6I-zt+pq`w1|H&p4$G0{ zc$87LB@}s%54n*6w`r&U7J3bLN-ueO{kX5?r>O(`a|WBSSDUq4+qGYttf3kM+#8 zfrqCLbQ^q?*N9;PaM!wp?|7fn_;t|Oi1${J85xXQqNkvx%@11)x?rl?Jgn&f1Ee~~!J5ajTFqy?0PZ`w;rzAXT)`Q9$!Qvl zzu3tQJ$6Gu50?0L`JhTaoMZc%cPV)@Q5(!vN44vGst@2F)|}JVe5?=P8FXCK$-JxS z!Nzgi)VKQ1vHH{XyuL>r!DSuE5qb~ab$<51d0n{rF>h)Y7B9V8px?x`7vHpv?b4+x39cx4i)1fEc`C6tG>Yn}8X*0S*cP0+^u# zq#6WxVXEK%U8=!d+{wKF9Ka2jp%rdm#l2z93t-Imd=%)r37%mZ?qI(m;10T>6-J@Q z?>*o5J)hPYy{hqD-}zmt-GCRm;SLHQ0owkP2VgLe0;2E&J1CSvC5&+Fhe$Cw=38tFu=^X6Ep4=G$4kCa9 zu3f58{l;^g1$-f@3m^g<00J064Wzmf+#mulKoTCF?(H7$wf@Jaddxe38O|QmrMmI& zp4_MZ!2|@q0Q5i)A|L`7VF=v5?Ezo#2_Npme(YCY?hByoEq}$|UfrQO0&IP_<;Iz8 zx|9#SY@vLvm3OyecWsXyviT^>Ll>5#7@48o_@&-X#9YTKfd&qM9z4C%S3KYg;089K z05~8TRDQ)*JOR1^18%(h&mZ7*y!!$FANHFDMj`M2eHsFQ7%pF`S)daB+aLBDAi9Vt z5aFML0tIM{DsbRmgaZg8PUC>VABcz%3m#0kus}2p2n#|$sOJX41l=$Uz{sK!!jA}V zY_2>C&iAt4_^&wd>ZfU(1e7d$#S`xNqyu z&3m`+-lT&DEie%woE{AyGIYqZc!3Vx5XKnQY6Qjt(<5d_=p1?h5Yr-dh=!0Jhn3Ng zZAuWaN2_kcW`bxIF@Tr@;Kh?q&NCv+>D5JqUw8WtUiX=uu6_HG}~| zX8KnX4W(cd13eL7bVmRY&8a3rY!YM;o*i{_9&sJ$!3cXGFcT*WGiij7KzeSZ&~j&9 z=Nxu$X0#_qG$B+Gdj%|5z#TmxROq1*!jw>(W8#Ubbd~Bc1c$;!kW^7%erfAoakbT= zWpD*>Vp%Wts;jSgBz2Ttw|cSfLKc$KE?L%Q)H10Vj?X&lZMWclD{i>t zj%#kY=$@-?mSQ@0Tmhvd;lP>k&P!Yp5OB~+s18JsKt&iN07nE4R9Zm)!EthjufCxk zz{tQH@InJZ$V@zD0-Fpp0x|v~dVs@vL;!&s_Z9GP1QAc|@KL}2`or==C#QTM znxnWeZz(q%fG|QEcO2m~3-7CNzDC0gumjyBTtJ5qfRPl4>sAdMu)5ME*NbTJ>g%on z6pN$PWT$I(*=RGxMl-FD}V_uhK<&9`r2n)+@BtzhHGbA=xs z4I|I+k_1E`5JQhIp+YcC2RBF(5e+W+$GGD>IpD@Kt@Ip|p%q3zPX`Bx@eDo4xPg!Z zY7o;+>9dbsI!}Zf4goYB6yRs%lUF{?MWT~#da5DmF-s?1Z5ocx8JFaCF6=Lat$ojbBU7c`~lc8E3)XHuYdmh+r|eUv~ZhTsAa9V zRAvr^pw<2s*uMrc@PQ7DAOt5U!3yp{OxMX^1}CLKj4;9^2>BpJIM~4w_NRm>L}3Cz z;Ej!>5QPLw00)pkGZ2=LgcU+zC2aV#SV@h7K;#2iZp1zsZB1(~vP&r12N$@U=ti@$ zpcH{P#VSh04{aM@Tp(eJC{po@RtzH;$0)`!lJSfU#NZ1tVyYCXZX<7GV@=jb$2y`= z61?D{9WerjXLMr|COAMGQP{>il@J2-aA6OHaz8ZgF9447$kmBLF{8N1uSLHK02{dF zNHcn}lTsQB7CBQuvMi}Hn_gr+oQW4-{4VwGA9z-9uNhoVIDo6`&@IL9f@a+1?Y zxCEv;*U8Ryx-*00lYFo2 z{WG8t9jHVjO3{g06qf0Hr$#r*(T-k6m`$muxr&m9NM?~nHHu#V^k9dvi1eZ`b)-`M zN7F!fGp0CwsZMjs)1CS>T>$|AA^8La3IG8BECB!j0DTAF0{{sB00RgdNU&f*2M7}? zOlYv7!-o(fN}NcsqQ#3CGiuz(v7^V2AVZ2ANwTELlPFWFT*maPz zvuM+*UCXwu+qZDz%AHHMuHCy=$sWbax3Ay7fCCF2Ot`S&!-x|r{;GHH;>VC9OP)-* zvgON|Gi%-~xA8*GphJruO}ez{)2LIcUft94XV$P|%brcUw(Z-vbL*bz8tLxez=I1P zPQ1AB_MN=B^XJf`OP@}?I_BkNt!v-Ty}S4C;KPgmSGjz=`Sa-0t6$H)y=d3& z+*pMVA`=%9oaYUrVeCaUP7j5g}%qmV`_>7QspvW4XZysj0^QD(kGYg5*X3Za|P7LU7;;YpuWr zE3B}*Hq+}sa2PA$u*^2=ET{=3+baapGWYDY*k;>kLJ+uNY_hpptL?btmdod|0VHdz zx$L&3`q;FuIS$D@4o;$8ST2%D%)?s2q&y?j|kndu0RFDq3gmF zS8TD2ZbTDqgb3Yw>%}04EV77NGGlI(o8q)^wUsBE%nq?S8esxSZA&E)?9b(_19pBE%w-Cmu>dh zXs50A+HAM&_S8Pi!`s%E=j&m6SG;>@juGemRL~;y- zMC@O6zI*2Zcmu{od)#jP@wR;fyFm1GQ&2QmdDA&C0T@Bg6>5xz!(7WF<^iKX$Jqk0mUg6#09_u2OAqmkZ!nd56)wN{y1n5 z;Bi7Ad3Iw9iAK&ob486$7PPV0j+8YQM zfS0=&uCH_CW275-7yuI%gn!Tr0UzBMk#Ecoc-{zs2rBr%;LWd%0(nN~Q0P10p)Z0m z1Rfqs$-iSpaE+&Ff+we0IPwJmjb(72@*26l0F0s`?Gs2S8&XMf0?(S(BnTfX2)sf* z@Efx<$Rh*5%1s8Kelyfw0E?IZ2QzX)dj1n3J;ixVY8v#QSJ~$`M}fzJ(4&-sBnSik z3Bhm%V0Z4r-z4oRkUV_Dl?)w4Am=$kdl~_SA%LR&DkzXq3Z$0?NoMze*H8OhQ6Q?E z3`-Hp)4_Ev1R;pV{_aPR3JHc#%FcfL%OF9TF~4yVgrnmj|K$LQt|Aglhm&89P@Bq_k2L2q)yYK&=V{9GobiE(73%Za_8w zop{JtacYnZej~ciGwxw_nh`Z<7q#Hc+Wzd=4RAb?hqnE#Lm0`=0B~@Yo!cKU3@4*oc@!=*MkvvRL2ZekIVSy7*5xZD%b_6jb7hGOLs)WGx{7s5q%;O&0CK?v@ zv5Q%eC)v$iG ztYb~F{}5N<+0cHrv8Da!M9X9g z)XsLax2^4NbNk!g4mY^RE$(uY``qSEH@epyX`86~-RzDxyyq?Ndei&f_RcrH_xxYWgHH60&pVJV z*<#U`KJ=zD{pn7RI@G5wbw}TX>R7LO*0-+pu5*3j^H#aQ15og>gCvk&_qy8C&i1vt zz3p&+`=NDm)wtVT?sv~S-uJHeaPI->C+R!i2QT=-6aMgqPrRrhdXqe$9r2J?yyPQK z`N{j;;s55iNK|h5%6q=^p#Qw+Ll1GCU}4dtPd({X&-&Gqp5G!-I?tKyknXv@^|-&i z?sL!kpqm~GR_(p;cTfD`8^8FN2Dr5=l4zX|L=%wsgeC&;iBtqY^TdlFcI z_9uPRH+|M;fkhaFM>vI2ScRzegFQioT4;q_xP@LgXagaA6ljAO*nt=*eKm-GUATtk zHW6R=hHMyejQ0~C2#0nUhj(a%#YYg+XMY^Xf!4Qy8+d^kNPj7ahln_A5J3f7U=Z#9 zmxz#Ijr-1>G3TN1dGMIsaND!KsigULSWIzTCh>5VM zirSVGvKWiDSc~Pidtumt6o`QWA&So@iew0ixOj`J7!r?HjLK+?nMM>S$c)gqjMA8U z#n+3!$bo}MfdVm#zu18VF^ykX60!%5(O8a^mK2Xjj_PQR?1*)H2#TUOe@sY$G-!s~ zNQ~mQjyi-25F2ffsh!< zhFItm{g{y+S&<((jRL`h_NRe;Sbb>dhky8oC>WAa*b?Iik}|1@v^bMC=>StetiO^t z|9O+MRehf*hVdARfLMJ45q%u!2#BDQv{pRvClK*hhp7h>1BsMUIfP2#lsS2oQ<;_F z7>wE|gG$Jg*l2^APze-?m5io&1Tly}82}KacQL_!S{ax7Sd(Tcmv&i~S9pHU$B?CH zjRYZZ?Rh!>QPgLie8Ih(|{k#tC#xS5%lxrf;3i-P!&^=FdS zXNZ}}fxIY;B?*1j7n%}yfg`C2tk8Bip_{pBe3eO^*jb$zsf&MUl3h874cUuO|45pI zX%J-yg{#S$rs>6{tDx1F=uWlz0KuQooI;TO} ztd;7fpW3XT>a3{Pt~wZe-9xK}nW)uS3FmpB2b!YJS&E^VoCaEm>6xp5)OA1McmgZ0 zCug4=yRkF+smiLa0l=o0nykyJrp)TB^;)DLtADS_a(~wc3Wu&*|4ETb%CNJlr4L(@ z$w{u;+MEX3t?7E3AA7V77m!B#vP{c#lJKl2E2nZ=vL}nL`--Vo+p;c;v|KBCf2gyF z>W2;Mv&LzESt^kZOPo^ot}?N%UHi7Z76h zvUE$SX}FT9nVu`ko)3$lu!@``8L%=J6mHwJmb-6kI}=uUxtx1)mP)esny-C3wE$bP z_!_62s;Po|sfF9QlqZ3usGh6pqRsiIUrP|J>9ZEtmo|HGnfsQN+q%N5Yf1~DMC-c4 z3vc!cx+BZ7CHuK1tE}|OviypvgWIUd>zL3-hzskPhNzM||9hU_8;VxSy+7HzAV;Cd zyT0`H6A8z@@QZPHE2n`wvd`PE&P%n&da^4ExKNtC0IX~9nT{PwrLao7-57)P*PguIin7w1y8J4*=u5#WJZQNSfC4cE zRuBL&8~`r7p|J+Ousg8@alj5Ck;-Y0W(&EStAGIzT0TJr8N9+u>}gqeyi5GVrq&T6 zY`Q^8!YHe+E2~r+yv4sZ5LOVwR&WI}EXH9x#sne8{~N`)tDX~BzG!<9e#pDEE0`%P zalfm%T%5!V#eQsSe0&jE%&aQww^^I1)O)>k|NO^Y`@&{i#*j?Ll03#%Fa=kT z1%R-~?-#CQyRa}?5fTfr4ttM&SaW+|#6ID8jI77AcE>0YysW&+tkylyip45h$XOe; zMC!qBy34&Ry3~uh0l)}0D$BRFMQd;cF|5o1z|79P%+TD-&@9GNz?Lo7Z#~PJhU&={ z(T6~(!)?rqMH+Kt(1=HTl*XLSfChm!A;Gcy&fb=~eyg{(OwZJN$hnNQgj}yAe1npz z&b&6m&n(RZJ%&~e@Ux=n4a$X6GjZn zAN|r7WWhF3vqoLirADt(3%&FU)xM0sQBBoT{lQaB#c4dBOwGhH9LCXn&;=dVWKGao z+-{YCn5Y@3XsePr4beNj!#iBQrd-56(TGI7)OuZMNG%g`fCyb3*zc#QecQ{yY}g__ zx~i+#ZpzraT)O_u)qAaLczllA*t1x=mT3Fdf61JK>Y9J((2|hX4|mZpS+jyI%cji{ ze$bMWo!XY>saFlU$qEzAN~A7rmayGuUVz3jF~ndPtIP?72x^bzdyl{f+7pe_slWz5~v&ig0S1KJ!pNMbk}X&kk&n`|BKc4+o@H&5|_H&dOQcRIccnbyO|xwS!%0I zy1>fGz(TvKeF?dea0o*e-72Bf+iqPO+CYr{K%6W z;S@gM7B1nDO=x?NGF2+5t9qc}E2tE!nq(NZSL%zb5R)eNcR@km4$jUf;o<Hw=;Oy%KYakF}9pq&#j;$)~2>>!l>)7Bu(a4`w_Vf=A#SO9_K}_%&2E*falF`+?k!J zoU8fdL%GoO{pU>C(U02VY`)un|5g%Rp3!Ss)ze$chCIl(OcHCp=!E&j7hd5QuIZef z>B?*caxj{Pme$)VtMc8wKilWaIiB?W#)vApKNsLTXX$?&dy0DKxZaF{d)QQa=4Ot| zgsjiyt>zw`>$CY2GaQu@Ih^p#?B9If;H&C$4Y?HUu%ejBPRHLIf#tAH>)ZWrFkTT` z^orh|&WVQOqIBL^{xvA|b;m&?mux&l!-z&#W{-@*V5IhWYflL!rfZE1R z;Nq7;1R)1r@CO&GddNZFX8gzyz^#nXlI}Hl}6Yi zeA|va@r_No>|W-O4H5U6^ml39M1jU=Z-~Me=(3u_>e=-I@%3L%5Qi_rDBtbCY7>p9 z^lBgZE%ng3^SsiRU(dY%x4u8@(W}2lZu{ie6dv!r<|=^Vu+Hd@9|GvC6uF{qd{JS6inGgI^3laGV+j#)SPv7+J-~R9~!vqV4p}h6P zjr9eQ(9v%Y)i2WkVI{yof_nlOM0ikPLWT<+HiY<4Vnm7)5ANYZP~%399X$rvLT1ay z0Ea4`w1`q=N|q~KwuJdoW=xthZPvtjQ)f<|JAL*9`cr67qC<@qMS4_eQl?9tHii0B zYE-IItyYCP5EZ5Xr~+U@h2Pg}cs0!!9x*tBK2vJLC@Y}>VBx0Xd4*6YBleX;rl z{8w;b!h;PLK1x#YV#bY0HVmLi)4)`aQfW#xaPnl$kRoeb<1w_A&;dvjEIlx_pTn&w zW_0YB3gbh0|6Jcjj9YhZ-n)JG2L4-kaN@&t2e7TscyZ>-oj1Qa5O3P9nB>A$`!;X( zxpC{#)t+{*^?_W)@_n8jx_b8O-M2p;TmF2p9L={D3aYdXo^3J-j zsynO3x@^=AM;qN3bAYjUtj`i?|%%rw_jvrRVNbhEWQdMU9^6YnfY$T;ETbI+)f3&5?izS^t0 z>1MnO|2sn!O?10Ox7+K-6#eWIx=Ja$aPgI=#*l z^H^j5(ze@eWt}zL#Vnk(qbV=*OtCMu6!4=k2Rh9Ua<>)NquMI`HeYY|)wf@M|7}e^ z6a6I^U=0acmb|Y5Ee}x{sr@cSXsh*ZJL>c_xZs>J<~YuJKUS;{iz>T}v2{*12amr7Mx3JMBbqBc6};IjS;$l$2?$9<~;0iKUG= z|5`@_kc3#Iw@#XC_sRr0Y@?$zNHYH{LziSPSye5;F0HV_0!83%6|^i<%>&k$`GDNHLVP=#4(cwdRyqsdI>B1Ic#~TOz^TGNSyL5{3 zOS{TcE^8Mxj@BN~f=5z4l~n_==oTZ=6{A_P^aGK`H?W%y5PyT++yZR;CCnv8qe=BZ(Xku`ikBArDjl;DqvU2;@}g zd?Q3636r=)C2A^pPb`|yh*q>FGSPU_sal~vvX<>pFnf-|UiGRtMk^Z3geOFyMHG@Q zQ^BilYp}rlV(2>Bz0Pg{n1gNpwFi;$0ZUD zY@9}vMQRl2`^HDYEp~3RHQOh zsZE9IQ>FS#nxKlDz&dA6B>{6G>3vFs8V#!Hbu(cR;?c|oU&Ck|$DFhLTggElZhl~yRCJ)=mqe91stpVSob3a)(B?; z99lGrCAQv`)OiOy;S@57jPd2v{oMHylAtq7?aQnw*;O}z1Y*GyM)8SNyc`E%mmv1M zE@x9b;|gaJ!gS>+jdgs+2UQWnocbNGURTc2tV|;-BMsY5{{zl7NO;9h-f@(nEED*a z7Qu>Hpp2)CS00DW!5EY+dZeu5nz0j`Kitc9r{{mSxhHn!%z7_6nhF_CJjbWZ7 z0?}|O*L&L(Ck)RX3Y+KcK@PUgb;vD`7Wlqu)k=C>f zbOgs^u46{bh?D%@x%he;Z4qbs&|<1G$F3PzRZ3 z?1QqNvfTUJNC(*8YKOf?lwuIJ9f)oMv23eC5M2wDUyp9vVVc9-~ASL^89XQJ#n1EyTkv)WbaNJjfWU zjhKxNR19Z>J?s-eGrO&U$OA7>vI9Z4J%mI)jKoQl#C-xpG7!WLjDkQ6B0|i*?`gt; zKt!xKh(P$bN2DXt00aT5L`rnUR)ob@JTs0ULYnD7{S!na#Epz8L|yE^{#m=g5Jf&X zz}D%KIS2sXQ$bmb#b#v2XLQD+;W08Wf-($%Oys~T{KHRNLpIE&!e}g+34}n%13~bk zBaB99RL68=$8`jsj-bX41fpET#xi6mfZ@gx>P6@Hq;~X2cl^hI1jsnagIv@IA|!}v z41i6X8T}K(jO&?voJBMvx`7nP|BJN9jKoOw(mxNpMozrNnbC+2JVFlPMTyi$G@H1G z)X0|HNSB1km%NpR{K#9R8HKdQ0I)@t1dNkxyit5TH1j)|jLD)T%A+(&2{K59i~^m! zMxC_Arqn+G{Fpq!i1rB+Ib`M}%}rsDw&{{GakmHP{16 zE9wK0`Np!eOS8Poyu{1qPz+q;z-nwg5b?>iD@h3YLNr>&z0}LaRLsU?%%rF?TI|S> z1SG+t)wMBpi6Nv zN7Ot`(nF~M%uU?%&35{T|6F9t$plM>!pt9IFW%xzz=|rTS}Kj;gWi z^_M(gg;2>7HmZ`k`G^ihQ4c*52wlreTuspO#!5=j8TC-P%d~+Y(cbef8s$+HWwHFo z#-<#I)qFV?;m@@bO&{&i)3lo$WegG9Q6-hqB~1^J(kRY%r)sh zE)CQ=9ZPjl)Km&nM0M1c&^M2egF1WE0_4yPmD5AjRG56BbSaHSEm09|q)I*1JeUJ4 zFwt`Rh>YmLjj*dtHBz66%!h(iQY}g_T?|llj7p$NTcuG`MLC0`4^i+6K=oD2EL5R9 z)>>`H`A8X!@Kl0`))QS;FHMfjBc~gk53z%iYRwQ=iZ5pM){I;;8||=+!cpxT*DvL& z8X~s`K?P8VSFJ!PajnY!i_vde*L`(H1?>nnD$`8URVU=v12wkpL%=49S4;o}P;gCq z#nifl*omD)|2z`aI|Yp}B~w)$%7SImxBCc$T@2E2gLswKlSKvVq*&(5*V*#em*vAq zgHrlYRgchBKv~(+bi39O*;I`Ui#pkX5QPBP&79?pqtVQLgW09sLgPzUjD1qYa8jey zOM@V;U~MC$>n@aiSf=F`!d zwXK$Qufg?P^CQ}F8`F=-QM+waM<622CCY)Yk9Dz~j<8#84K|Iao+$lXL*m>8xn0$T zKt4!Z|M|dE*&Wf4;oXh2T1SLjD>S8^y}TE)0y3@s$O9AmTlmG_D4RHR<=)|qU0q#U zf*8>J?N^kGT%TpG$qiX;4HJ1C8v+gr(1Ftvg+J|u;Dswjl=EJx6<%pI!b`p2ze5%C zy*%aZ;3_n>ta@Mz{;x1hJ~$2G7=}5gg;r-}BN4k2#*kq=1eD`imB;D`R3c%;FyU|# zUl!hBBvxW@yx9H{u{B8B95&Y_R>vPcr$#Kc+MAE%Dz?jYODeXBBo<>O-dWgf;#L7w z{}TO@8xGYKyUbdzS=&ZWqEVe4iT~sSh%#TKCny%JZ z=HI6!>2W4!{j_PT%hpO$XSti?^W`eXup!(1>7}k|D*e}tg;rA{(I`IRrpCBXMqX(? zU?cWr85HSYr8=etLb0}L8g1fWKIr}p4V)!w)q@RQ&RRV--F3)m>$Wy) zoUU1&w$XvzYfv-bq_$|5PFRfAXSwUX!RG77{!+gtXSEG%J$`1#UNM8tVu9vgx;A1v zid~ovILIb#%x=�@!tt>Q~0z($>2JltEMClA(TNnjNms*5f)xRme8&|K85aKPp{K zR@@Bq?aanwJ3egOE;r8>ZOI;CX)A8mj_!c;JIBb;;3enP9$V=Sy5nUsxu)%|4&otG z?s82!?w)S+uG*ZAYUc*$02qYrPH*XQJG^b)`_?SFW@iJYTrM%z`BrZLm&5=l(BN$G z&NTLY?$I9c7uRq-gm33AX{=2R2aj=ft6TDp@MoRtQnqY#&7T}6av1N|NHtff{*t^d z@)--|=KgHEItTB*{{*(L`bytwW^+;I zUvuViG8ag`DC5*laF1Y(IA5>=UtWzKZ>{2D#ZK(chVmho^F!Bj8Sn5-)~qb|=|u0W zWUTOZF7Mhy^MSr?N?$Wng>*#s^k`068x7zu?(|4!@AAc3YPP&L676@*^bh{@STFU$ zC5SyE@)yW2(jw+ahG>*r*|fPXKE)u zf%b9aZu4G-ZoZ!PfUoy~*Q=hrbU*iM^1F6+zj6}RW`T!z|ALqJrNH&ch46!o<=F=E zJ-&9%oau-M`H2^K$1d<7kMdEP@^@8phQ}{J|M;H{`IsO1nO_Nb4t8q~a)qbxJ(lpx zrumG^A`H12l}T!=F!e*Kj-WV*LlyT^fjZNOZMeT85UvsqA{_NkoP&EJZNB{I! z|M{4S^>_dGhyVBwMsg2;%G3A*cI0v`C%eDAUImCf0R$2pXs{r{g9;NeTzA+3|3E#{GG6PrG33XRBSWS<`LgBAm^W+g z%=xqD(4aMzEp7TV>eQ-NtM179HSE~3XVV5N`u63ao>2AVEwrwjpLeYcFCN&aVZeg< zE(Z!|@o3!Aqf?)5edP7*)wgT!&i%Xh@Zc@hE^q!k`t++as&DW9J^c7a#nUHGH6@>) zf`=L=y5BEh!rAp-eFctZAc6-fm>`1-I@lnD4@zj6d=*-FA%?AKl_7^6diY^e5sJ4} z9%qS^2T;ZlcU)2Z_0-&7%>|I3h!fgaBaS!fm?Mun`q(3o5`s7+kwt#!nvqE=x#W34 z;`S9vS+y9Lio>NCV2W7$;pC59mV_cv|0tTsB~D~|d8V0ZqPb>*OuG3doTwogC!KZL zIhC3emK72odCc)&QYyYUlofuA$tI#sZo)`UpD>l@OOQ&MD5aBHS}AFFYPu<>Ma|hM zsG-(*X-)xnLRLvVff>|_ty*~{fJ2drX?j#w+7%a#NmG8=)^yvt1re2Yn(C1 z8z+n}$RWR0ugE2v45i1-mNu@o|NH(V#~jiQNHARmrF?T_MX@YyQ~BZibH_mgJ@lj} z8+~+FBqO~v(@6>45NqQ$yGI_+1>nU`cl=}`(OvV@a9a*PJnko+d~5b^L_h5{+iSZ$ zUej^Qjg-=J+uiirHp2$(N_wx_HsCSo?336=-Reo#Mt->E*E-UAgSFE1NlOuj{^f;-?edRN=RN z@~zLm?tXmh$**oZ^B~jSJoKI7uDSBnH&#@|zW+3OqhyoIv!^X1Kfd+lliy(U=_mWV z`t8H{wj|~8eZKh{>h`-*|E53uecJTnut*C`4+jOPl-@C=g7_1k2VyD9H$;8MlpH& zJ6+;fxD+uGg;ZTUBNWr987#69K2~fa98>eOJ?JlsXxv={UD&V*8c`@_6C3v&CJ8Rm z>W*qWq#;Wc$3@C-gp7RTdbGGjL^d!F-!dHk>Ucj#X|gQ=cncvDNl8(XQe%*$O)I|9 z#y9S;6skO>Y1a0h|5Cz_D4GN$AF;PRO*$fBlA`4(g9*%tP_YN4F~bGlC+{vYU3u{`OsK8a-}Z4PekikQg-?Ag`DiB#I~7~-f7TAj?e`t z0uWSDfI<{M83lAqs??dn4U6Y|shnC0M`S?NHZiT|QWqB#jN(s!EbE;G`R7fGxvM@t z393;X!3c0>|COn3Rml{^z=5i2=yPLNIAeJo#Xh(n|b#SD10s$EZJ+4F4nvX9d1ULjkwntrjHV~wC`4U1FMzA&j>l!{R! zD_ha3g^dSgib_4}G^y&gw}%B>d}uqXjQ*3fgoR+E#99e7It8t0UE)Y*D_!Bju2|?q z&nA4!m4yx~yXWz)cIg(lJQPo(h-A_3Bo|TE|;uFpW;}4Su0}-s$o; zvpBpiSX8OXgq}vg0U&VfxEtPhb}Rtk?O}g$mI@`ZwuPW8?0aPuUH`>40H}DbQJxE8 zQ`v04|0b61f}f&>+%8hVDTZaxusO*Sn+aTc1gv|(c+s$S6uH0UAXIn*3g0}p!x5(O zkw3ELNy&)WU_({ErtmtsAv`new_;xF=3g_j(*~tdmH6Q zuM)JgZ1kOb1PJdXcZ{CybcTyUS^M_J&_Gf2P)1y4&SkpQkfpR|Ck^W>QW>kRR#%<{ zo5X$kxzDAQuaAFHB24Q}h(9s)kj31av-)P)$%gK&9}Ub|<5kww=3$*f8*NN^`6ykE z|LqcSB3Dqn;7n1kFCOg=I4cpGKyi3 zyIX}OvxiZQ@SZ&P&=|}&yvtc{I?H+DkfbtsAslf;Vffdr~N%wuTrm(qLY3Cy;7vdh+(OEIJVwjifT3vS(9639SF`A>-4>}D%_;qK-+ z#7S*)23LIQaGYj(?6C8ww`pAZZ0)yq8gd^S{nY0kb5KxjXjG55#9V*76Rj@Kn#;X? zPct@pwhr~4dtBESMWPr34e&!I9px-{dD0QB>>(F*?{J4SP~Kd3Hqw3a(Oa7%|ICd- zVn5zM9p`4)`ziK^m)-E_KD*h=jP`bO-tn$yo8?bg`PegWZSkAv>)Rys9{)P%eqXQO zD}gX|os{XOM|!E#o%m!wSMQyF{`6=c$JtBYnT&fDdO+&@+=Hgdp#F6jJzZX&5QshsK^ zn^C-7|+jIw0vqAPeG= z1!58Py&&auj(ODA2AUNS86dw2-QNu!Kq26&DP7Yw;0dzY4Yr^~$so2>|DY1OPxS2# z67~oggr;cZA%mkFT1 zdEgw1V49p%2_9Yh1%<=?1f!54AYxs4++P~nNemVu(&UZzX-FV?iA2fa2ijB}7NFt~ z9_q0h;-OvYx!l@Sq8m0MO(7!xp`j?+&hm`R_JJaGOantKm=^_VynQq`%d|lyCWT6U)nW?cHEh5~Qu^+S%qcwVrl8izT z{Gy%tA~&jG^+eJ&0${*>T|R|g4&q`@y`fNrVGtf3JPx7zjbkqU|I9Z+87bbQ6Ie8ZPpD;CCdo%eT3bp5S|+0P^y5{4*H@CGC25oBc$-Gql*8x>->I4=ZsN1y z*B|C3Sf&e42E{0}f>|0#cA3CWfWTZvLsF>aP&kxRXr_Y&{{Uqcg&w@gTYA=HR_0|+ z1!u0tV@Bps>W-9Xo>wTQ?tM~*9akm+Uk2VvLYfbwNaQ;*U!tX*7}Dlq2FcuvC2HJ& zPlV=d7?W!<1su#xX>x^g8pT}7MoQfYbOyi)K*e<84FK7tSzM)Yh6YsPQx*2$uKnF7 z^rnbt#~;!pQJCPeo#$}|i+nJHT=Ya|$wp7A#tnGGsLjSUVkWT=;xJZM$SCN%9H_lG z=yqwLRN!ZP0!;R#mf-Cm9S)PprJHjBCp8vlep=|7$cG+`05F(`Hz?<6Y$Q^?hEYld zf-y~XUPuh6RDh;OE11WOMn#Q2#U`*o07RzN9mxkpj1!#<}MqK)Yk(we=M#U)X=;ZLin7+w_{luHT59Ad^Vqz&{ zCdFDc>8tS|9qQ84E!DH+!6k@-19qY&`bJ{%>6bRDcEpp5YD%0^g&WMNh1{r=A!s_0 zCKM$pp0Wj`;GI%*7<|r__c@H|w4{{|#d7gie$1!SwV#R3>WM}ZoBqTNXzFT=fu+_) zD{R7b9!Z+QQLe^DuTI4`WM@x+z;`Z1u>!>?jKTuE%Ty8=Trz^l8?|7!r~DsF)TyV41!s!zTiWmEJk?xE{N z{2}W(1x%Y-9$@B~n#{h6 zWU)+aLtN`@KHtzWrG7bVjm)2n2^z)TOnOLY99?X@)@!_C?27hm#!{vj$ScQ=tn&=3 z$=YbmLQTO+6Qh#UsczECwGGPZ1XTRN(qdy8>g><1sMpqO$GU6SdTiKs?8aiIy_QVL zCatJct=>S?#{Gn-!o^K;jN87%%$Dcfg6Ru#Ezbh&yb|rZZtU37tH+A%>>w@M9?ji~ zf|0>3^gRn7_{^e7)ZR+nR$Okn%Ant3|E$@5?aqeo&knBFiftFH0=<%~;x6vVRBfy} z3m|4~A}Q%p$bld{t?VjD-wrOu0sn6LJ}z14Lc?lq z?qZi?%&;q?~M^qK@& z*e>qIub%ARyaKJ+_H5XKZ~1oZ&Z?#N0s}}L@8=>ezOe7Lb%T>R@Z?OadCl)HQSe6O ztytjiPEH``20%mggz!c~Pll!*eC%{`EYE)JHu}VTnm`huZZs6=`mQdWAQh25l(HI2 z*2YGuYOr+putH$4Ki;neuhA8q|7LoSFH(8e7?|x1T!I{|@W;X{FZ9IWHp2}w%E$I+ z03<=Yu7X^QW)G$L?(D0>g^lz&9W$3Y%>JXr@DTZBL-DyfO+WG&0!&LwL#y16Rm=66mxh z%P0VBQ{b#pY_g2Pu8I2J9y^I3`^e#x501JHQ&JwN))GK8HLr;3| zfo5#+20$u(Y!!1t67&R-qOn!ja!E4iyv%L9kg`)qGf^1rX$&aj;%6hZvN%J^1dp?g zz)36oXBoReokZ&>Z~_AN|FRE&7vlPF6MVxIyRjh$01fmk3Zri{jOHq6CZja*ymYdB zFp8VP22#mzSU5CoX!B46scOLJH=k=#nlp|FvTQ`JM(gg}j3%8R^8Ws;Pav`zZ!rq5 zs7PD!-%fHjyz2q8bWeb>BfBwEOsbL~bbPRJ72C-TM5?HLLr1=GvfSzrgEIqt^hSU5 z4>2`JKN2=`#Uc0TXBtyI_j1qP@ZrL%P2T|{=z&cGv`9w-kruQSt22BwE6S*GSg^FW?F;;4Zh3@bqi+DkF>lCzQoq*EO;}1vZ3?cq@fcZ=P`P2yvT-N7uJ! z)LrlXuQ5lr==%36fR`i(F3&B`3+_4iGyx|^Dcn@E&x}z_Y&{n8gklF^r+Z) zRei&;UO0tYcu!n7KGG)bLLrieM~7=1lLzF?VR?aXd5e4Zf}gE_-+2oEZ`oq`$%;9w zUAT|W!#s2YkgK`+6uDr}Ii%x8wxkE6y?Jg-1(ctXfsbzJS{C^}a_ItZjBh&ca;*6J zFL)Pl$40mvQ#0+U`8-T`ke~Slo~!d3d89u`oTtZKSh^9<9QXG5{{jO$1-g_+Lw0>S zcN@4`UII${ZZqttm#1%Xhq+tF-jDM*h5Lk$7tnbQpPc)8uIJ1|jXQ|j+_96mNjE{4 zdv5>?|1FFM@YsTS0QkW8mTgE?g0jbWgA=;sXs>+CdH^VTPXKuUY`bg#{E>d?9yG1y zh|$2XpK)yL+v>#EWd^*P`u7}hxFQ6+V6aZSfLEi8ED159QY{K1RCOf^efd@bjAQk!= z|C30oKr=)`;0J&T48ffCeAfFrEXA>8BZV>D-X@FyoB9JZ_ypMZ1S%u^SUhGlSaYlA zWt+oq+gr!lzvk~-EzcLJRcxnxFy~hQgJzPnK7Z}mdhB!xFAIo)KU*o zl=-qqfM_#idc(Ps=fWF0dnRmRbJRg#Hi1Q>(IsgsfhJf4Xm(Rfv}nMJ7EE$<|KKXC z0mB}kW zO~sGtu9V!8@?^`EF<;h98M5DpnL%$B9h&rL)1^_LR;@a9Yu2w_$F}^r^JCbzW#`7d zTX*ldwSgPFL2u@!sBVKNG#&ss&n^M@`hCvT4X07pS4|C&6f9Rpd5>1zF0lIbQC@+q zaudMB__9~U3OvfS(sk=z1%977Jb8bDzx@mDx#)mvF2Dm{n#Z4nrU?L!01^wpk-`X? zN0I=X3c#NxV$j2s{3x`m5(G&!h_@4cOA$rbO5|xp6=8f)#u#aw(Z(8a|GW{!7U9w{ z#~*zR5=f4Cd~B1L2=wpBgOY?xjLx{*4!-%U;wpeu9NbP1`MPsYtfayMMlhSE8|$d} zs$3FGtFRnz%b40K5;6egwChReaEhjzxALmU-f9d5du7P#ZE8Sz`8m?umahrXeKCaQ#m+9N~&Gv`F+X=%)ewY@lziUAuyd6T^Y4^WwtPXxS zdENn21uo_xkb%x~U;`mYu;o3Ff)RXC0Wo+jZ;0i3+7qAkCg(l!g^+`5;vNS(xH-&S z?|d2T3Hw|aL+L$gcK@@VObR2Y0P=58{2PeZWN5$&4v|!BBjOP)h(skOacvW1;u4`a znIKNFL}%(=`M9$=#!>EY+AG}6xX3;98IFsXvl10w0z)*ev0?R-pZ(mZH67Y-e{QT} z4ZDH6Io^+ra*5&}cciTa0+NtVG-M%%7rjF+5|J+H|KlSG*Q@QBsbhjEBjdzk$uCk8 zh2;BQ2{*|?NG3#$qco+Q60^S@8l*6U*`Gk**r++y5SFsz<0%WmNL`XffsOR#E+qrZ zU*<**cH_e^g_%TLF7qu`#3bUHL8TLZafB?qWF|?7P3}Zfd^cmJ@J{kgal++&J4w_H zVL1;y%CVKTt7Q%cqC-Ww6PL+EX5FqeNPF({m;Cf6X69fvKjcB5_xz{Z#+lHC7>1dnMW)p=TMG4PZQ%II3TX&ZbR=mHD-_F+uCy?_TjN0u%8^TS zN>(tnX-scw(>>(DZhI(bPH~#Hj}Da}+&c<4|JRf`qgGO*N0la1ABsiqozSSqVrD5b zSWcq)r)&Kyh)4-COW1YIl{7SIx*nAnm!c7<|IBAl+X~3I&J`j^qw8JiwbhSqaC_sE zW?3u=*s3BGdxae#H&+NvXm0eBSk>!eUm~bC-p_x5s$(kWk;g%)Ql29nCtigafINW0 zQ$L04X)Op^)KY|Rs&y@nAgj;~dJ>d3L97YilRd;%@^U4FXyUkd+irSJoQ{p{apT!2 zIp)ret~*GGa;L-0&aQ{K`{rwRDLQbj7Q4jju3OD^%F=o+lnv7^- zCwfAk-L{h2gV)n?{=8{DY!*0;2kY9{~6;QVfw#dYi|>>BmHJhpS5uDhdiZFykX z$&xw6&G2dEG~pjFOvs@Z7L2D#_A{uyJjz~CbA?Z4 zZ_lf-*$HDv)sTJ(u5p{G8|KFTIi^L@GpSFV>X7W= z$VOE4tBFMEM;aEBjU%5XNnGW||IwGq*2D5|CGBNd$5x%vHL00l9ANo5SF-;bE1Ba6 zXIaL1nXrDIsZpJ6YiqkV!Q1w?hyBRG7S@C84RJ<4$ZM)jxyr(2?5KquZlb#QhCZ|* zEcw`JJCd~6a&&eayQ*Sv1CK-l=eCXv-atOIx>j~R_`^pdZ#FWt*1;Y0G_P#y;40SF ziXNfBRVDGq4mi`D^(y}Y{-`&)7{KxqtG%xaaYPne<_WL)%{@Y!RrT5CuogKQK^ewq zEbe^f&a%_#w`HE=K_4clgg+Qv`j&D%_dle?BzJmz&P(%>2pIULnvo8%-73nn01eZt@_Y>Ld_f3jqfw zF1}$1gAgHn5FwgjtrDV27Dow9Zy=(?Oma&D>nn>Wssis0b3O|FwD2H8KnI?#M8Kf| zqvM>u$^_wWkJj(aLJGiM>E(WC(+VQ;hR+>daCTy_fDBQnd@2Ag;Ux&F7;$hJ5sn!f zDG$~l$ehvU|4PvX1_um!;=U9D97G@hw9zkyE_--|0R`hGe6SKlY@7ayuxJLOz^ADe zZKz1G6ba%RCZG`yLI(sw5)~stW~?COkMP<_rGgL5JZ%ka&jjI6cJdDnDZ~pIvO#=N z2B(q#lJQf9F(WIo^GZY`RqM-)z#xF48T2tBz9A$Ff`A6%5eb4MMUo|RNhVy<6a_*8 zn?wdUAxX+$Oj>d~P*Na}AWB#gJ)(gT10X1U3)j496g`k0VWK1t0tukw8>Dg|bY>u= zk}rO8AXHL#2BHYv3@a0(vtnr=T~2>sY^+?VmQqTj+-X7FVGez74k0ow<1o4Gkas+C zZ|Z6<|2fYu2Sy-(us<9QFkyfC4xN;v0xVF-=4uj6gD>Loh-h z56S=uXe9)+5ezUB_zvuqY>)6{5uR=dAe9Twime6xVFe*?8io%)>oWlIQy}{DH1BXD z1=KJHv_L_H;ednZ474JL?OXN$D9T_#c+wFGqFoRoUm`I87=Ra=P(lX+9L}*xKBF6S zAOx%vaQ`Ay?+79U!f_x*!Af{)=wLDlF|j7SX%D6$B%gsSf-)p|0q^W?DVZ|fobn}z zqZ=O4Ccoh%9fBURa!Ch*9-uS?645jr)ElhN1qfssx>6t}pq02Y1*uD(_K23k^7rDY z-`vuER?t6dX!z!kE(f9)gHIS2ltA(HPWAL5j;YUrBTsYi%gB=;#8V&|gh0G=A6t?i zfiOA_bsz+z8$ZP~w~|mAktY`r62GAn0$?W@P%LcnD5x|_kRTeQ5(6o4Aojo#8_ICD z$~fuK8xm1jj9^dh%Q+QwAqEvIzhOlAq9iBuAY_2Jz+g|h;Th6ptg4RX%5;v@)W*UR z7XQ5}Lg2|zBku((iw^ISA^~+zy|r7x?N9yiTlX*AzU3Q4fJOJwNBzbodj~qwRauyn zLt*l60$@+Vv>@VjT?0U8Oq50^v(G}Z6^IThbR#&3*LAinWlRn;`L6CuiJaCWr})ak1TGL@orqzLS!c*x#pM_KD{)8O=W?2|ql zRlv?z1c z8&gsscv5oi@q8HIMg0a0XyYcBHgd283Bci0=R*RMf}6Z{#KHoyV3qm|^5=X!T&hP68NkyDk$Piu807pECQH3GjPB=bO7rUDoqqExSkr)rbv$nCN2wqU@r zV4`C+0{|TEkvT#j1b|_1LXtL+6)2^`8@iz?Au|y}@D8PUo`WFoHzzBM?X$fS7zcMj&oclx!7A9;y*lQ&#S+?CrRvgNi`Zu z8GvA+gE*ixS05Tc3F0OoaB+a4Qj6*vx)q=$GFWxAoE^jqkG9!@hl9zqVRwjNheCv+mDy~Qo8)~6GZx$Z~=S&DYp>;zlK zA%pQQ;WYp96KCs_kzcSd_StL>+MnV2t>rqdLxssmwyk>tUH@u>jo|`J_F$Mh6+?~! z0wYHW&C!dx;e{(ll0Zyykc+3mrF@x*X|od_GIw)FnCG}BRut*76H3GrGgqSpYQ%2Vi zN53Fjwud%2K!qgua8kCcnDlxwRKc}^dN4!?WH zd=Da95dT*2o;f(Z(fh(JJi}YtzoQ5BB4=>E2Yk5%#s5Z+i#aKxSZujD9BJe)tkCL} zT0CbF?`G%O3$uI2Vz3V5GEpQvheo%(9}L5VT*HZc$d@nuBwKoP0$`H@9m9k#JPdJG zNRo!LgP2UkSG-_Qj;kk2mB`R`3WJ`~dUQ!jyZKY)>{(6?HIj4JiHiKqjU3M9oV>G~ zbacX{o#pg4QOcpI$~W!`ue`tOJgcs5*~pAN-SXK81wUEPbK{Us=g_okmX`8aTLD** z<~-BmeA6`@lLvh#k{mdqgD(I!KERjaatqbVXxOm))3xP&>TN7lR;#J?X3cs*g0FMM z+jd5m(RVTbsP0~Y+)p^Y*gM_Wk=>VE-Oh&&6aV=_HqolJ zDvxY^1?8D^)={jP=_kyvA8cyfxSbNq2S}xj)yuL2Uc2-hJZXo#HM2ZT=l5=v`Y}`YZNZ*8Dx= zd57R{whbHEKfk-+3nDH}z6Bpyl4)q;b+F=T{^D)E=G89nWM1SmDCBv*fbLJbU;Yil zj)MC$+#%%+=@S3cy%2H!=Ar)SrJkRDULkhA>TN^Nu^yKkey!KsKRdT`=Wy3uFi{{f z%-h_pr+)3DzU|rmkG4MUwTnpQ9+x5y^8Zr4koBI1>htK)eY8vW?cILx1;6m;rtS&i z>JguH>3;D?sZBMc;ltJ4?^*yPxM1S_i=ysDPtAz8ufu6F0h1XeLwiuNVMg7!D(n)zEj0YthJ8GvMflhfQSJpZ|H-n50&NYNPCbv0}@TJ!>{CT7qlUx@`;ht=zbD=i04{ z_paW&eE0e#yRfg|ziBH%S`*-KV#JCYGk&}{G9;md2V1U;`7+YLnLA^Z19agWIea{u z)*Lk=D2bRHw%&*dl|GI^cqKXawz5Mv}=gSYuxjp;+`up?$?_Yob z4k%!O1RiK$f(S0CV1ovQrvDLm4o=8mg}p5VNJoA=NTGlWf#L~B^^Is>eT?L1VTv89 z$l{7Fw)oqNGQv1xjWph9V~#~ZXc1`~;s~UULJqXY9)QR(M*w$JBv6pf1>i}EQj$pJ zlvXm-t#77oNeZ&=0>Zz$>*JY z{^_TffdWeCpoSKTXj=hPVqKoIB?{@GmSq{KaRFFaWu}ab*|but+d)^>;G3nn`#QKe4vaH9i`iz%PqR<4!3T*>Avf3sL?5QZKv?w8!x`8 zMf7dIBXXv1z5)kK@LB^OTyVk&Hz*Kj3ppkkWDrjbS%&5+jPb&!PGswQy?*R*$iIU8 z>m_zbm~qM)udH&*EWhk>%yIFtp2RjsjPt}k%FHv&ZPwe+i9h<>bJ0W}ZFJH|FD>dH z4^4M7#W}Z%BGXnYT_HvAb&Ltu?|I$zt*C?u@YQBxeRhXwqs{i(`98!Q)KkwrS=DXd z?RI!ViyUkK@G*qztq;X!Z{CLEjW~0MFP?bgp+3zWt5Qo{q}_~P?l@-NX1WlksK_QC z*_mTb`skIFj{mypsn@v2dJAdCSa)(uouTTsuTGe2CD!`)LcJf0d+@gpKb7#t6OX)y ziOC+fwXZ`Qz$il}PyO*&UBb|=oogBZ%GG~wJ@Mg>4}R7_pN~F|-gKjqN(*7b5d0B| z^vyTv-;aO(`fJ41{{H_DzyJzxA<-B~0CeI$_XWUy3moA2$OpmlB`AUvlwf%rh`|g- zq#Fv+pa(w)!m?E*gd{AX3IFGfm?&f#Iq9G`=y5_dU95sMv|tTyXhTHBaEJBkAP;{C zL>^%!h(s)+5hnsehA{Am9>Jj#bNIw4LeWV~tRh7?XvHjQ(Q--Iq8GmiMkY#ej8i0| z8O!*eF#oD?jcokj5ZegHILZ-yXRIR{?TE)a+GdV??4ut+^2R?3a*%{9oF4Ot$U`bJ zk$owoBOlqvKtghol=LDa7m3MBYBG~ysiY@ASw%^La+IVj;U-VXNmQybhox+#E8Uot zSITmh|68RkQ;Ex4Mv#`g>?Ivz>C0dWGc>x)r7??%%;61lnat#fD4Pk*Xc{t^)I6p& ztI0HJYIB=i;ifm4iOp+@Go0jX**DKA%x9u=ouw?NJI4vnccP1(^n9f{>xoZD$}^t) z+^0V`md}7z@}2@MXe9fI(0?j4q0=&`LkUUHhe}k93#}+aEsD|bOmw4ij3`Gx8p?}` zH2V5m#!{oWgRZJdLSOdwLn23e}~+9I8=6 z2-Ke{HK|OM%Tb>SLYzW%s_AR0RhNp@t5SukTos^Hy9!nrW_7GuC97G<6V|li@2hHU zYe&io*R#sCr*5sQtJrGORxGhzdgW?d|2o&eW>l?a7=s$@nkT#>cCirwtYZZW*?ls0 zvL8`wO7}|HpFDQ6k?m|XGYi^`ST?kX<*aEvi`rF^cC`~3Eo<9)+SsOcwuzi-DKPum zCKfffua&KDX$xF3Qq_mN)$LY`+uPtO_qWVNBXV==+UG*#usLn6b(f3X@JW}o(f{4< zQ?Co&?20#a-X(2!%RAKYs&~BXJzQArXFqVDx3RmOZ)w|$-}UMjYxbq=dHY+_{R%k0 z1cn)a8T;P@r*yy!F0g|Krr`S?H^Q)eu!S3p;dfEEt_j}oMl%fJ3yXMJ9Ub8mO4Cge<`9f~C zX`AZ|%{c3M&U_9ko?Ck7KQD97gzmGU>mq2D20GEfEVQE!4Qar%k+_VG;s2s5&C5q? zI?|jzF{UMY=}*gY)1>aSsfi-$4`(^m`7E`oPi<;egL>7nrsu0~4eJ5d+M%J|H7Icn z>|D#)*9Gl0u?H&bU@M!-$L=w$o6XH-OFP*>hBiKpZEZ(Rd)w50Y_@Iv>~H6}+vMhU zh+kWYN<=f=Kg6~MTXDp`QkLC`Ew{bR4QX+&)7|>^BfkHAZ}HZv6rtS&ZUboWQS!Ur z9ieUz0S@ti!?a%D=mHJo>&PK21bZMr-zZv$>2^Iw4>yL~<)H$-G0=Dt$02G=;)4uj1V*KG{ z@4ML@oYS|<(dcn>c)jS(qZ{Nsg@%XvwFv*~@?C^7ey{xBlhaBXG?39Te<0C;h&_sA z{eLH5XXiOm`iMXtBB_VS>H{dej8I_mrYoGR8cvbF$bo;Zjv-cMfyf)3BMZ~LV?_l%> z`HzTxki7r>d@Nl6$gq*^*B&C2X9^d?e-{7Y zD|iPTAa*N=hwm1DR3&d1#D}1`jyrcZ4k3ukScu9vkM}4MXvm7103P475Vo=!*%$!4 zB9OFLJ>JM(TK9|PSci8QKLNl3;82CdxF(-Sk(9JHfmn+2NRRgzh#IMb4FQB>_=6D< zjniX)*#io(u_p|%k}WAg2$@u2Gzu>m0Lf>E1QY-@kd7BgU&Po(GDuJKS5Id%ff6x@ zUdW8HCxH$@hKG2D41s}lLSC|wE2sb>SeZR6DgPVeBa;I6KRrNm3o!~KAeNzblS$wa zQy2iIP?K`kla9iW_XBeoF_#RnawK$fH|dJdcThmdM#<+8dFe@eDVPpKn5|@#AZd@Q zsFWbFlv3G>Q5gV=c#@2mHvy21UDG)$iIrSQI3Y4a;uDFnRzh935#SJ)KFK5&Sw%|6 znt;SM*|(9`SC1Zvo2AHoCV7Fa7yyn?8dcdf+K83+!IGB(lV7Qv8ns4C5=5wZW$nn9 zua_r88JibzH5`eXj%kk`fsYUI31gUn3y}(`00^UbIEKp%4mkhC{6JbAU_Yituof6@fA4PoQ2AIoq1HouQcZxpL z_Yi){5mi?N?UXc+iKwTjomYB38@Q!pNM4)yqhR`vzVkT(83-tOJ7`)`MC5+6^r`S? z5py~~d)Ws;eI9l;c^MKZt>nK$SO$ zoU*8yej|{Q+BY4_tQsRrNavIP*p+`8#`XL_UlIy9d zq{FGR^-7QUTqe3h(mG%4TCGI$5K?NZR63>Cx~u!zgAievI!dfR8jAyo39CV-_R_2g z`!-IBKLJWqdCIVpbTmy;tGJ4kx5=*=yM46zl)V|ALC6Q>g`CG(oLkeGWHY1+%V!bm zO7P0EJH;~!Xr&2?uh-&?IqI9yn5pD7oX9z{qCl6I$GUN38?5Nos$RWzX?rS<42 zQE7$@5uBSki$2;KEBTKeBmbI2%eCv0w4pdcU+YOoL!%fwvl`odil~qJh&p?42?y($ zR$H6^yE^GQSYkW2O^Ub1XJAarsQ8Mu3)+f~nt_nIp_w_0!*Z~POSN{}S$m6YNDH}h zIz^_FAuI8@T(5vto#b+VvsBX)KJ3u3d{}Pjv%H#A7ZyxwRxSmdlu$tF8Kq zik-`#`D&x4J34!yZrEd{P~^B42%Igq{*P@{To%cu&k*H*CK%tDyDr36c;9`|HD!B~9w^lHJoda?PcCMAh7UHrqR#Zoi;czLp?0+ghY8@9Cc4MHh>8L`J4 zG02H0#uZV>_k+ha*tb=zu~)pqj~AsK#@LFv^*hTA!3yPqAWgCco2<#1OkbWX%&PTNIsp?ehRSMO zRTB)!+H1>@>=62UAPcd{z1%8RfEd{9%bZNd-OMdqCCVk1%*yO}eM`Z!%bh?W2V1EJk0r=M_}a=7vT;w5y6u+&g6`ueM`FsozC}aySR&g?ffLb ze9wpx&(z$@ud~ThaIQh-&E-VUN{hlBtx^+Qy9j-|*IT;~60*}w(G8)^5K+60aGnSO(y{!}JMD)e?YD4z()DH_4sp*%ea-W{%@s{GX(rRCv=Eq_1q@LI zULX)F?ElkNG|*YShp`JGCJi*wtUU8f)<_-EFm2Rydt*<{Pth^e3sD7cJ=Ikp2eAB9 z?qJtCwbfh=6qUOW%{;%GE7Tt$)?oHJWu48f^3q1F*e$Krj>XoOR0RPr*Ka-9lpO$* zP0EuM&Y`u}d@Y+VL$M@nt6;s*w_Mn?^2x$UC{v`2k60Ekqu99{niVS z1(&^D|GZkC4cf-#G89|Or0qYtEFgLC+O1t3&du7h%_^*d%~+7qxQ*RRHryD=#M^yB zm22Ez4cKS<*vf&)j1Aq+&C-dz$<8eYh;Z98O;^0_+r7;YyZzgFt!Ult-KMm(r9GpW zJOAFD%e%pm2UIiB6rJ8ojo8oa-~}$<5pI+XVc+x(*XT730j*>F-QP&{-vD0H3VlDR z9Ufl5-VqVq4Q|#D;m}%@-F5^K6^_}MorfK6NgQtD9z?cFmqrc|&nq6=CobI&zKAa_ z;mqO?mp#`Hk;v=>+!!o5<5kX%JtGjaJm9ws;TM9@XwBMBv&|22)(GC>V=m}Bp5~P8+q@0uUnJa# z-db`Fh;(k+JaXMVPS2~I-p{?|Tu$Vgyv2j=>4=NynH}n)-d}4DRg2E(KUp-$J^#{9 z3p5G><_^B+vn|`3{0JKG2zgHAh@sq{?(0x0>T-SAaqSS%px>!peLaorB(yZ69pJ5A z(se$+uujdge(676(YM~qzP#rPQQLt|=D$uR_f6^&5e?CR)yr;mZ?5k9)9iT=zt2w6 z(hh+Ekmb+8%hYY{`+m)RKI>v0?g1YjG(!0xdKw|>}szU`U3A$Oi#A-_oH;RU3=*YJO2^#K$DBGBae6W$Nebt;cpWqP-vj_j~VelGfGE<=TD` zRAU9_3G#tIu@+9%CSUaf(GB*)_8HO72gDGK-w+J23lU)xFOfb-Ko4Jk5#FHrY=r6> zR4$1h0E@2?A)o_q9{{PUny9ZPm>+dbGIGseewelT&7|{l4&op_@eG&;lRiN{-Vi^o z?X8XA7*EfLQ8^kd_=43snO)fwz7U-cMhuh(^q>$nzY!s@5?wE^G@(D`^6(g;1JSQ) zG+#;7Z~ddM$`ine5y1d?CqUr;ez4yW(l1D6L$SLL*a0923<3~)0{>iD6Tsj>gjUvA zShL28l>h}HIt(xoV#P-q9!>FbP~gV^AVr2GInrcFlqXfDWVzC1OPDWZ#-usZW=)(o zb>`%`(`QehKZOP*I@D-Uq(_w|WxCX9Q>ag+Mg__zKpuouvQ+VcRUksJVa1LmTh{DZ zv}w~K3GnTk*KXeo$^{$3UB4Kx5WpI(DT;T zAU(Q%%LJ$#U~^~AY!C7ZOL}ksa0W$#MM8IT0KaNw&!%15HrSiDb?@fg`ygAlaOvvp zdpB=b9b|zM4x~Xj0-=F_~{{jSXKJgukx_UK>2F4k*u$b03*A;z({T`gYt+gfYuI* z4w$iG3+n-d9E;2|Vh~zv9%KSIEG-+J6bvC3b_~DvVuGMMFR@w$o4F0m=5OCV^=itFg&;tq)(fqIS_V*iACK4=n2tD+01vH&N0s|n|^PM14=-qa~V)ylV4Wb>wHT<>vOKYDe`Ya)KTJ5iznmpA ztf5n+vR6aN0qHkli;W7cq^NIHCIlkPoc{!km^>tIt2U(D&FYSal!G`#UA7C$?4;jiabxk{nLw5@JwZlFXhT{0 z0-!SyOhaWJLkJl32o?rZB7LJc$dwTC9>937Es?8UTcC$AgG7>mmO%n$+{2w#5T^_- z%ue6(q(*B+5S5x^BmWNa#x$W#jfv4gDRAdJidU)-Sw29(SoBDfz(@=%$`?SYY_$dCC#Ac8p&2}qz}X&@L`OlCqV z8n5vavhu(jG%@fi*X(DtR9V!9{gauOL20dw^e8=nMGgsF=wNiPzfKxtpmX!aDUEcw zu!wN0t;`%QgVY%xUXGE>e3UcqhAF#DicBFO>Z;_@nHBc(r(Q)Wq4udyaEgvU-x_R< z@*oI&MlnLKbEjg(l0}Im?5~s6XHh77*~(&8vzg`WO!}J2NHNrHy-E$mKqIyH6bVz4 z85}|qW0(3FWE0I0h#dTAnEyW_WPAqM1wumLGA9IJ6ITfUBND?Lci3fP)(b&2#6}ma z~f-R0fGr*>2<4(B68+AdKMNG}hY4 z!fb*8d-(E~!lD=KO8@IRJPiPPDDxxGY=#qV150$+*j5kb!3cJ7lT%-*r|kqFrB^Ah zk#}=cM+Nob8{Fd-xAKf#BVdIf550_yuiBC;Tn8Y)I`{_6kg4eOq{kn(#iRQM z6CIY_+mPc6!Min-PKi%c;;R2n6HNn7}RRccWxJeGt)2E8DVUgv*@LI zgJ!X@K{vjVwESvSF1NmLL(LR|iqRS^W22T`>;f2`w|wR0Mq*$ETcESE7>}Y&$2@Y^jhB)G>%+Va5bkIZt8u6nj z`tazo`ZXhcTwWD3Bf}TYBcw2!-(zBSi>zL{rE34H3_YMFwz)7qH6hGSWxTQa>&oS6 zu9pk!uN#d%Wrx;bAN*|~UHnHA*YF5Iyy6kU_-966E@<*b(k=szq&XLF(^M8+qzB=y zM*kq%$Nw(wubUdWAVGw^WLY-~hd0zd2XoMJKla7cXvbmBAco(cX^y14~z#bzmgm{p3 zfk0<*7lYsf=~9>=+`uWULMpsME6l>5_`&&*HB{oahN_YLaseTkK`uPQ=3zNBltC{v zFZHuN_;3L}=s4nAo;-*R9oV=vWJ4^(7G>Lmrb&yYAvd@qh#Q-+2C;%a=tC`RL`Hl> zNB@jOWZOfus2Ud3oDw7?--rxBDx4Cd#506JP_(j7{H#k%i|435#0#QQWS(z8JU&6c zQRG8OOtvB%!mzN4838;Ov5G99F?zwaD*yy&gG6J@kYh~7T2w|xoJD8sjZ=I^l@mp2 zJh$zO58tcC7-YuY`3OF^#bs0)l7J`0S_3SYgFle2Vhl%aY)5u{M|X@zDAGm~%S3v- z$K+E(eB4KVL_>M}M-_29K7hh|0Z4=FiGxf?cwERJ>qm#Qu7?ymYK%yVtVpC%NQ`7i zjm$`n+(?h?$fdAIksQg-nn;p7Nt9$5ko-uNTuGN~Ntk>|c}z)~tVw@DFPqFso&S`y zz>tWAM&%~h%^-2}j~>`ma*Mmg9;+`LWW zEKcM+PUR$uRp8Cwe9o2w70x_~=7dh`oW(&{PVG!iz~avB{7xaePVsEHZ~q7|)*Mgt zG$Ke?14uZF;hawIj8E_+qxqCi``wvJ9L1E%`aID4Oi%<} z(0A0!&MZj<)fNY3PznVK1zAtBU{6)4P!5gI4*gIM4N)&zp7%*Z5j{~5Eh9bv1hZ&Q z2TV~EjnNZ`Fa=XIJySJJ)7o4Z^;%OnWz&-g z06r)L1wn{P;L$kUQ}5(cKb6zBG&|7xQ$j`4CtMr&EL24W)J1JnM*r zSANA-e+^jD98iKqrJX2PgqDPTD6>7tu@gi+gh!~+N%v)unkJD z-CB_?+m9Vvv0YoW%}BJJ+OvIIiEUf9ty{XCNx4PZxXoLHy<5BeTfaRV^J#hqNq?K{Zb*~h)y@vL0R?Oe{COwE1S%ne=Z{9MmHUDJI# z(q-AvUESbJUDS;D~JK;;ec9ntVjSny5X!8~8? zEmYrh-{N84zLi?~-Q4%h-)YTJ+vo%Rt-|{~T>A~+(*0lg4b~$&VBaC&8abx0Twewz zO9eJyT9tz<5EO8l;DWhX6R?pI{oVnNVBpkX3a-%r(B47GI}#2UmW9K?h2YHeLC9U< z5`I*pYF-)!5fM&L7~Wy1%;6fY(4@lQAg(MQ7SA3&;-v&)CPq&6D&kI6V(3g_Db~p* zzGB+MSSSw9Dt=BX?&6xnVlj?Pu5r>a)?qO2%`Z-4lsw}tHqIH*VmB63^c;l_h>siS zg0fv>HYUkAeq$Te9RK}e4C&+C4PirWUqB}06aQtUMcy&}tUdzf<3z4VM!w{hYZiG9Obc4X0N?vXAVb&?NPA!#Q<1dYBmev4Qk;be04Eed(EQ=!Guo zxqN7)=FoFOVQ$jtv*2m55a^1QYKuNpqDJbdlxeNzLYw~Tax)?`w$D37X>>jdidJWX z5a@&GhsZ#}4DWp6vO|Yopq0n`LXM=INA9X|TBHv>xoW4(%v3BeX~btoEG9eoC%hZR7)M z*d8)KJfi!|56%AT!d7c`X6c^>?TqHy;mHq8fP_>aZsRU)1CyH4qH5Bh+=IDk{$WCvOjBWSk;^lE7x6F#Q z?rxRdWx@_^*%0dBK8WM~@8k~f|NbKO&dJs;aASLK1&3tjQQ`QkZ;TFavFL1{-tMyS z0x$SRDTRdPCJT880MtGU9qSham+bW}@rPV+70;(}Cg%swZP5nr>~86auJDQ0f_%Y> z;TCY@HVD*CZg~K3dYza+rKZb5GuSO<6^Tt-#thHugo^q>3h&M-zc24PEu#_9k;dcSbZnbS{EIC;!jdotAJJmvY)*>oy-me<_O(=W-s0gdk7uJrDFkk4Pl{ z^ld_PQXlWGd24wli@tXBIHz*82yLmRbRG6`g!phx$n;DP@;&$Owc$jk8$meb{Xev-mdR;q+_x0c7)J(u=sK1K9?#bk=;P=*&z6G&)E|{ zcu*{Mh98)LyvZlu={6^5dWZ9gw|HisakVaOd=CqM-*Q~XbmCraPlw)0sDy$I_4JJa z6GwNKKh7ZrbC=9(pZ|{XWw-e`SN2x_#VptYe%f~s=W?XJ z;i4L@4z9@oB9zQO_O6|Kv)KHy7=>0)IwgO3)4$D!CluXL(7e`fNdNo6k9Y8HXR0sc zYQObu4{$%vcOhTy5zoz$>;d&S0khO&g~kaZzkJQn1G!i8(|`WYO#K_lgWC7brlD|0 zxANO?eO1?&wEtLqvM=uD3HOmKf)SESF^G>dlkSA~d~>&Z_)ih(H>?nSXY9A|cfXCQ zp7NNvZh#1|MHDUdweh zZJ0L#c>lv4Kuzu5MD_T_`AvnHox!WBJ+6E?^XASO-p-wy_w%i>uQSe$eY^JV+`oGd z4}QG(^5oB(Pv2H_RP^rEGltx7GUv+mBX`DcUo&RQ{_BJGnM!i>u@H6v{J|P)tD(jb zg9>5<$b0ft7n5@moCLJ<#f_1WiSfYt1PIaLPQEgb;hs#X}UyL)#SR;)$ z+L$AbJL=dYj|VO!hZk26^`nnRnna;~0o=!Aeoor=WPeQZS0zhX1jtZg=2ddqgAQJ_ z5JV9*xZ0QjjKUClC{}pkO(3utXNS2hG=iKBC3mMwbOvB&OhxD!sGM-Jl;It2veXJN za{n6HD3Xsl8Y!fcN?IwUms)BUAAJlYM*u|Llqse%9<~ye3(<$Fllg&`YLu^L7Hg}Q z1;B@a;ROK5YhqI55=0rv<7-3@`>nX)k~^-sk|vhNA3_E&M;2aqF-HJ-BxY;5_wJR_sVeO&AgTfGn<}d*O*t?_ zlYv*NOATgZ(SjJ^dF-|mQ(SSy(^{Od#v7lAugB+t{ISR(lRUD?ln#_ffdY+ovdd(# zr*D??0bKLJv(Bm#W;w4q8m_hqduBtyKE%*Qn(S)yksLGKw9_z>__WkKQ8%^KUH_Gw z^~+mp-8I)=dmXl3w}M?ZNd6J*8BY5a>=4_U`OKZtMH3C-!w?=luf$jL-M8N`Vf?q? z0cbn8)Mg_tw&IC1zPRI!L;iSq4z(StPp{gnDwJA`M`6+n1*^&3V2VC$O0QKCNQ{QF z-ulylyB@pj)ka>sWcmCICczi&rIAlcyoEmP>9J7o7oX|9e_ zH?y;uCiZ;8zPr%`RALTKKf^`%Ab_(?`OI2Xv@@ZdIUKo< zz9hX9o~9Cls9po3#fTO%t$Q$>Aq}T;!5daEhdA7!4tvN$xlry&K@1)UiwK|)zC?gP zI-N@P2RfFN5F?17(F|)MCu_u%ScAv0c}cZv=Z9ZIG_1t z&R=R$BJ*PMu+j|;HG=@jduZ4LKHaHB*4hNLTDV0oR;w8d0c1*Uu%ZBrfsqVJ6AJ<$ zq6$?FfoC)$CpYQIPJ;52nc741+}K7o&QEw*s@(BFL&pOtX>I`!-9?V}t6D+=dRkiE znhpqpOk&F?itNn`7s$m!GN%Ut5F{dbq6bN?qz5!8oI@@FCoIlJng7gG389uUgoSLV zn;;TpD9cICbDA@q=v-n|=tIQz!7~5_a*t_%l}qS7?|-d%)C`pgO=liu6n=sUHaVs} z@FBCA86)IE{&}ZMPHmm)oTx-AO3{m2)F2r=rTk2(N|*#}ReOsbnHu&QCz3EFrr>3c zN>os$Ft90r3T7Y!up*lJCZCr0%s7VBR<>X>Hw3MPSBeXaSj!cv$Wpc=hEYV4aiup9Q5@i#WbZE4KDpU^1 zRX0qTrk;wr5};5Fs)Cg&VFzp2!hVZARq!Zdt@^K<(ad1;^#74p8KO&Bs$@%LrQwV2 z^iqxev{}r_BytjXSDEOvHig_Ca3*%x*CH0SvW+cmfz;TlT25fCJQzG?&Q0&Gk8*Z3uJ!HrwykHoV{+uXxAOklS)} zgRsIGZ>yj@e(s^30T8axP{;~D7>R~su*p#jwOX9;X`l{yAusgm5b83Gs4oFQcQ0nz z>U;>nr)98IZj;*MsE8)~WgU4tTwV`@_`?|81D336;@sx!KJ4wx7AAY6EwLh(!?LIJ zSizqBUN^@_`ENsbY%S~R7{gQnRENId$7O~$&=d*WjDk5&0WK7dZ{c#$UYWTUSJtZZxl;f ze%UGJS~H;$`rtw%n!|FAGou&n=tjTnyeCet{oEEu-zN4}l^6soDnW}%_+b=N?6Ywb zZR*yY`P8a*;Gt{PV+R(O9EKdb1Bi@--ta|6PN5brHYw|4M9yiCNZSHQP``pr|%+93b?p#~@ zg8(Cgz0%D-a^u^zht4;@k)>{X|2yCSANWZ4p#Lz-G%4ZPmba=flLyri{MfDd#!#{{ z@t0h@-%Uxj#+Q?EjSD>F5f?ehCn|@TRhr7d(;x`R@DcKUTMQi!?7nAi^YD#4=Oy2{ z&U?NxnQ2bp@XnbWAnsviSN!JG;yAO_s&wb@JnB7{I@NWDRXDy|XL{3Y&S$68hK!;W zNj{3qkMebcihU`=+$KxHK6XacoY~rp=G&jnm8#=?>Uz(6?j$Xlzn56W?skYD=2h+M=#l7Q{ zzrAmIKYZ(B|N58$F6g_@85i%K`XF-d^PS^*2Sfh$w10`4&M*EXYCrq(k3aog7BiiB z-wS*={P5pjA-DE~>@CH1p<48<8VD4@;;DpR`JYzI-vicP1VZ3-0GCR<9Q`#QL-^Np zxdaFxgGw-;RRq{kP+08=6!fKEzdh21WuWCr;0sP549eiF5riCQL1yt^qqQIbISZn= zpT4Canla!GF4YVgVGJVS5$ec5?4Ots1g|+85lZ3XiQ5!f;l?Rp5^7-7pB| z;xU5UFft=}3=c4R0WbzYG)`j`0HYS{nRpfBHa3+qwqY`QW4>|Y<+TL7L}P$tV*qG@ zHBKW#Xu%BzTVai3Ge%rEnqe;5qutG8)wu+`tm8ub<2eH4G)kjGl*&HRBSHdEK29Mx zGUT@@WF5xDo?+uc2&6?8B>zET%?(9lM~2Zu${{|2#rB(t> zSAHd0ikMl7pF>thS?VPa-eq8ZVN_NDMY5$-`lLI823)RXJ4$0+3T9;DrCt1`Q{rT0 z9@}INpOCbrI;JC4rX(~rraw+&Vglr17UpPX;}&og?r7#~&KYLD1TXv`nenA;1{rKV z*-N%0X})Dp8m40&X8&n=redC^Z$9QE>ZW8eX9bcO64Yk2lwX=4jQ55fpfJn|6tav^?mmK?EO4@W9}Tk;qc8_%S_aV9in?G+G6hpy>IAg zoG;URFo}O+2bi~&SC>bKhCW3PN*EkPxZ{7|MGsU|2i#bz!b@EtvVgZx%?=K!emG#0 zLx(Ii$%P|kkmY5LrCU;Y04{6PD63!Qu5nE%DzADs%Yr_WN{N2F`%+;~<83$kjh)(V zP@o`|&k?7**{%+xTpqk9hrX9*N{}S+NN(W(Cy15g9ur*z?4-1yp(lIR9uNxG>gs`A z?8%%!KoAI1SGkjHc$CGKs_BMSNsnKHLsi;#<|LP_K^Fy*JtK#U1B1$kdLf1yE1phY z^~vRHLMP*tq$nx%=j-kN(B|D%tF*9_vPGz~_>ekB>bKIC#j+d}z%*ip?Xv-jsvM^?B=*rdU2e(XDBOoFW-Xb}FM zuuuTR?I8QR9blDl_b}%cAUXr1hJX(p!@#Pq)JCPe2y1aef*#i*9Cj{w}Giv8s=1^ zIyX54Eih+!G#oCxSv6EsRSK1*PwA@T{T=?JGCz?=k68WbqJwq;ivBDDc?OXoVB;p*O-IXPFqQ@E^ zPa3$M;G#;N1y{B)1Yrj=Ui`v&;W!%eh~ zF@rYWcNZJ;g$eV4;)1hMY8&$C1O2;UCzN@-C(G1WGq@F*X3nZ7FIpt(-mLzPG1Y3> zPk{w>cWJl*ijGk(wI|el@Nsz|Payjt4%*hyb55M6z+!j{XO=sU-PfMJ5<8vsBO;ou zBZ=~6(NN+dilu_6OToPmQ)S@`dzdc>`}|N2B2ZN0R8Nt#`8||ac{iM~UiC0kQx@+R zW>hZbZWul(GnoTQXvVcE!9kTnt<=Aw=({T*LEs+hS|@5>;%z(XuqUTbj(1(=!R4IS zP?c7q?(igwi3%-{{IGF7g%S$@dP^(s8A9>pxTk;@Y{~TUR}EprVU)`u`xvj8NlNbuhEdJTcJu;mwPqMQ|qMGQ?F`bNCkfrKk#4+ca`Dl$UuZzj8?9x0RWf6>)EA+Y0}OF_i~(g;}(o_7nsGoVX29g zt9zs16t5cb?E3QQL{zr=dl^ z=+M~OuVD|xp(M=p*l_DoL{;yu}nVCHu?&Zm=IBX2RP#x!JQLbZJ=2ZC- zs`)CT1$jaFn&{K+G~Lv>a)4^yuQM#cpsV591YK_Y=F^63UJ-kNI5u8op2p$#A=p3b zS+I~=%+cugyT=txZ}bG1hde{K6-)0*pUw$Nxm_l?Y|&gF>#umsl;D68WL3#@U6z^< zzMz}Q?mW%XRo^BBg80MsLYWrZ>H-eVtUcAYRn^}u9VISa4?&e>ykMb08(c9bXDY)$ zFPkZMz+qKGByz?;l;7lPK~i46&8=OIQ_2(QqTOO0;C*fE&ZfjI4-uHaY|7;=gIPu5 zsfVEWuck8#^UQBBc}`U}|*+cpl=mwYqQ4?Htv#{$zVHkgs4yVZN-T zC*>$OKVU>6soN}5ma!lRPer`%OF^kt{t#zU}IQqvavF zRkQ~fC`Nu^ALs5369xtLy1#wNC-#s{i_QbuLyrEPo)53S-|reVXcQ5$KIEZ^_XE+T z3w?shQn|6WQ!B*_!h6<5UJ3QA@P;x?41|fHiP@=z@^b8%MiO6eYW+MNET!L$9^j&Z)K0g$JwqZ-@-J=Pc9_p_Puc379fTO zs+L=+5Z)8*Va>~V8vCSlAD)2cw>bC##-ZD?2;cl$FE0#AQWH7e50CPwD1<4b4sw@T z;5B-DZ#;==Dp7FeyUr2ZLwCED)4p$fD<83Qmn6YkjjDm|>a_??&XxPUTscnUlA!KI zmngB#Tj4)673P?#F`J9#qVz?UP94#QH)xZlzpl=jS8%a%?GBMQjkypAaYadiYUSbe z4OX=f(ov9YEv7+-qw9^btAo$a)*l`oeKblzxO;|%#*d6fa7yU`9MG)J{@I)N9T`@o#tqc|r2yzO zs-2QX^NmZN)?tY5^X~4Gju?~Y*$s9T<$0t#Ujw&o>D!T0@AMF!dDtGWThS+v4wKU> z)E-u;g)kCxo-7f3J`6&m%I0n6Dbs;?tc``1o`v$O{e4&1Nv!*0F`~rZ*mH@fMRoSw zt`BozUw{|1tNZm2q^|vQY-s~KZ+xojnKl4}B!9EhQ*HUbPT6075q&`Iq7pm=e^`|6 zZEt+xfR!Z2RuSI*-HK5qAbxo?g?Vv(3hQxlNs*3d3%S@>T5J7qsVI={80f}72@~tg z2}K?7qwtSp6}ci#8sFKSZJ!MVif`r5zfevfG1e6t8WYO;15yAWKsXe_zF>2?Fgz+z z&kQ%Mph!|ygCgGv3&mI(5%0~L2}|V!=4WXvev?+JDcp~JdAF0+>KQ`TvkJFh+tCP9 zbpHuCZ9FXmu`@-n-?W`>ky@&#_1(0+UfJ_vgJS;~2ZO4Y%`MZn-=UmN@AsCcw#Jd%6*#2GgSZ<9E3`K)GoYhM(KN z_ot>+3-$i}!~NxT`~p1^uw$&z;)>q!`wNKNB%uqF=!{72FH3B~_h`b0fl}PuNw@EW z;t%89U|F(}E)or44v~eW+g>Co%7eHUVr}KDKSvBqPwMCLUu5b_FX%`ds6j;Hce6M))MP1*H zq|QILmztfmLJ+t~TYb`}ShOupt($lFu|8wmm=;(4!>RL$=b~GoMoX7XA35c+$Bag6 zyX*1~m1&R&?sXa zL?ilQH5SLFc12y}udijodG)-hlZxgpjV5OED2*$dG>U5bP?zwdO``K*>vp#DFQc76 z5JYr4@An8uEH5OhjVeDiEp9i*@BJcwfrNLaLs@mromOD2X6qOI=54Kg9+I=~M4NTL z?hczK!bO6(n#wHdbKWNf!w;{F=7zkZgsk zL28%~P6!Y8&psU%SfTA~ednGm(Ee;J;NmM9-yhd$zt8{`eeu9o=kw;dQmlQ}D5o;% z&7UDxzg(~BeB+rMoV+IEV6QGWJ%zVlh~Kz5_k?RV`p{ll>= z6Z6HE7VNd(=7bW;d8a$@_1l}Rc!r+Cy<7&={pA=+h6P4$Y5EQgeHAdqohsnc0yTGFKw3j&EYnYi% zsuXTrPMqL~&(c3ErYuW8nGj|25&p}LU1{B3Gk$m~9&DuY%ycq|&cU2bU_nLmeUbvE zJqydRu!^p#U=rwblS_6%)!-M4XszcKkM%7|?ugYYrDWQg%g0DnbfhA!iOhx{!@9?u zJ~^Xb&n6(KeAr5Q>Z9Rek5it}h@CO%E9b*SkhF7J~wS1z{2enhoN@r3(+)nn(H z^HU9`ozU4#u@&D@;p?1E&bjR=ZL5nMl^P|^^O@d&-$Jw{Go5qq^z7vF!?U$xOYHGq z?JBTRj|KaV=Aw&~DPkCpe@GiMzpvnUrXr!6fNqvqgs@dHa+pk0btUpW-<6RuexCVv z>NUV)PxJNQ;N%HenG|`&`~BW$-1N=SDl-<8i51Ue+Fi=;`WJmYcq zF%?+>GYf&rA-b8Wrh1X;b#!-}7DKO=zenFU*i9IjfQ1>@U}RJ`0cqKIl`<#qoU zV3K4b+ID!6?~(QL)xOOpv|yYUk4`Z6z?iawmHdgDQKivR3s=Y9AD5XsGe(41FmbwB z=q3lrCbKZ0^NZ^E>csfycO;P?>l~a7%bHJ(YWOvU zM@C~yLS-z~h?VAf=FE~!CVwdTJF&*Byg4@?-5wjDc`7^%)L&yzgYvtY?XkI9M5Gr9`uSQIgBl1@K*mmPt3uIz4rG zk$g9!Lypa|i53Oux17!8#kP{CaWn)eLCab^0X)?X7I>Rez4Vd!@0Jf=Kc&g;Z@DaG zaJEsGxgQ;tzN&a3<)mNfKMzKI7Hz86aPfHVx(F=y0(izb2J10R?+`{c55*BH;iE4B zaa!5KfB*i;(e~?%@CFInvNMs9SUyk``{MjN1=Gi8$(~L5+qX{+@AwoJ0F*bqIKSTH zic65U7>Ipw?L5)L2afm!^-vr9IujYs+IYqo+0ZJLpoN|F6KCN%-}2~9?sc|V$X71_ z)ZDG0_T_SmO&B7sz*L5%XN$MCClvA5CJT%DfK_Wz ziB~(b3-Kir%RW!~QV$BP1PEu#fpeNcye=Nba2H>POyG^K=jLkzxmwHM8(~A9{)KP z`F7J9^U3R%8pF*h<+pc{f)63P{u{u=`xgehwte@R3t2DvSH8SD{dWIHc4IE^=JuNu z0Tb;WbYwOS{}vg0yH1Ff<$Ct3wzo#>8XUZhUvS>;deCrue@6tm)&kjDbdYF1=D-b~ z!Otk<&<;MJDOJa9Y5|FX_ipibrYC)Z+L*9}$2CvrpeEq==vomE6UjJM79Rk@c=x_G z?v4N$uYC{To97rmmqZ-{9l^M^+PKTyL=EcTEBI(mUDqZOI;-{Pz1tSq!6?2QhDR;b zy9OgU;je)PUg0=FsT-!&33C-UfDDX%JAA=2f`(Q*&yj#{rjBdAy7|1;cbm&8rE!+=#ba!1Tl;S4iR-qMb}_Ft_$O zy*7@~1hz^dsck)}J+TB5kL>YFF(5?W(w=B4zaj*pZg>g!-r#uEQ6x`LqzZI&oRGdg z!pj#xy&c4S{hC065j-b2`EdnvRHrI{s!oQ%W$m2Nb279edwurJl%*k)3FxK z*2Fn(5+kK9^MOF;Uo#%?6DAD715A?RD`#-$>G8Ax^9y(8r=yGva{KN0Ogm$AuZRhr zoI#}!3&u&T0ZHsM-0Yb7xbNz_$(W#xwdD0KL>lfifs^dHlbkEKjFfV0vJGtP?li1P zKi!TX0y-3hBwXF@T#Nk^hKM6Wb3(2z_sFC!p2NKpw<}#|aC0K>mj-Uj(=$_T)~+Nz zTb-Y71tjwgeD;pl;RoE@V~j_9+~x@^d<}oo+`jhc3StRy{%zplXhigk6VNbms!R$| zdeD2J_=?$*g#{WTmzX-$h4LYC0`-Ce`| zjl3+3cNIdCl(CZLZjh{VSit!=bRnr9=L{-^xO*Pb0hk+Y=K>5KL?Jp$C(q#;MBvb) z(#^&k%<0UuWGT|`7;EQ~WWq88@eD*w{VZgeV#zY~iA-J3#n{OdQYWU>LjX_l%DLoztyDe%%yOXijtkpfS0${TSL$4; zd|KmRN@DRsVuegx{*2m(RHL<4jW|VfC}p(Ha^I@<$vv64_L%5*U{BInUJ^N`zK9TM&9Lu|Y)%#~)2$Z}o~t`VCpU7PnFCH4 zKEV|a)i>@wX3P+T?o=u?lABQeZvGA;`6{BCAWzr*DEI_RtQ}{qokis&b>UoA z3R+h-ffTj~?=FcnBPSN3n^cJ~QM+^ldm{%jG?rfvA&RP3@)@%ZSyv81%uLGUugeX% zHUai(Ay$nPjzeiqQOfWd5ryy#;a-ZCVG5TqQ5Q5;)7Cv{q^RhUfy=eG%Pob2x2OVJ zzawLh>;0@7o)7=Ns931GTe6%g@wFQ>r8^EOoX0@7#?qbjg@=$&AD}gM{c*cX+{Z)d zg{MleUFA$|RmfR(v8T$M*Go!f`WZvkB3tVXT zS2xf}XfJj!!kRP+k65CyE!3qg>Ua55{x)6e8N}0@u)vJR!j;H;!#E5Jj(9O*a^Q!V z?a-GS(uAQAth|WojJE}OapW0c#gLetHl4Mk7<;UzTn5Rbl%4)FV#EPyMn(CDg07i1*&h{t8*S??YB4Mn zhl$})`hi`7HMhIcM>r z3bnSx&x@bXjPnpiwFCuK)gP5LvPiFk@UdUA=P5P3$$AYmFh9V8>(aQS8$)lXGs67x zaDa`RnQsq~ZNxtp=8fMX?yHF-_j?MshVWkpzNC76P&ET7;cQQW1(f{si!8stly0c0 z|EbFY95QPl(RPV(ERurLD34?m7iQ!wFF=!wTT-bz)&leg3Yr!IN?(WqX3s#@P6<Mtzn9m4C$0vQd zXzNU6m&H9#B`{;8QCl1lVzJz5jXYBp<2EuS3j{?EeDU-n4Nors*B3DClu zPb*QTdolFhuFL9nX?un5_aagU+uUr%Z1#&y4_**`tYqOQwz;L*BVh$&Iy(N zh6fYT{<1@Y2$5%3XL%N54;dBZN)t0menFt1gwgQ)@=~z*@53lph13 zhOz^`f!7|AjGkyWSOpMtxi3)7H0H@$tb(;AjT?&2yYkoj2RYCWC}`-)B~I{-x#ukr z(}S(XGH&pLkIw0s%q1%|6o^d*QJq7E>tS^-}vTMaqFcPFtPBT5tr|{#t9Plik zD$&kWjZ^iin5i(Vbr>1+s+>dBn0<>TQ1h-@Xs{cr&=~Tr{@LQTG8m_p&L$Uv&CO*s z&u{Sz@k2r5A;7I>tv!;6TXWd=)F=-t5fdr_=KL~<#rgQoY-_X}%ae&A)bMZGn=I3+ z)Ex0|KA5e0J@i2%KvX%#_X!(5PaB`rSKukzu=AIe)Ajy%?mjX8RI8XwR;97W3`jQV z;bh0xqwl_4AFY0iA<}x^aeKx)R;l#u>$~9Z6tlY=5 z8jNXc_`tbI5J0qlHH1(Rdo7g2K-oNy(zwh#-2K(r+W(SG|)2PaXU z!yqLq)0-lCCtqaC+{P2O&Y@k1`{83sehQO=eQ}0qg?&l3%RXoEx2UtyG*-;J^k|0d zQii}!EqC@AXnIK{g)v8BtFqah`};NRSAz$&UyZ4G>UyM~F{PkwigKAbSm7i_8x&wK z)J;>oq-RKCWPKGUaR%A@7;3;>)4Y+UcJz6hD~-4P(8Y5g>V`GONx%g;msN2^K6~`_ zVALk1I?ja~i~!pIWJZ0%zRo)Njtd)p3^90(Tb$Sa+rU);i<7(d+4UZcKo2yAydQ}} z;ag}Gmx)vUm9h2VbcnYP|2$M^h?f0rA11ukg(|+Sz6ps@$51Ik=`5s{1yOgj5?54& zX0}WgluhU{ID5?)zOMF~-PqsDk~2!e<>)&hu}Wm7b*}cFLA#Nqvq9H_QN8^@GJLt{ zyHf4*>+SLH>%fGG2+EGZBxMHkvVyXhf_E_b`YO+Fot|B*x|$!YJRYh)Z)Ce3`R(Kd zN|P^RoMs<31QFO=jX_A+{C9T=}%M7WmzOt_u3;<5ZEX_wA!ASDOgb>&;BcVWlH~rm7 z1f@wArdMQAv`(m(@p@b%M~eh&bcH87StNa)QV*S=vO8Ll{*lWyjWTps!T{frypKci zE81zSoq8cPkzli6k^&zFW8zv`1|@RZk-v--d!xU<57SF0K!bY~pW(-s!?x3ecj_!m z*Ui0(5@(5rsOKyyIZ9x?e6IM6qW;qs_JL;(5#nOO=m?veY#q#DrtH%quBQWo`tG<% zb+N@fPjoRg>2=?qi|3oArDomQlSH2l8gWOcWEp+LcR}zN4nmW$29n+|^-S=|*>ve;Hb)5R*I>ZS48Z{-9`F5~6NZT;mk7 zNF~=MJds2n{!-4!ZcA}*6mHR1#2;6tZaJ71vv^Um;jzsGLb7sw8ZBg+VI99BEBJs` z{Y>~%Im&o)L0eAL%Rb581~*w%zu4fig1@WWpp_bc4G}hl<2cZ9l8n~6!FW@Anc?*i`KL(!ZnQ<&aK(j5#gp?aDn z#)7gHGg2ScK-^G*X6I@*{UlXhYM%sIfiPgi(9)bldTc}75sy&CpkCOeqp|}1+uggR zBbU%}jLKiKyik=-_*eN`a&DSv!dqr_$z8&~%dilN13JV{= z4d^Cb`df01#B|9Hpv+dR&GaZ#y5OPvL&DVI)W}z0hR#7eye7#RO}_0>dWgHUM#4Vd zBVt(>9jN@%Ubn%b1dlx`!GE3lbtq9nKt7&4XR}^GTdxeE`>BDt6}_|=Kv>hnJauOx zb`Q}j$1A`fwt)p!bCG?B6HI8W+A=4<9a4PHAxjvc(3QlIXS99h0ZURf9!BeF0-SP$ zmu>UT-Hw`KHs&yYBo`P~s-0Mj!p5(c&?0Arz27|ql7v?>in z1IZjTimT)Kpl|?$%om=}u&okJ>>G#emT0b(%$yU|^qGHGyFfXai!~a-9G*rCR2K8~ zEZ5J0BzN>I))lD0Y-%t8+$~iLjbJ8pvxSBb;}8f%w*J1p#d0Dz2ho=4bG-B|=(!?6 z0)@g*Ix!fN%~)X3RA9Jc?{lO8Fx$2|8b7pqh^7#bR{D(lhlLrwDe#A6Oo?qJy~VQX zCFEu?>kyH^nx)v;)_NTI$j^Tu5te|DIPX__cISH<*J(Mh%8DXD*rtsjg~ED40J9=3 zzaDCm2dqhn7-#)V+(suKYXh5~awC!n^4QH9<6impty=B7i!1;k>u`FlqqPj#j>DO( zzSKe|yX23CFCT8M8pK*r%EIV)JiFP*?(};?(O5XfBhfs^HSlqr=kpQ#m{)p1VDKC< zIyO0^@YV2dFL}=Darj4^<;TRsfcJEq+Tp-uxCKDtgoHqlMS?9M$zX9#BN;w9zM1?q z+=v1*T()=|C=q?DZTVgt4wBN*zaYYd5XcrpaZWymCZp{phQg1ZZCOQXw8+^3U=WG% zES=l(?ZOw&#-|MuY{+E277_3z)eBF{14`ZSMO31qMWk;Y8`^+{Ya@;$Q@hzufpLTt-~a1RW|#iDX@n3N4kx zda9?=bs>7_rjg1o^zs|&@((cWvae8dIz-ZNM$U+f%v~O`d4Dp%QCf92$kTdwHpJh@ zb3QCQ=X5?Iwo`RJDtUc)J_g5naxtzz>3lJv%wBylsSf*nF{Q2a6f$ ztclO>%Q>@%Cs*@U>CRURcBR!R5Wa(WHQ0jso9h>Yi-Uy4^ju3e}yBQ}R zha?F4iHC+$Hb($z;mlvX<#=V*icIr__)>Fg*Qdo zkM@@xjGbbJO;bjH*uXARM8}IZaQ_luKpBKud*aVG{u6^Sq7|ghheqVII>|LIvx;Mf zF~TiNAPPn!_Z|gt3$L!=*P&s-k)X&az=1+W(=PFmdwwA;hAu_k_i>dv7ZHAoohtUP z6I831!O*G!ze|^IHyPt2CC3WQlp~FAX%7j^)BbaOqT=$|edQ$IPq2 zrt+z5W{AE;k6kEwC~gif?P;vV4?8HAKSA9DqPw1PNKT9nM?WtA^JU74n_!) zOf-R5!dYvfDo@d`2ok3qAe?R2A`JQ&=)8%{4J4-vr`f(P3n zs{IvK$3smW^B-p$YIU_5E1VEUog{r<+iV;TUVJh9weu;~%*ht4<5R~s6bgXJu^NO< zVYiCFW-H6naLdq+qOtH?>t%%^8D#Gm6 zJIlAqnBpv$z|+>%468Kz?T>aP<=dG~EqiRHg%{c?BwM@g=*;r5?g_v-jB(Wn@Y0Ev zVfqp=8Lgn=cq~1gTMCmAkHokJB_s%`M$95DL}GO61y2M`j0Yte{Zv`U{6GsgOJi|U zKnDBRIHN8~WrKrP;rXqK5&-u?;+xVxVpy*@q zHj*GKuU!WVrA`*qQ7-{LYHM^)sDn$n(ZFG7_m$wTAF2nZeb5M=vwoIzr?UZ-F4^u7 zr3?Xcd>4}&+1wWvV6^veXKtEOjkn1T0jDK%8l7yO%;N;b!c@pYn#2|z(hh}?VGhiv z;BJ{;kp~*2jpEwpWh~TBv?qYo*JW^RHRt*aj@I8-Ki&GCTrYXf5zu14;+F$4`!Au8 z%9KzJBp?I-RIXb|Ved43sjYZKvk`sN>oNmaEOY_=E1tMNkmdFHy3s0J%TU!Tu^G zw!ucRZb4maPbP!9aK?%v5Ow*ja(nMGeR|a^as#B^0}7D{25q4Q>@x$YpI0czqQ{tEo zu$98Hu@f?ei`ZZT1DgKsFcNJfBh?8S26Q#KOud-v#lWCJ`IqFk5C!5pgzy&S1%Dv9~icgIR7{vTOwSlGU8x-o;g2UBHB7I;^Kdv zwTfRV)~7P+o_U_Vt6nNGH!$kme*Wnswp4OQWz2u^k*N=tN?#A4#@^qZ=K=`I;8?2T z!PFOd*cxSWl!N2p5*PX4xH1KH)rn~1ivq|Qv^UZ~@Nll!njXRL)(KjKf2@c7*D50rh2>vPOk&`rvel#AUg5 zT!mh#>P&_4Wrg8Lg+c4!OpX6#r5Qn`VV~-3L*`|bokpec+~90W`(?FTT&2m5>RkKc zWsUDhrRnwH+}FFy+8}}|Gc2|F@6=ayks4JNltc5q5?A#JaaC6AY72wLR}C2>RW`7p zh0*r%q#S~3J0)&PGW*D;GL347_{nAftr%wU-^3n3sH}IN1LM4UfnGXa}r- z{+)p;*X4DEpwWs21^pWXRn1neFZdAy!I02KLgAsYQ2FS8Fi-`2AZx}ypg}E`R)*qG z_5=aM;(LUKg(NgkDkfDN_+}L(pYsR}sr|?h#49vj;uM7hw9#V2Mhoi%bJ_ATLWBdI zS_>;WCIeVFlyp^gq18==S9=_eh(h=S@dwoOWFHwQr(DA7b2-XjH)9;D6S&iAUjhM_ z8@G0>ED-~f<1d~Qko7nHr>0x#cYLE*^pUtJ!1kttKKvrE$w~Xw3I-ktgr9%0G*T<{fF(?MDuN}ANyHk~~$G&#nU!QCYtHJo9yQ?8!MUJ&lG6TD{Fo;drS~#uu?pg#ioMSzbCCzR14ZX<>QJJlLGf7=^Z!=k2k#j3W z&%k~w)zGGVE6v1vZ!6s_oOAoXL&J8KTQBEMw&$$<&L`jP@|~QwS9?3TL6}^-d7%^z zyZMo96}ttoqWimr35r~MMJWajd&LK&J8!ltkH0=#9UOlHVDX$F(J7ryy0F=+ zPQK&A4o|wlN<61MWG|dfe?V-jPJ3y64*wMz{{Jx0e}M+)>t)}a>g!)`uTj6RSAwv3 zZ&pJoU2fJQ*=ugrV_`=(8wpChx0@+1TyD2AY-?_}vwecXpj_!|;2tE*4W?M-<8LI?=Hq%zdhXa)yh0Pd^-;Ob2ijX zj}8n5et3K>s^&Pc`?(e3(a*N>W2|`23j(0t-NQB}p!GWofa5f8Ml(tC$ zS(exjb_0QEz2O4#=ExsU{Oe=PItn14#d`VD1Y(UOEwEI?(ZETJ0A@QYEKTL_(u%8Lsx0Q{>X2s| z?t%!73>Kh#`FD+m@syX6vaI@kgIfMc(FH8#l+`z?LbDCY5I8HZ09=*xeNt>|wgra_ zL=G&Ol)=;#irY}uC&{Fn$mNEC=U5m0np!ukG7=5=qJCJReVoAT#DXAe0F98W08{2I z8{ugig8U>nOw)uNY+e?uk4=nDjlCwo8ihi0DRCobH)Di@qEso|^YZvSLug2L!v(66 zqf*tOqU{6t|6{_2TmJ@0de&4#jIoa^WUuOi+mxk3%Oz@Lug7L{7S+57fQTADBLpz#$%Pd@ zx7}5Ekx7zD%U_|EJE-&ATR#je!X-e%86g>z2Xgyu3ow|7X37RbkjQY_s>~PSW~+qR zhGCrk$nOGe7UVwQ0&asKBux_&;)B#09ib!c7p6LpnHvUsCR8WeBCmk$(G|*5BSbAJVOLCTf3<3_U)zxv*GfG4Uf9=~RPqCz8;TYLq|9+F*k^S*GE3&@ zED0c5Fb5W>GLL+rnTuO#ewF4#?mPG434$~hZjc_@)JQb zOo`mStbbK54kWyo?){_BjMHMK0M?w$F`$v``^6!wmak0>u0PpeeWz z`bg{>Y!|CX*g)3^45*8GcJ4kTPG|k;;N#LVq2Abr#&79FJ3ai}d^|`SS8BY80|Rpc zTcM#WZh$s6#bkSE$8JfK!0LYQS5nXza-YkWILOeOv94#kS?Wdmxas-br=TnA8Uero zjl%T8m%mk-%OBy2{b_7rbU(Pn1fnSp7ISDbL~9ctOI;w}7pMQ8`#3Icg11vbeZ+$G zrD70vt0|#|y>Q~ePe^)zWf3+$pn}+UpWDY{)mzg80G{8bs5J{<@!UZR&~_m`p#oWA z?_d`&zCo{$h%82xTy>F(oh8{ytm9zS&{Dh)5qqAssvUmQ=_bHQIlg z=TBz23x_AxT)h8)hR%fzu$9eIB{69RQ63=&1@gQ?g*NuE)0_d4!xs4H5F`cNv&I+o zr*IY%LU!bxV&7MbA;DLv?qX)o%16;EE6)hg^aX;S(6|wz0|-@jKow7sSG}TNu`+DJ zA$eaHDA=VA<#+^fGSR+Qubh{tv$1LBJ9*+)nUNt+4hVocKgSF#mi|H zsFd|&v-6Itx)qTB*^)3T&Ox$N$%QJ4ba5pegzF1ilGAKU&;n@y4B6nuo-{$Ama8$C zN2avFH4OE&b8MavX--~P_68Z&wy2XWh2y7fevash*%h*EMruL%9*}sNJvpR}JK8G9 zfEZy@w_+*RFa^pKcVa!9Ss%HC68OHNZs?{im9R3qCx`(iJE!m*+r~OI;T5>)J&!Pg zA`*=l+tc+@)|u5D8>Yol1hxu{LBv$NgSqadZRW<77^{M6w)8& zAknX5ixEj3s#Sog0-FhWrK}dNYyh5#G$~X5nxdTBAHhkc7s3%0schgC8Wu{X#X_$4 z_Cn9~nd`p_n*T4Il*J$tQyj$U;p!ujYL=gi) znAn+zT!x`V@@fB28p@MpF>#ooAA*pL|4hs-DsY zw7B*)W4!PG^QFxm8A&$;8X+4kuF(#ij&lmDhP{14H|=@+hlF6V7)|6DEk9RImq zjo|zH=n3W;(ErmDJZwGwdwC*y39zx!Pq5u!M0FW{gjk+!fTQUzw z+KdOC!zc% zYk1a>Zi-pAaM7ZCLXUoV+Pjl*#?3rhlqfbPOWRtAGMPC^dRY&f;Axa*EE8EN#}6*u z(`bWXCW_XwXWUmrIA-HetS5$Df=Q>bHtNjOb7j4ve~Iv2R&ycuSlSV>jIMAmO&7Y8xV#Nx(?PI!f{a_{u zWWRwSdEF;M-vOYIE5HCZW^hVj6${xpG~#(Qn7AmVn6nrgQ*(YYtx}i?6)V;!#$=wt zB56Y`1sTHf7YG3svGN*HQ-n`db2F^%clEsIPmQ4Efbi;s%j*h7ab8xLUTf>xe{hI*iw|T zE#-J;4i@!o^DeR$wl}u??M+$wr>NBGp#SBroNw)f276-a{Mhu@=DzjEmbr8s%w;A< zBLADbq}w^ov=T8?AE(V8PF%ot-qmsB6j=11@y>7H5OCL^88K;cFr!pP~C{`Gr> z@Ja7I>y_fgwfCA0Q=igJzTMBrkBkQWi|O$XD)RZiDO<9bAXYg4zk93yK}E0xeW9!X z9H~^ne=1u8$>hXV*m+Q_!GD?_;@Yx7M06jNEq3$RM;=X&(cz#~^RYs?N7KXjG^x8# z`lITn3QzNJjAEeJ6ERgN2urU!`qSqiXiPX)U&Sj&5+?vAT@U0ii9h&di|@H&N&*6b z-VRzr-_2h|{OXRRW5ceSv;m0uZ!V@36aj~YHexlyx**S>FP}qdF?j!Jdi<}l^}ov2KfKldd1dRLR3!H(|G!g_ ze_KzIz6DnQCl&Evfd9GKsYU&LxITWg-SGd*^!SIe#qsrC-~@5+0YzVCm&MUM35HDP zUjjXj>?Y7Q)f%^Aa!?~mg!!BarNhHBT4oP#r96d#1i~^ZCB#c7_ z+zt+xS7)Z`Yy0}7A~BZ!F?QgGoeY-ZM3jcfI^|>gG*d6}ho?Q3V65_IJpYHi_Y7-# zYx71!DuDn2=>mq1h!{{hV(3M>qJW|isfrjBq$;6>4gu*PhK>{sNEeY_q$3@KfHbj% zDouFf-e+d-=b4%JnmOe8a%N-tX;NyxY@!nNCJ zV7(sy2kDX8_4k&_lW@HsNs!A9qenG?=weFCPZ16TOGgbP71IyPhi zg`lG!pc@O_p}J%Bz}(r&UPQs$T|#ucs@yAbjjaGrN(vm0r@(SWMK$oJsG3s+2-$@C z$mG=~76$suZDHqb1M`?cz`xjm-vzS*khFp`X$7;Vs0UzRAVv-DPAmzdlL$8GG{rSF zvM~644g6q_Wria}q}V(0T3kBJpmT0vXnZh0L|Q2!VQ8)pkes$Mxg^{eoyNo2nZQFD z=MGS~tIT&6%50EFhd~TK9a6st=Ft#v^Ag9f6kh>}>>x=f0^o8R7%(OK6$3dlQD@JM zF#v@l8%HY&Nw=VI_)>m_(;EIpA)Sr})87b<$FLa?9Zg#Upa%m$VDMkx@*n=;&sF*T zkxywK>5d^|@qti?fRv&TMpOsHsw0w;o0tx;0ai!~xn+Tnw(Jou^mNTyiei#B?4O|; zP$;7+LrFqz#!KNSAFw6W9N28e3`tU9!|R+(Mzc1Xd9_BRUj(vlCZx&$wBi_K{q!h< z>c`@MkzmtCK$BJ=A8;Cu)tL*V$Fj14FIE>%*D)-Lwcbs>fBhXD@NbJD0004?00vr} zXMWd7!C(P_V3-gB&2XzJi2jW`ZXDDEf*`?aom6#HAOeA~D6WwRtN!1V z0GEQ1vJ%`o6-Bi7`Rh9$-&fvkg)r7f(C8kT;K=Dikxh41=~VT9s05g62|IS`Mo&%I zrw*d<#Z1H6^2P3yvv+5DYF{t+lQbg*jOr>re<-|Ee51Fn^2-?cc6X-Ho2qY9|Be#i zzYoR#J{12KIu!rS`~1&C@o?{--{=4Fp_qSPk$M~$K-m&lY5b}*aD0ZOdk(e2^u>mH zVsobhDysMPKKj!MVeUrc%3w4@5)yGFfyZE|Ecdm75JMmxKtmkNhmV5dBk7(!5<>!n z_4<#u?sTxQ%;TM@**qeCnY6jYL{NoBn3>Cr!UCo(7=d9qa?;^p&JKdRn|ceH3TaML zW6vAnk9~9!VAC3C3lRwr(4%N=WU~TT2Q&#xS?b(5th5n)O<^5D1ir%^Boa&_!dROG zP$yMgs2b7CbdB_fYf(Y&LGdlLXiNwYZ=P13aH}VKjU3l13Z*2F-5W1hH>qBEw3fiC z2jr7)>h`DFvePLFuec}wYP74AkmICrXs_rE*EN?%)h2Ln-U+V`+r(yT#A9L|@i?z>N*niajUa#bPm!%HP?_m*A}bgl6MkKU zhYN^j#mK}*oudcHcXb(Qti`^&w19#Pb(t$uPLwu^98=*55NW)1jC=q)V?Y-p3^Yl= zEZEWYaWmaauw~D|wekzVI{^!EPvI%;XDK+?rHW>*Qf&KL1$Tgv0D!j?AGBeu9%YWe z0;c(vHn?x~_@gv42Hpe-Z{qt_WI()Zf7b+Y0T^l5L@@25$q(R=Xhf1oK|wrh()2hg z2@FAS%DS7H+BQZ0{aOhC00B>6AOBJjq15RS?A-sNWB(VdP=!MIKpZs)g~+1U)oW&JI0Fzf%wo9O+00o-4awIO zL^$fT@J><*iaml;cH`#+R;i&FL^kp$RIinFj~u3Fn9Y{l*(#R(Bg{A|o4r7n&uzXhgFt@7(@v3Nu- z3I=}{h+HRz8RiOcIiFXNU5_J1<%)>wcm0XAh|v8iqzO#dpOO&v6N)F_o2RWO{3S3= z{A~qu)bIY^P++>5uh{#-_z!{UW&wt&=>J`Tsn7On(r$WS& z(WNeq7pEqVCbz3G8TYPZj;?{((So@KTD-F`fC1qo)fqH%n0Ak4TTo2z$4M`dr2 z7*mleZuHsU+TKt~Uq!w`?`LD*y$>Yj%0gYERrB<{;X>oeqN}~DSKjQ6kYg%K9F4wM zPwkD?^i{s}?EP|Me{Zajxr!WW^wswG{&=TxRe5snR|l2-iGi4^iUOl=cdqSEj`vkn zRrh{#_1&MMFjv=f7=3?`zCXQUTwOQT`~A_I{g2d`>iT7)A3jt2Gdq3N4L^H-JlWr$ z1+dgK!f3GT@dFAyO*+WcMSW@h-?epy0$lO0z#<0{4gc@&VAOE$YXHqwm z+_#yp^6Rr{Y~65y@mA5bU#ph=b)(gNTQ7ZoeZjK48SgOOE>HjU)yd?|Yi9h5p?>-@^?O%i9%Qlf9wz!_7jIx2sqC_eS3wZjoc(eswh2pPV|}uIYdK z-LrpxX8-VKBTEA{)Z}3P_|ZV(e6NO!)AfWuhnZud*l5L+tvNQzWW~S zQ&`>|(b)CF_4K2I6_a;+V>d7P9JCz&`cw8il$Hc_cbDe~rT6Qb|Dq#zu)xPg|0_%W z5Oo%S1pYF?A4W#fh2hNz{)~eRbOU(i6+7@aa?mt@lN-R;i98|(@YdKdtRRmt0fJt3 zTsz1a$`f%XyTh>n$?-sGJ=PDXAg%2{HnSjk3A>Z@to7s|)y@F6L58EoK+O2<^G?W8 zQwG}GI|>)9o)ippgGCK6^x#6o=tIsgF>dH08%Qu~FT46i1Y|8(o13652Jb~7d$0s2 zFFPMfFm{KaKgy(|7wVM~>QfWyHy-+AClt>e7N{5&Y#B!I3JXgKi>L{U8V`#(+6g0a zhsP_1Cs>9jd4;E>gs0VnXN-qu?u3)LBeE4EaxEkBy&?)zB8qAvO2#8z?nIEeBg+*d zD=Z_cydrB-BI{}*>&GJ-b|M?OqnZ_?S}mj6y`nl(qTbg;^^8aL?L-Z5M-M4R4_ii$ zdPR??L{HX4Pmf2>>_k(zW9AiO7A#|yykb^TVpeNnzK+Ly--)4e$F3{JZd%4}d&Tag z#O~F^9*oBx?!*Fkh(IMG*op}CCeo)88ET1$2_o|@5y=zBrWD6+702lv$DJC-TN}qW z5ht)4hvJDBQi>O`iWl>amq?A5tc{nNh?m}tNAoZ!cZYRVSXR z?LNiuBxoolXjvubcqizkCK%Kv7)~S@?~( zC&^YR$<8Xt!8^$*HR(=mlFLMr>+T=p-jtHvt&$&kCwrwP`_v};O(Z|rO~&)21S+Kj zTcr@ZQ^HbHB5G5jCQ@Q{Q;0mN@k*%)R;fwesVS+cX|<^t6RDZIsU)7XY^AhZtF(OY zw8GT1qS~~QiL{ryX=I-Ca;5YNtMn@G^qSQ4y4v*miS&lu^hTbHW~Gc)tBiK46B&KG83R1ehLoNSTRj`~em0)^Y_j&*^u)87-Deb@%=sgw%mu5=CGX6Y)Xde| z%&!xf-*;(o7SGp}o^M(`-}ZjKllpwG_W8lY^TXZe0A3PMnFO{bL48Q{X(Wa^5@M3X zyhlRvX0a(}v0G}8>NvxStiMXa;Me6l6dvL%ZYax1c>_p(<; zv(bCHvdTH~J~@hMIV7tb<;fhiy&Md0u7+~1mUXUv9t zCGAC8-HVLL7nyr6NW4Ya%0;=>MfpBOg=s}abwwqUMKAY?$h^hn%EcAd#Z^AVHEG3l zb;b3Q#SMGKjl3nz$|bGVCG9>XooOZS>q>eiOZxUo26#({luL)LOGkZ5$J0tD>q@65 zOK0{nO{I+eS{bKr8FzXa@0&8dsWO56GSu;M zA(e8GYvp3T=85V9f_wA7Is9zARq*Z zl0Z2j0~-MhvS=bks4?#%P?Y^L8G}`aD?Ng{2%$rDp!^{84knwiJrt;-XNjB>1S+5* zvJ3!m@g4&@_j`9Ad?uI#?UHQ*N9Ro1YilT9FU_6RU3IfnGNJ@2LZvph_FjBZMbR!I+#_$MEF4k3t zLpbcP1L}yJKuw?v?L8WRp=VN$rzho8M3Jr{Q5jyQ6dJb$O!(lT72g5jk(DY$n_&k6 z7r{fwlsV02Fax&~{t`7T6(ByhGOUaX2+9PSNZP%?(gnZ9rBlsUibM6FZ1hn4MKe@4 z3v|!CXYw+&xf%G+9Ne)#NW&Cb!8jBd6%1eyV81S5O4kBN1{+v<;V40fV~q0f0m;xt z1S9todYnjhqX3<}G6F}$0>C&!Fzym@kX{iYDngG^59kh6;1Z4xdQ_N z@=IAF@i`b>;2TBu8bBZj#sMvIUU8rVCeuT%+*ygc)fbSMhR$Whi?ji>V0E=3NSYqQ zn5W1^Q4SyYT=xf}J;_dk=dA5zI?NOQwokl5Z$~EC!XXBn<2;$x8HuOV7G{j+j%idgWS0rGX9JBYA3jO#h! z_6MxQl$2>SE{;vV4|K!w9?9V^HKPIf6=YM1%TC-*{ z_qyPJLO^N1k!c2mrsg`ySYjD*tU2G$-v)XMv_tUX?BTSnT3495E*)f*{a|a|`!BkTP*ULg2 zXSw43vSX&Q^U-T-o17DCMKV30-y$vls~vNZHfSu^@c9SBJ;WIW#s3f3F;yZOq2l~_ zx<0u-0zvL-Vc=&F*r94G>ZKVqymKpTBgsl%lu^1IY!nPf=L_=v@oGf(Ey+V_WU;EW0t`dBr4&T`yk!s zTQ5r7DO}4ey9i#L8P7QLb1}q`=_k#OdE*|?TpTUmPlt@D1m1E1&<(;>-)c?5q)dSb zW-O_h4es*sC(m&$)8-MAjRy7k@(-Df-#N5Qt3LlGpvquqe9g#*PYrb#hU>I%5^Y89 zeF6%SJpo#=?;7^ckRR6!GGJK!_Q!tn^L1)!J0sr0lXMF@)on)o_rp{GQ_Pfr9ra$1 z%f5Vj)}BgrAmYo0&u<-L}3hXuVPK2 z2E$TNFi%>hmi!&OLLnY>0H=?PPo~tmlNbG;{I^z2jR)p12y%1+Py$;&$OVHRq0;!UTrBW`wQ%mjh9OYpU zSMO`!g}(!-)g`anG~M%PK&rvds-d?K-iE&csZ%!Rrsp{ZoVTLe@G6~G_*ygd$o#7%S2o)nddCnX_F$9 zMcy|2#4}~tuK3a|=6=a1fsLIG73T8T5X6Gu@!C#3vGRBt3@37Sx63%@!PA24xBaAS zoAJ-I(XEj)P+zOMH(PW`GJgW8fi|t@zppX*?0ovK0I9qK+vEq>%7aCN^sAht6}q%P zfK;DS?G?0|z`q4jzn{H8m05b5eXI5cb33}KEXKp-Hg}T~^`3p%jLDxs>g6%jRm;Bv zsZ*01H61sysa*Ekm45?LS>DFJo2l;Jx6#!Z zHT!%mcauJ&QLA*WmjRlyXGka*v`Y=x74sb z%%W65TQ`3MF67ez4M_F%6&Be16G+|f3w=3eB{g0@#oejwQhfKuzR%|&jv5U8%h{_v z8a*6FyYG1>ADlyFaQC1gVm^4P)&Yv81QT@3x(Q*Yr3N>W2s$Hmf9JfH5ZvYrkd5}V zK~v{>GPvIS22$UzUjOzdkm`H+_4!w?$M1J)3*fQ7wWv0tkCa2!=ZG60j~Ssn$R;M=6>ZiGBC|rF;J!_#=FypWJotv6?Y{IGO_e${!5DFvj~fr> zwKnl*^i*d%^`MZ~I_~J@YL*Zn1qmNy*tqJ?qW!2c8P8CIM~vffk9<8xz$dQWsHU*Y zUUBwOKvsG6#` z^eriHn&>A#0x6{;qOf=^J#5YZLox=>)aI}2bjgSWy^>yty&mZG63<#GayDhbdNB2Zwh@r37Uh4>yrZ!+K<%~FU1caIMhRMB=bAS z5F-jSy^}uCG`MKTDu)bzY=*Z%1s`#RaEU@2uuy#zv}VPuOady+5q!lmq#qx090DtC zG>s=lFxyA96Y#cw0;zhA8x-a#T=W+-6Gfh>;ZgK=lv@xq;8CLQUSa@n&bRgo($De6 zYa)oV4JwrqeqJK*%ZOhvJuHJ9l*}D_Y8>K%49YDCtHTHSqQbvuKVGj1mz87S2nYt$ zgz!ROd5wsAGQ48Plt$1n*+cu0A)Yu`F?W3WmVZB*fjJL0(1;LIGTRS8w4xa{#vq)j z;B<5_lLdqPQI}~tDT;L@Qd$&$>H*9LmC&dcg%|<%AR|~8q5VYg9a2;j8P2km(Cfw0 zDo(`47Q4u)hs#NJAvJ_X!;^-`Hvh&~PmFEhJ?3U;MH zqew*V7jX!CXb&z<$U7~ijfmt3X0L?zG}1qVr?Xf*We$1DT>13a;?vWRgycs0x2eJK zTM38}2Bj|gHt#6rkcjI}iFUP#zk$@+hu=+^1(#VVRZNQ{g8hrD+W447i)Z?d&e)?X z%=cd0Yyu?v(nFu&5pVE*3!#BMp@Ff5xam-+v|`Hno#1*5BAFPdO-&|MGbGi-rg{a* zjd1lNBM=h|U-DtR6A)8HGmY|uQVb%U5~+{G+hEc_Dd4V7(miK%#WRT}Kkh>LKIj+55s2Cpk^?s4%w&-_t;LYx4S=YAawIFH z@cb^Kq2_67nxA5-pV0s~krNszCRTNgRY~jpP zi5uY{BeV-8E8!=-$vx;C5f1-k06regU_(x|kbtwd(Mxs}>6YqTNW01WAe4ee9+vaGuFV#6$d)k_!5lS zseNrFnJZMBV3=COzXCm(4$f&5mYph3dvgV$l*gt08ujIMoxxLuYj6i#odcRd`EE&e zWY)!WbIrRULlFsCos14BsD(i-VlgiYebSPBub0R8s>p8w#Ul7{ZhI!UhHH3Ps3Umt?7A5Yx-km1-03mU7%FPFHEV z(?(1^WmdXk%A%5+b1_{ZJh$c~G)kp}?`z$@uX)+Ekko5W^JFqIQ>$-C=H(4An0A-m zypu>EyhIaU_WAbn8Rlq_TF4R@S8b#+Gn22V{)5~t^! zL)(H!CbF{O$9~(_zxl+GU%FaxO^0&bhxZ8eQ|54)ge0j1rHEXX$`grz zxH4-7gRVD;fMB#s%l!L{;3~LPalI#n#RkxNK^55Z1!{GzRo}0>=K7D>w zO(?%c@ys_0$bAlkUs1#R`ktjoGh0J*8|=F?9FdQ+$*g%H&%9y!&ZQcK&}T26j;}($ zv@<56iu94?!gos(ufa3gr>nSwe#!xx^7!ad5JvXZ?lqRl<{ETQ^xqx6WH;}MOFZi=c-4LBg#s(@xG zasgU1o~)f7l78%V@qo?vYq!hM3gPl~IV(7P1)jfQ}%XiH*&sS@TmOURY zL@q+}Fo;$xFOvl?yYo|9G(+bALyaYGCz_#owdQkuhY?bs9y?;p6TG0>wUsey=@HCX zVrDYVFhCtfeSX?}CpXJ-;M|KSS?`m=Q{Ws5BRd@C#BYA&*-xW&)H~Z*Mi}x;(;Eg% zvkl8z2OzE(ghe=u$yY~B+EGPei7_3LWCE4fwTO}Ht(;kK0K|hN<~sv0uTjPa158$3 zEUp7^U!AF{=ttj(lS6l-WuvQ1dkFg$2xlv2oF4K~XAFMKlk@bwj-u1$i49S^LVZvpG#AYMTl-2%6m~J(1owiLv-tfgO=}mPR#?r zXK0oR;?yB-_TZc2XEMAz+KEn$;vvR#8Vi5W&a94w{feG-*MUo8DVy>v-x}e#c8W`* z)8RLkZ)6zGfkmA>H|#Yhs5alfJ(nxSBtd3)GCC(3Gly`BIqmk!XS*A>2)e|_*n|nS z=EEu8%RKkfwC~rnu2NSo(={)-f%d|#sb34Ght|zgj;>gRE8kz*m?~JiF1ilXTQ?|v zDRta#1RLWhqSRR&f%yWN;eZM;0mR(-K97eoo;GC+*I9n#>PRoW%wr5<7FcGL2BqG+ zchCsFf9SA%LEFn^9EP#9($@oJw5YAa=qZ>0m? zdJ{h3?A;gJlG+dK8y!D<=J=@H(Bm7n@b$~6Cx^h*QdkiGV#DE=ERUHeu~olOmiwg7 zhe&uQGou7?+2hKJJ!u$%gzHCP_3HO#~oMe%JBdRMWeT<-`CAffRwQC4eVy%^Ph)U7Dx2PomoWqyB(Q15Zl<6+&)1Wl zK;wRZmELWfKQk}ly5n`^@D&Y~!vYmdp>|y?_wk_nRm509`1 z$?ql;kACI(@4aA&i8!&VIWWIVq{pA#()QoX@!xea~gqP4$cR2v(7h&aGbu;nqu}zCx-w06EIKt?PEb@hM5R?7n`Q5P25mY zwX0=`i%jTS<~y@BY5B>RvwNFXtS|KPjh?B6@LH^1F0v@S(UxZM<;qK& zw@)C)uY9#Ex9`o=PQUWay6T?7%$rb?`RjGaH;vqMpMr1PY{0Jkdc%dAZ4B@oe#TfF zlIOkdDiJ;(JIf`0IB7ANK+78RAF?i)hqdXyuo+iEhUqThdY8vlG?CBTHv z6DeKSg+p>E5>nbd;?9@u^06+PNz&!B31iC~J5JK6`UYaM9FkCXMkg$kSDhblSOKD5N4tqa?9_%5C* z*PuVOG1z%l<UNr5K;L;5;XM`V;_(=&&i2d6XhtF1KJ-RwyD^ z+u!`tEe&yo_G(Cgu+Ps5Z=Z*^I6e*Gxt{$H%GR>+uRj*|GMLH89Da`LNek5B((A0* zsU<7$fTuI`oR0B9t%;3zz%6-GiT5i@S<$zi)Y^NAen1QP?81c!;zPFwkLI&%FCYkN4+S=SghZZ9-|tvkzZ*b>4PgNxqSP zN^@CTEvq3};M03d+G5H?!`O2_ag`dY?lT>#8=(Rt?nCK@8ifx-6pe;2*?mY0vA9&- zm5_Dsa=&v@u1{L!Hp!=4u4uw!`K{-yKTOSQE>yh1>r<@m6G{8C{+tA-oGr&=jhV?; zCXJQ`i}zStEfQSCNe(S##th3+iGp`Xf`m@vblhsqg;TN*o}Ja&u)&{w;#ALmsrZNB z@oKpMIF1;rN2b7h8@UbzY}Gbj+;@I>y>Uwb@F;pJndE7(lG$STHJb z$2RBqXE_#vzOn6($g$%>Fgd=j#EA1>pB`*-@(MZi5ks}?w9G}7LtKI`H{scZIp&E& zqW4c}J2mHFxfrc#K8Zpm?4$w*Lt^OLu3S`KJZTXgm!~S4m=tU;Wv_J6dUPnMy47CV zV}jK_y8D4oRwg_ST3WV7#;w$itGQ&1VQ`#q&gCMq9<6&vd}!Pu~-GOAms(9+P!* zUAt$p-WJ{WsQuVW7wtjv2`S5S+FfPzoTBvLEJO7u@8LN)wLqXkx}sp5ntRNzLd~Or z9jE~Pr&J|U8^fHvu$WFla6J44=aHxxx9#PeR*4wHUVW$rYlMN+uiJyR!SsanAFhCn zlPUoWX!e7-f;}zg$Lv9wEuY=;9&2UkSMm_<>)_JEweDJ{SL*4MFPLXN6%l2%ud|W2 z=S&NJCPgrdxmR-g_*u9;+93`UOB#BlWo9pV&6&0B<%2Sh5SJr6Q5CEAS4PW|+rEBI z3au58IBOkz(!uF*oZ)Q&2eXK95OWG7k}p^=oYgL^R#Gq3yQ%+%_lL0ryQPY@+^BRB zNGi)R+p{)$=D;6aL>I1yGF!v85e43zqpCh9xa>-Uv^9>Ie9Qw2V!#fBK&xX%H7`#$ zpyf{ncJ`BM%IWg5+~z`D`Q1JQDkGZ&G%+pwrR`_d_CDEt38B~S?FMcdv_6@68m4e! z{bsC{EoSQykQFEYa$|I{zTvtx|M{fs0&fcq1FvRJe$SezJtoZxi@OmPhFn#TVnioM z?ANu$$214!UURJlu;^G6U3weXk);|gfxBf=0b+V^#hObLxrN8r~;Oq?HHMztA}^c z=?0tm;0M%r+_%plB};nlU$|B=L$lC1cUE{wXG^L=6GJCvKr0;E)Fc)R#tw7~_((ub zk&tHtpVS1&HxEfmum-;VxU7(lYn41`i8w!TIYesVBys-s7cb+C7YsB%O}yGBX+Lnq6$j2CEd|nsw9Kd;(_z|t5Hy7u6SxJ);g{4Kt#=bU zF%{m#P_zaX+rdIa2yFLxIo9`PK^>O#lWj|!CL05!Q7P0 z*4gXdM{b)2>c!f8IvV5ibdu5Mf!4&s-NH_Jt|X+Uhs5KQ#^F#ol+?4FMuv(;#@0r} zaAQV#xmGiIZa!6WudxoF5~i5$kctYINMTh~-$$ymxg@Y^vGaf%wVj)IhZT7Z*~4m^ zcsKL2n=&Gsngq2(JjbE>8XEc&P)JFx#0H(AY}g53jB#2BQ$_Qs)@G^U=Fx zjAp9&m|VtN^nu5)bSGAV1+>~M6WXkD+bXgftXtc(c`%wT#abmw=Q2Xf=wtY_xI;?Ez_2Kq=3+=9(?f1dDVQ7sTTFs)HZRx(v zV)X6T=%MCwxmd4O!Ha?7cI`eD9los{e#0FmgRR|bA;$+I>sL5W=Lp=y1>4pI7|?fw z*mV-zJ41u@w0X21B#3CKb{yTT2ou+mO*|X%c(XGW+(l&XitA~uQ52CnOUI1^`L6^> z`U;$R2zJre4~gnbtLRE^?aJtBam&>+lIe&P=_0Ye&k}i`ea_(es&3C((%TfBcwum-ck47u@c6fT)p0D!@-K) zsn*`<;oel0o(kUNk>K7V3VYvN{ly8|E-LB$c)4%Uu5ZcRctOx;KB4b(Zr`dZyC*ud zRI0-RtI<8z;|%DhviGlv^sk@m-_YvcyxhNK*T3!F|1-FMC!v2gw|}pqf4{Z=V7UL+ zLjU1r{}Ffqz%f84IslX#0BH|^Ee0U=15l3vScoa~n-OhL4@2d^*g1`FoB9hbNsq$@ zSRjK)jzLz@K{mO;W7>o47K0r2gPb0NTp@$piG#4{KE}$y<86Z_*WnG;p|1D@S8DMC z%W%#00Uo&_VeKIiiy=|_Au*33@sJ^j#G#XU=4{Uf_}Ye~M$ALqVRYBS&n(txNe)Sh zevp;>a8~=nIg1ZRa`qqOJw7Oed{`?PJe~JJx$=^=R8wf@C86jKYFi&JK!!0K!|I~L z8gj#$as#K{e$cib)`{+v(H_=I9M;bpHmDrF*fwl9a(O4?lJ3^93B=-h@UW5Sh`HRz zCGC;R79$q+BZrrVO+!X36GsI3My^zjTx%P-J~CpnICA6sknyt-EXU|Axm@e5k=xp% zb{3=d_M;A4BUeR7of1c#e;GJdj^1q>br~7Gw>Wx|rqzRt;h3)6{x0(t-^NfU?3i!cnBU0Q}dJZfY-dT~5vYdjV*LFAZ-6P<{cn|P`{kzg^AXg`tUF_9cH zk&-x(nm3VFX_crw?u{peEly-^O+1H8k{|=&9Fy5{lR4UxxfYXo_LKP@lLaA@g^80d z@+ONaCyU!AOGYM37bjnCO}>ImkvXQyM5oH-re14LRai__+D}z^OjU1 zx@Tm%cX7IJYq}ruae(9FpyhW}R(lLC~ zKPOyui%)>?ND3NrY+jr&FLA~CQ3gchiY*^!sIB6>3tl%QasCYS6WU=Ms~0ZZFc0jR zKe{Io3LLhT{SK8Od{RvMEU;uch;8(nD8$d8sB z2@WD7A=m+1TX~1OVoRXbPddy{H}aShbQ#CF%xbTz)xPACWN$5Cf9m_Z+oNSqf+MJn zAS(~E(pkPOw)9SD5r`l>i(dAJuHdWsEhR8?=azyR9IvU}cHmqgJX#6W>A%`JfAr9J z`F7IcXi)od#G!dA`nJfE5O34Z z>Fv%pn4v1)Po=zcvOYW|YHaK9Hv+b3Y)R(tU=&Tl1t)erR zfKm>TeGcy?myhQ!6?1-}4KsVoORyb>2CFR>%HO_JX*lhOLN>i&o~>*E0^P`3~(*zV-$WCKFFQf#UFEH~+_8)sOx59|xm9el7ht-2QO{r2@F9 zbmCN?0(JFcEa)l~@@iTIALNWBm|+Rj60jWAaXmOl1z;jq2;Oj~a-N3Vq&K;V4!VM+ z9@AZW@G)3A#csn6tdl}8$F5QNAc{5AwcsGwbEqOWge%mI^IM=1y@@RjWUIF(toy*x zW{p$VgvW7xvxh3iv@TMxE_riQ^f^@D6eNQLDInMQ{c$`$0;NrZoD0E6G5`}DbdWSI z$Q;nmH;dCxfhurs$b_!3=t31tgQU?xCqi+Glm~Lu4eI5G^0QVs!Vd+X6I5q6)Lx;Ih=aAo-uQvd;-D z$!kciwX?APD-t2*H6WQ3s>E}s9C4$;Da3}$O>ueCKmwwZvaWPB$cWp6pT$HM39|cv zvqORuB_O&hfdUt{?Vhi@UVWr}RBNvIP*9w3cQ)7vy(Ucr-$c6^bGh+21{(p~g)eMe z52Y3Vz+(C56Ryirp&Q&k+|gWKV$~jNS_EnI8XmSyoept^^&hc>dQ#m%Ov!6!Ji!W> z9qAREhC&cE7!>_{M`?^2_iD#^9DKun19o*|?fSNJikAZZksJM+RZIpGSoS78B81`mBP z=5=Fy@5R+^X$cQ`aj!MM-SDwp%j&hDqb>CxyR`+rk`OO`M`)I~SE+*6xn*~qMvu2Y zxAGmqGS~;aNk!k)K`JH zU#IZv!_}eZu>Sg+d;PI~*KV#++5Pfa9?4Ms0%5)hW1dIF9ecq)cG^R?_^bV*P8_z6 z?Qq2UQBL$%#qOqh20L>98q)psx!?)c+x?N}k2BsqdQiPv@&nj7=A+;VmP5K3&bqC8 z{?ym;oA2;Gi}R3c+_U5Y>7zUy6g^#7JjWEC{CW=UfYHm{kxwkra^WqL#?dFGEwvk| z+cwngq{$aC=9uGJ`AS3_c=Z&bF!55(>mu(HB(Ijcd|nsrNs)CNDskQr>&sB~T>tuc zLwtaw5z2P@&Zfjru0gWwz17W=!-eJr=B0PGBu7iEs~_0czF;hO_N3tLM)T$@iRr^( zZY@2+odyndVP+rhq@7Xl%zqJgu9Okm7?ar~s8Tk6?nu`avsAiI6%m%P8Rm0Y%>ORd z&aN)m{zhqS-nmo0O54RMid)y~qoJ#Z<>_k*o&`-zO68}%PuF>FeE+tuK&3Q?9y{ZD zpt!#9KKbnZ?+3L1O5dzYuUvmAZ-1Sve)!}2FO?nYe8_h<5R&GSS&csvfzAMC6z ze|UBONbT@o@8`x3ljjrmHZ^A9{t#X-R3Kck1{K7lyn_m6HBb}`;js1+B=Fp+5e((` z*%1sAj8GH`7ftgLia1$RBNTbMZbv9e=DnhDwA`eZaE#Jwjd1Mwy&YkqI)jo(oHnnw zNW8vet;ka&{&{PEtdI4OWZSt2Wh*GD2dT(V9gCt7(=A~()J`D9+Awa>}? z)H`)23!eGxoh;0XP?mg=m*yi`^rEOvvbeNvPqL)!z4EEj%1NJ7FKbupPQ7}&w|9!% z#GoQo*2e2ARo*4}M(TC1^1f8Xpn=Ni$`Nbd(^V69-kh%f=(B&iW-dZSx^^+mSGw+V z(HrSE-|F_I>(}0^oO!!7>3gPOclFJgcfa=b&ot68s-l}9$NkXF@Kg2Z7ABPgbSvvc zRhc%9Yko5AJa_A5I{1ALWI6>SRb{(G)BR-MpDeDI?LIpF=0LVbrd#!FuiTX1**>K& z^=JFf?;o5UP-i@UZczL9<8wp$r{13XV5IWv+_2fj^Kv5=*B;A_THSpsH)iAeOK$vD z#qsP=^g&FTJZxv=A@BdPu1Td;8&V?L*qBtLZ zszLEnw9294Lfl0)rNzW+Pn4EY?=~ncKl43QTFHu3Q~sQn{zQ58MR9}jm(n+f%3sU6 z)l|M!PCZfiUi+m%<;UCoLltTh;|0~Vw&VV)>s_bbsc!VD9I0*&Uc7LAYvh{$`R$3j z@6P}H=zDa2XD;%B+U{byzuMmC;&*EM-`*Ul(T3GtxbSOh%KyUQ(e9Ub7mj}IA6)=| zBpT`1$attK3V5ur5h6|Sg(aHc=Hx)mdKC0*VH48>a*)6Qie6KqnKhmqET$^R zaH+7FqnsQf?I(!1Ez!a=L?+1B3o<_}Y~f!ghpHV2B7-Da1=-5NbX0}do))%>%9e#2 z`w6kV*Y13OmlPm&HCg5JrJccB-?L6A7v!LdRZoYRi_##rTPc&}L%jo0rEY)rw)WNpSb##jUtIL5o9y;!wPJDHKY9 zlFhf)+UNV$+UHqk-<)UP?3>&q&wG(M-!bMK<3IjCF458<9go_?ySpckGVk!igxaJc zIZi&q(&0z7waJz5I0bBYN3f<-wJDA5oG{PQk*BA%A3AS1;b`7bE2_HG0XZ&_#L`ha zk-D_;cU)rmykkxVb?LM1ToQGqV=f+b8LKy3QoX$6?g@37J96AI)1~9ywRKs?@3`eQ zor+x%%Gnq74<$i46K_vN)jB7+5!bx>bbqOHt`5~q@XPe6e)i-5RCke3;=B+U8qj2p zI+C;O3s}4!K=zeeKDFjcyjfTtE1sj;gE9jG?_UJ~+C5F1R<^`eqmSoFytw< z7I#)IK(`fB{y#c`9IAijFE#YphM_5U>~hv@qt;SeN}*z)_ih@k>HueUZH2bHh)IUuzp1 zD&Gq_+wd`&TFcM_>&|-HgFkfTO6(*YHG}yUG0EK~RBpBE#k$x=whAX!$y+XLB#h zEj&zFV1qKTxev>wARJ*>zCl~p+z;~;jESbZiVB}0$c3V zErV(bA_-W4n2!;QTanaWfgRz*mQi~J(e&x^ z9r3!BF;_p)%uRvs($g*DKAoc3XXW4J&srw@Zbfr&1b3CFTPH&l#PX;sc2z}Nzr^^7 z6+i{|9vHSxrF4oFiB|0Cc(zVw--?we3+_KmY@I1m5HB;V*nd>l`nA$eyxd0cz;wEG zwy{&Z(zD{==~?Ss=dE}(TJX?{x@~?yL83OX;?PdCZDHI`qCQ{n$jPv6akf*Uv999C z#j|Z`^%g79+$(tOp4hg$qafKjU2*JP*S2!(C)vI!_~Z3-+v-K9WanAMkN0P7-)?Uu zF*vY0K1%x&I0X!|;uzf>O-_A}hDeS@w`YCyE|XS8SgCJR<-P#Ja_pV+>| zr6@gYSb3UU*S-z&mmal&ouy5;??`k>k9$_0Wu3KuSHMb7qG7*ssXKPn6lJCoD}NP= zcI@f;%gp4%&PxqD_Kmw_X6q`?D?B?6EU+^3y|9a##EwIIMcKva%8Q1&jw4rp+2u{x z@0RI~W1lYB)w9aq9cLXs{IIfXI6{}*)SV|GigFv&RhRvuoj+sz<+h+ge})V@PgA<& zcCeyVf5tpJ&$6*{yUIdWUlKci6)DQ^8&+L?t?N9m^p`)h5xQQO?!0L1l0WvWx?VZ! z{N0I_KS2xKtW$Sg4k#*|CRW{Si+24P_gDCpFZ6fMuhF8{(0F2&okD(v60F6`}H5XR2|kg);ja)2ys zAg&w`j15O32ShoqAYJdgt(XCs}>Azfu7+sPq2W+T7IA-`p#z|W;1W2dCcrDS2J;>x9hVcDrA za;X*AY1DFQblGW*b7?Ku>FjgqT-oV;a_Rlp8A5UyV%Ql|av8JP?-k|Vt7K=wrV|@aausQ?DM!>A>2NB+BY%fwBUJidun+E{Cddf$BY5`KdnnUz@5v1?qkr z8X*N5F&qz43La#0XciS{R&r=H7HFZi6dXC!J2`Y_3v^aFbax7Lk2&-%3iNI{^zjSz z6Ku7|`_!&B^|=ZSV4Q{$g@y{8Mrwsdx}1-Uw{_?WweJ@`a^-yNQ~21A(9vG7SJr};pk`8em(*}|t~HfBA2W~-dfE()LBa$4dSS(0&C(d{Vj zY-=4BTEVz%B#LYlxNOymY<0OFLyEL`xa{nU>|MDWe2N_Wuw0HIMY7GkGI91!DMhk~ ze7nZGMpe;^PA=zxBIj`~m)RYyakQ*6+O@J*Y!c^T1=`gWEjy|#TFIqVU*y77>;dEU zlqmL8;P!%T$^pJhOrpK7xDZbr0Eb-eeEIH`xE^Z7zA@adQ;J__bH5oVluc6h=1LZH zb&xKw7x63B403q0%I&vP?03xVe^IQy!{yUjtmM)wGg_<-DfH(mLBV*?5+!JbT``bj z05K1Ol1F69L37?g!cUd*7lK|2C4?vmi{S}RDGARmdDF-poSo#x0u9Lv3Ng!vE@KEW z7^3-I!sP&_9G=*VlGt0GID989&~C)6Q6$}FWHweM%Gf44JAfz?k+|qcPsp2WT$*gb zn_^!oGmeg+E7fvQenn=JklvS|n=h1!iC)D}cm*al?!`Rk%^WDr9OuoNEmZ^*e>hDF zrrJzRQAy*nv7qTDS_Y7M1@f-tG3`30wkl<;BElAV3)IRAbomO4xIgUd=?C(DWN{R4 z1;#9vL{DKdLY!bSprjs5hK!vo1_(z$6`Pf~1#Dz6O64L@+51YLbPMG%-F!y23YJL5 zOJ$i7C02&bvYqGBt9-S5o8_}*9xR_^E>QQqlx2`Fgm0f$Yvwob*(&%VSvU%6{cIEh z0-24m3BK$S0+gQ(iE zmOWGwW^slsDpOQis1+$%X_IdyYL01Zl9B829R3fz{Ztj z-H-1qP&k-pxZdVhYs3*Q3kIMsJoXx{syWT1SDVH z#+a2W-UvwgIl~N4Bmw2p5*5OW0+ia&S>4S^x+8I<%iYf?syF2X?!HB1j1MInl?he4 zg<6$aQk^9(dTYhmd)IaJJy?2IRAX0O^{G2>y}OaPQt}o;(MzhstJW7A2*L zCLZkODiPQg>8AE7-lv21)+)Uc5m;73kvNtgh;$RCViH>erX`@gkZ!Ih(8_V%m`tEz zweo?h>vzrk-Fx}_!)S6jA>rpj`}Q`9eM*h9oFh9MzkN0jbt}M=0&sg6e5)5iaZDTl zY{*jvkA6x-!6Y*Sqjvy|H`dp5o97;a-wmKLH-J9|o7c04Oy-p`)*vdQm+)H{EKixp ztBRmWK%g%maTP=P>$&1gH<;l7w;&MzAaBLzhj?ud*aY-fti#;n9G~xE+(uIWt%a z^%VgxJ!*dY9YfTszr@r?u0p0oPH|u6vs~kRWy${uQdVpaBuPbbS|UjUU^0^d_l;1j zo+x>EAjvL}zqz~V%9@eHX3DxsY}Nyzd&2z#flLwZw{VAF;|Syg@cE;tf&#d^0_n+Z zWb02zKJ4QRo`|b?l0B=_IIh;hQx?YEm09kAYx6{2od|(`iV?cQ$&QI819(T#yka$q zMpzJdhG%mJLaoQk%-=oLSwrd7E7$G}-oglcemQmJ0ozfAjJ_bF`E=LqCQ9O$4N?M^ zW3F}}vYATY62L`~^8J|rVz$5mh!`mbKyrg9kMIH~c~#ujQB`A*o7T$T12{3D^n`N7 zU&@df0BNciY!OH86NciKfcBgcrD9;U&o6c84}E%Z+1+||v>43em!WzuY{N^F97vMH zuhr&NV<5N!>$%Z9S2sNWq4tYNU(Efqmoe(lm~ypiG#pW?bLOoE1vb?7Qp_OMQ!qLXru)OILQ|Y1$yuRY;PGaj zC3Pd>`mVvM)S(b)lo2p0bD?hdX}`7{3xd;zzS8RYrNiCx$f3cx*QeUgV~EPLU-M$H z(FYbGPNm-vnCL9jb)n+$De&=y9k$;7U>m7$o2WSiA1K4M6HkdsFy!_7_MX^RdNxh^ZQ>7Q zOCRz#8{{`ZwVEKTK5?5KD(aUon9l|Z##w?H%$9r;+2r>-~i zAB4P9$5S81;*At*$*A(RIARHN&qk;!45>s=n^6h6Zx< zbt_Z*ep;=C-ErrmK>V5AKTms^ZZKTtJ$#6AGG`=5^`9QTKoTPq)UAiRDd0Yv=aAni zxet;^0JK$|4Ns_z-!kINuv1bu&*g7p*4kVWFU1CYJsVh#R&CH*^8q6-RP#v37HVfoWjwuyXVKlQ%UUE z^f67H-2pnG6$o)cvf0Nfd(Y?c{rdU@YdKv5!Lz256f)RgdY+~$83~TQ`cZEDVQ@LPUqaQ_)I=Z^9+yJ)eRtYs((7% zAMP2FqIPFp?|pq~Iag)XlxLga{YjqFli5AJsb*8%*_@HfeEuYNoi!QW&!*w?NsB&i z>CvK5L!uUqux!6~O)0E9tju)jk*rC%RbK>e^5iDZXzx33XSiI(fY|=K1PLbtRPVQq zBunAv9DBaj{b`inzqT`a*s5}|)d&%f5LIZq+MaMMkM@smgs6}n% zr49Z}l3tJE_j!(h+#7^kqacxaHlxruFC?h$9!e{W&|hUkBk@PIG%;m(sxPv208tjB zqy?D@kA8h)suDuUI_gxMUGit+)2}3~nQDH6?+YM616kr(zN=Vm%8XmzhJ8lEGxpBr4Q?D%G z1*(Res(fL$WJn0blepO|`zT{BHTgi21rCXYvV`@p8gr$tlWR<4iarpK33Kpzo&tg% zZZta?8R7d}4xyP(0X8U-9VZ7AGw~3bx>9qc?1QGzdp1%!O0e;~Rk+~nyiL50@q%5N z%j|+fZnW{DQ)$KQ;)|Lw<0Y4tBcFvkB`bG;`;g$PEGr8}`z%ZAA+=?Xb>2H6IJ^uFYM+?D6r^tZrAZkY}JXlp-R|<%)&ae*(p>_qNus@?>@ujw--e(WXyRoU! zw~9;HC1DFvEymNmTrUK*V?gDqV!c&E1or(n+!v-sPz8Hj~bcZg_}n!>V+R)w#+WVf5jy zLSCnA)uv~^p+n7!0+dDDZk;UJ^i{*tS`<7W)KzeiX?f-`Cj0A=|1M6}?*2~x6yCa5 zDaq``vV&y%p39wz^vfnnRk}r-$kiJ9gh>4o^W_2Di9+BBW34F5k#sEo^$^hk14tqZ z5bLC?Wj<4V!FpC&=-(A@MKM(^_gAd?RiELv?WvH?+CMIy3Nkbrr}4yG9K_7RVcnQM zNYuR~02nc9b(tihP%oh4esax(bpG9uPFdp;*SmagWR9goRfR*8B8__RvzR2`mp5r| zqYknM6~e#9+3A9@dTvF(@jnKRQWYsb1wJm89-NQ!2+9bhqLfDRM7XdhM18w4`rVQE z@W{4TIdF9GPDi&}LgvR|EMx4=W?IP3`m`Anc)E@zx$LDR*Wgs1q69%4hf`zSm2r%_ z$?X*E4V@ebaQuREEW`kmXD6KS;5|Y2n8eI&vG3Af(saEm6SI!IxTU*kt9-K#Xlugy z;Wz9P|6tNzpML)bliu+>$669@(yI%v4wG`mRb7h^vV^08S_4f_0|K{^#nO^{hwKay z(0I5z`d(DLgVo8xOO_OU>YLB*=IpCvn;W9l{bHfqBv~rdJ}4evH_x{uMx<_UKou0^ ziN~KHIoM(R_JME$lc+OoG0XU=px_)v%Aai4NbW~j7B%eQC{=wu#G{giBt6dEAl)$0 z{$_3a{BP5Vs)c2=yDmw^Ka}$v5-!^M|DiPA<)v7-Q{t5X@Jlj7M+UE^133?$v(ONE zZZ&GeKl%~cwh7eaZ#1YZDe-#aZ5xpk5M&V5$Qbh;$5s)YV+78{7YhqC5aglckqcDS z*CJG!lu5bE=C$g7bL9fkr*k2KBHEK>1(r=R>n%L0j;}vj63KQI96pSB(&D7+{pPDQ zjJB9-QXtIiB=_p?7}dl6$DeN?+F-}{M{sW@XeT4cnxo%GBGkN_fDTX%;p^$ zE5{@R?S9-D`9xOO2Qatr&v}BCmLveHPA6BHVBC?5%La7w3BniTD)N{kWl0B0zVrCCR zD(Y9`6!feEG4neW43KaNZA%c?WT!~rXR0};C~<5zC*>)SmvVlNwGd=hB)PKp3VWPB z*Ku)P70B#tMV>J~YZj`o*+6RA4q;eFO0@6V3B3KAxHwJdSij5$M=>X{OELh#trYQr zfwB-9tzDuwljTu#kMpCG)<955R6`PoI;<#!#@~keS!xgcDUq<7`8^TtHnm<5dvmsJ zXpr&Y68&@u|IfhrP$Lye+Gv!bNQqu?@Wc?n@^-k0z2^ z``wcpSU}eddSA`(dPr|vjCYQPa`GUfdvgcnT0%uhSuE9Wx7D<4g?^vt>1wNF5hvPb z$=;_GMxw1035PqxDlJK)M=&Q5b1kB=wD8Ba4q_w_x`!m+o4T;hbF}XzuvHyjT^2rr z&w)n;H0gvO=fdCW2c}_i{$d(@%pUIjjPEySUs>;ZtwjbmS>H>xp>+6mIP=Nj{lTle z3@ffgwKzx}fwyq)OeaZpTyK)w>S7G2L@N#^XLy9rvYtzNh}~A+@uuiJrVd>n2{z z&qXZv&ruS+^-Rr3snoUFM!moBfq33|cRmcAVSnOPd$`QlUwRCVH|M>u*mIhmDD53kkSo0M_f4!t0j?>{>M2lX)rxG~@J2%vQNc((zxofr?Y z{*(~aDUp6wpYVIgHITjoiySarKbn298wZv{%Vz}&%VF{eW;b>Lnd$TtIkE6{@QxMfqEbPr^yO__e)1@9|pYl@D)_g2G`dE2n%J#gb zxnc>IYdLIhnPGsHO*2m$lRGmO%vPa~AyBVzk_eI~kzWuIyxJ*xKhG5S57Nj29DyDv-vX0eD!+nil`PA%@`HT*^p za}lM;1&@9DCT9TfO(y=TvqlWxQ1BsNg~V{_MU0enIcNn3N*w0lhm$NYA~sbJ97S0l zG|5A;q=Ss=QxWO13AJ&{Fh|P*n4Va>`=!pt>=)LoEYBlE% zMc+U(A7}3ill#c_&d(f=ft;H)95|u2Atj8D3BWyUf&z)pQ{Cy#)=9nANFpF&xsmg<~8ANtvQ z&_P4bYoS1Cf@VT%;z&+r5uFcQYmeXU38S1+^`q0%OIeYY1Q@G~zQ}2Tz^{X6#7yeo zdjZZKoUDCoAdF#Z-xS54I>ouiD&X+5oPH|- ze5E%COUM9gD_gF}%rZXLN~Yb)We^?*2j%lkIJQXTZjgg+)|y5x5MN+@)nZ5B9fFLn zn^I%y)ee&Clug;7UZtR94Q1rMqr_khAmUJ>+OoX=QJG4n1i_I@Rf~?2=%&0O)?I`s zN_^(yup)nFB<*auyNuTQjUh1QVwc)bvzpY*w&7{)(TF#CP{@TJqzieYOzv7Qb^;0! zutC-q<{Ua`RRK3-f-{=oPLu+Qb`5!zFtcNBvZO3!L0)EVge}Pfk>83Jw%n{ z5$sV_o}|emVMZ{KL^l+mw`M@AM-7Bxjq*i0^2)~Z@LfG$S%{lG&eblN(F_#TVPwCz zCidWi7@>bL`=HTT=NbXrd-Mbama+XwT=26x*L-_Mn&zv{?@G0oH2PE%iZUvXKdabI zi3x+Fi%eo5+CVY@rxCri5kF8`o7Vi=KECp+!@Lp^EdQ|(uC58YuzMVSQnG2sgry6O z70IdPOFZjb5A`$5lSpyf_J>G?T!D6S;I<__4?Avk?u%wGzSC|MT3YiOF*C|o?XF+Y zJ}&{6Mgaj@LsJj~A&36U^Uj=ls@6aQPP}Imw15{xN+B9jKHi#*Va_o=DeVo8j19&F z;(*6iPTl#gMge}a7e><;sg}9;*who(ZrV3_bc)ZFzC)BAMbkb_)^vD1>(HV^cI9d_ zIIuHsO&*}j{CBphw&20*Tr#OVT8XlU;ogrZ=T6sxv`T=)%jbq^P>Z1*$w3XOjQ~9J zP4)~5N=;7~pORQfE?GeQw9gv3l%OFG9>J+47=lN_3Lv=20o&T)lXcUKQ{Qh61iA&> z&5J(T1%7~8k}z12_~(H^R@CNepsRI4SPtV%A`6clK5@6N)eFSBq~G={zr7|u>?Cs$ zJ0+U^=_#)gE9JsXu2T565_{4!j$fucG%D=SrOqR337|C}kOs<^8)2y!ud2cbp-o!D z^|&*cd5$(+!!IFJrn2h9ny?Z0s!4teZO{e;vqP(O+3>w`n79P+jn)VO!9_;l)Nc0l z8OrG3=OIkRkM-Ev#q#h?0327jkF5}RCNF!Q2UvQsasSCBvqt#*7!7e}5GlKih_;;Y zDh{yR?XBZ%}rZL{yXxDTLHH}F$8SAhWlq8}l` zps!i>8SqUogvdB13DXk7yqI{lH%q%Y0Mk9|A!n@7v=IYZVmx^pwX zOToit@&`GpEpy@@ZY8GKu&AUZIhf=`=2fPY;WnE}o%+UVIqb9H6X}0`kjiMyCEjy< zn9Gh6!H%CNxL%Dg%~v-KVRA^0UwDvu8KhXywWw5$YCg>|!3zOE`x%pM%pywXBWjvM zUP`HFKueeenVr+n@+SK%1Hs{235g6E3DYf8Y55rvsqE4G_^%PTBe+Iu#VtJ4D6_2!a% z->e_ro%^k05aqIHOWM8DBF2n!1Yg8s%k3yLJm4 zR4iK!ReR(RxSB^GV;~b>G?2k^_KlOm=V?|Im)7-ZzrD83qwCI}vR%pj%oLOhAxcWL z?hJjoh!rI&lKB9M7$r(6UX|r?nP&dZiiy=jgj=xPOu)A=SXH1+zY86# zirra{pMGu%dzWdDX<{q~5xcqjtZ3Tq5AW}NC}(0B$oZ|b|B1P&nOsLs@Ibt(fKCXU z3Hezo)71H0rm@-J2fISyn0e&8CvVzKk@}`ydH!YXCXMYTnVG{@@60-vM;Z!8gxhmX z%}plX88#xV6P(i0aZF5bnL6c$RBy(A%1xZVn*e@cZnRPYGFvp}VhWEi=Y>?XH%mNx zCd&w`&I*!c3@J#B=0W^~V3t|s&6N04lha%pgFq=sW~IFLEX(eFY?qcrtVy4|_>>hjcVb!k+2 z-0rw5eE0VI{<`z~dyAD&<}LQ0^Xl1K6-NOd@vJ&Yty^;zp!?RtW263l`=y=x6}S5} z3I`2-2Q3z>iWa-)54Iw+w{$v9Y5ev|nepG{9xQYot=t~1D;#h89q)A>X9dq=yZy2o z563K)R4A<}R*#9gPDrsQl!`xT{eLq0BhU&zM(mhcGk=iG|H$a4zn@FuYDw6N zp)Bb9sigSpZb<&YUD+P{tK#;6{n=q+@G1Mu=|jZ}JO2x(t_v6Jh5NIWN6&UmyUwF; zcd6xm2m4>%4f;o8FXI*ej5z&{&YJwtf6h&Lk$g9P-*r`iy{b{XK3HWQ1^!U>W6s3> z@qc#J&~-D0z4@Z}_pATkXDbsOiidAMn&xyZ4u1T5guVT#h&}hm{=sGftXEn+mS(;4 z)vK0TUAA$r-VzGu^@UUX)@ch9&L4>SeY~DeC{i#O&!Lqr7cNpboGkF9^&OT-v}iO< z?8RbxxM=ZsmfYK`8zQli$y}Ald-4%trBj9488YvQ#mi<&jgnhEx#0&gucmFymI;k* zKF2;9Ntch5s90=pUTS?$B3ZfI;&rgt5h+=<+VReND)I8w&RliS=L`8LshW-cC?;7y zuypO#P!j)>&M4ELmOYlF4!2;L`rR)DS{Vw_G7bA*%b&=uj+&OM&(*(J>Wr3cI$mji zdwom#PV8`=L7qS{My};_do)AVpG>~htE@L%PgFs^?c!*;?Jbr}q5blw<|pdUx(Xdv z=Z8ye{^W|CH!V9hn;G(ociO0%>uW5zWx&t^T2E}&DQH~&8kZt9fJ}i9YZXMLt6>#P zx8FyuaB7J&7D#XP5B2GxvUNCDc9?YpU*({6B&?IrCQ4*n!zNl{HOwYP=6KL1R^j%} zYaL1Uz&2iuCEPYa6E&(z%JR?BHS*;%yr1_gN5INypS`m-SyDK{#-y9D)eDu2|a?C-IX*%YHutYfKMZks~^J5f_y`vlfvqjf<`$>UVWqKN8pGjl0Tk z%T9KrYwLdHh-=$%C&|n9({Zhr9T%&SFFUV}M_zW_-jcXsK;+tP-T16gZau_8qi(%q zieUFXs_T3I;!yn$-{W?i;eYo%CcJ+)gkFdG--0_D{x^`a|KhFt{{kudZ@hJV&-PbF znyS8a1>lgdX#ZD8S?3$Ke`ntgm@Zg-ANx1U~*Wb>bpvTqYzU3WNce4c+o%GOX|iQP3ch1x$MWhLt& z_kHem&Oae#JVhn{7gF~BH>8Z-<=6iTDI?v18;i7X%lgyGOh z31@%iQNj7Rrw((|<#oC%+u*u$_;wPuF8bSh0=PE3zZv>F^LTm83~+#39lh%C(h$7l zLZ29o7`snayx}cr@muUQxpK>v3rRNVa(bKT_)0c;fv@npYx&j%R2S8&{TF1@&4}{4 z*-M$~S30d*)dV>{!cCig_7c7IYWgdk=*p&(ext62wIOU|G4k!hBhl^ z4)rN3T42TP8J9#ALs#^{BsK2!>Kutt=USJ_W@p-)Cr^H@cqiykNG zd5yniIbkDc2?9g?!1t#?(F95-;^S|KR5z}sB9@#W^Ghh_-{-Jg@Qxh(!$gps< zg}~sNU$eT9bnI*Ydgwh85psFD1TD&fO~i>woU_Vl{L}OSv1*^zz@(RSbeRNP{XwM8 zV#oBr!BxW0FAv0#%1Qb64Oxr7sDiEa?>~wvOv|Grb(VU`GVK-$rKb9qI97#Y4baAM z{AV2d|A5poUNM=%X{r24)bqf5@Q=mvD<;p4O5vcQ7}*9m36XJKrkuS!jI6F^O{!w9!k2>qI4o_yPBsbaOYQ&A)}I+(h^ zlgWA;_n{!NJ&*{wO&_9;thLY5{GSYOA@NL!pLu0oM5i{VwiX*vt}gFLBlTHGWxXDI zD5T1;b8*ZYU5Uw29X?A>G8+_b*xz+V=WG3z%c5{!LA4TGM561?UyS`n9D9{gSLlb$ ze#M}f<~kuc>$AHgbGS=0C%HU!Be3}mdfo9SV}O%j7bQl{CEhowOscX> zs}}j`5~@cEPYnRUqjGsObgWV)H)EA{#O#tEPcfpilA7TOA@|jA=*Tg5d3z4=@E6jF7R|DOir})X@RzFW|!mqZ^cxkYE)U+1D z?`nU+vySA9am)MY_L0X5N8;*l^$@ps4jTc#<{;n)lcG^xcuRkOsvZY$`QTVCU%F-l zHeXF0M3$ja%l~w$ge{cAJ1!uIBefb65=*Y${^<#^u?CQ;^tn{`&OB)nkZ{Oj+9vz>yI}s;J7UL)-JR8UB(~j37Ou*Xgo% zn5#T<9Vz--^(us{?Q$wgjC1he5YNGIE)n&Qfwv7Em;0ZXmRx(WPtU)KEm7lA6>(Vi zw4~1jI4*m3k>pSp!ZW@4a3~pT`{eN-6Y*Q+SPoUgN3=cr>A8E5AEKuJdNJsFL8YjY z1O9w?A$KizeAN8C^7@#*0TAVMn{&3PLA%t=K%wo=6(^Q0+K5*CCADl9g{q6s5EC@9 zo}e#`#*HLt+50|^nD-xN>v<+cM|61`_A%WY`l#w#TWrdMxvTj>YL(cN^(q5JD_<7P z&d_K;^GnLIJsrSzs&;^*9g9&nzKmH4=eJxuX2CUFi(L>Row5qAp2`9p@k-9WxsdPH zGMjVotee7qGKjK#HNW+DiffqL>o(0)O+rVEq?8)xE%ir5EMzx-Zd6DzjF&aayqcIY zufTVP7%*of#`0G@?A1R!!kc(tG;bzQWyDCD9OH`8>X)c2o+ki?#5#r{9DI1m6p=4( z&Y4CjnR|T#5!HMT+^?L~8>6H^Ck#?J`oc#GM}TFSxm)gOu>N5 zxXEq!TFpFxBY5GAg( zXE4w*gR_VToMiMvVU`%ggqzo(?eR1Fam)J4fI1CzMjnJ8Dd{KV362kev?RDPWBN z?IXW*qkfxLvhVRgp%K>eDOeg=I%T-D0xIRuH}3X^ukSjT?6DKehGw5s`4L~X%ew+mc&}i z*{I);(YYk{v6!Rp4j!Vx!t?W8VQ`JNM<}W(W}PmE?kLsyPqb#T+wg}QaMtFr&q&f! zmd2&8;(kg}yHmsY)+fp)CeB!z9!DVL^r1Kz*9YHUV-rkfZdRY2=080Lz0mC|!_e*K zQUQJA!3!h`B-n$eh91IEcG@-uc-M5Yx8=gnIkrM>g})p_4g zK9E9xIN1^xUKFlN_z&3P>9jZlRQ2;Owig%K%A0iGzaE{Cs^}kiU3%0rxlG5b&yXdR z9}$e<%I>9)s+S(9@$#i+uTjf%L2FRDTUU3mMVX)}9LiIbpS;208gB;cC~BW?=_GM0?c# zS=i#l?L`~*?@4Ue&38TQ)e`pha-!?@_XPI#`tCl%5CD=2z%>cL_YELS{SS|DvdaJp zhQPb#1dT}`oo^sR>K$Y8uCx@$av8|RfPzS&I80Dnz9^nl6kjt+;108RiGnkrMWoPT zCTIy?v{WiurWq|ak5;%uBN&2^Qb8&vL2AB18mU2=%|Y7pLAsYg`V7GaQo%+h!N$J9 zCaJ+@&B5mL!T;_NZY>pJYZ7Ab8{(K6^1M03c|OG42uvsp-hhL9;S}AIq|<95Z+KpB zSCdUwlO5=j;j@Qs>y!1vL!07AC#plE9DUzoc_{o-Lvh*3mJk$6fKVVD>^K*eS>pYA zH}n7yp2`4N(kDyip(v7~7-S32nvZCn4fSULl$($R@ld$=h8OUJHNi>BcOyFIEqhPG z;u#{Q5s^U4h+-*s5nlt{u9z#ji_7`aHvW65D&$O3D~=Yq=PVeBTUbO zAtHw-eANUz(i~OL9MdcnbJDEmX#$RC0Ql`jdxk~rmqdT?4QrwRPg2B!(p0^qVwWf) zK4^q`n1Ba&W10y=t0-b{q~q5iWI%YRM@d|7K&&@I)Q9=#zp3%s%JCeGuo|{ddeb<@ z@OZkk@ay^5RUV2B3KCA!#MM(`5cXl{9iUN8I@HrK5{QZSWQdnAO`<+cR47f*2&YhF z1k|#zsWB!mvB%S=h0~QLdNYIyrjd^G+`$&v_`?(KWC;D$;m=;DEPY7=!bx~clL?iQ z9T`6)tS91pN%1aC!S9arX!-CVJ{jLVIUqb47x>|wX{t(hgx8&*1Q2Sykl^c-8ux(F zTZ+UjH7QVek&h~|LfdbGpnULt1Ts@TT9-3ay zOFgC!K937~C7q5FPu}n+Lk>tfi2wrw5^;sW3vlp9UWz`;bj+Vj_Q1q*Hi|XEtl1&z z>3~p>F!}TecoLp5$CyoxLrL*3T&F6WN&p++{{M3?(6?-{0``#_rn;HbpeUgD42`ZU zjltCYc9}fFc|BHy#T84<6db*QgEHynCWko#L4qT+{L;>|6@W92N?cQe3S$Wdzy z32deP+u$DX7%dLD@6%NKBGBb*C#nBceosTL^5gOQIjf>523f;fP4nN=fs+BBjI-Lj ze|F%BkDI|}PF+_FZ~}02KgjHUf3`OpCi>RL@}#8Flm3aKz*2P*izHe5vG-qP_on?n z5XWTEhf&lE4$q8^R+6Yyz9Q5NY;ljp!Ioo)pfR!cEuSZYv)NT9C7E?Rr!`5HybL60 ze1b?NdT*>P!uXN+&(wjLyWuWFM5YTX(XqQ{noV4E5_wG4frSAM<=@37 z@PPj-b*$lTaqj(daip}(15kM65lj-*xypegl=vo6@m4jV44ekbgA|*+sEy%pfyZCg zFwUSFd2ox%lNJ}CMvs6*xsAFl`IF9NpD%W!fPT7r0(8!)ZGjtJr(nP|`$Rf^-B3DL z_56snV1@-vGYyYXD5TEVW$}@vbUHKpao-|M{zQ&bi~Y8A9ZbKD7_T$pK~tf6CH=^I zQuxbb<8s6i*|M>xb%2x5tE-j8>hG%Q_|J4~M$(-1Lg_a{XrFlJ*cJ7|GsYLw_|?Ah z{uy_9+sdz8%C}u>w||x2{iLo0WG6c(_B<6F`as^Sh10zzksqIbpuKkMyUz6L@b44& z&lxcxmBrrmzb98bIe%ikyAB69+f_cKjo-`&1oavVH0(+N0HDi4zR9@^qq&ElrJv(MzIwH1wCIImR~iRPxz zrd3ytKLKL=IQS>aP0N8%#FZm%&VUjeQL629k?LlE7GA_AzrluK`G@HQBgV_*k;j4l znkvq2>uUoGwzT*6PihC_o$!h>>09QfKjAwlWDQHDVq$9j`Cuj&w#aXO6#izRYLh*uIc> z=%Cvcl#BZeuqK_P18CC!`t*^-3e~72wGxgFplUfG+BbHuepc`t7jR$o>t5(%WF2@j zL!gdrGeI4v1+61YFhGTn7opnL4HLMP7!#$|t(%eK*QNRISHh~dkM!SGBB;O7g~|5? zi&!X5KP|t#{&Fi|8ks_!TERHrL8Xq*(+I~@dmj{a2MvAUYz$e_kI8HnS%<$8LF@8| zo+QhL@OTnOF<53GW8ro^_oW4vDl%~Ue{KzWLcV+u&aYhBJ$+n{=D~A)+8S+FlOt*x z@gzcgsXEgoqm0=^+e#%}oSImSH*#$;`MGJUSE)%(k8^FS)mvJGO?Tz}ZAhlhtA!uPZ1xPT z27)(S@z6eBU~Wx>A8=m5-dHt6B**gS;vS#wId#_=(c9Xeetmtm@OgqZit@?T=|V=A zP?zso`7+zj!fyedVnm5Fu^`TLtHFGqVh-8=6mk_3W zK;b>DLXY7;Z-W#+8rQt`;CdZb&Pr5OWIGP0!EeVW_5u%Ki%>4jPJUUNF5vu_gVxp8M z+>@LW-u<;hW^>M)!>ODj^2-n0?@biRy&QWZ?Pe?-bdT18d8d|J_2abIa9(=#Iy@TW zPWNKB-eoT3z&b^l3!d5=+8bfFVm4l?7wA%~gJxJW4Y0%;Q!xKjo4V$0`5p?&gs_(nwJ&tHU zQ)W=Q;`G%DfOh<{QkwgkL~crJWjHE?&HZksx&h|N4LF{BG+~7!y=4Koa`N3o+*Ma1 z5j7ltG~rE2b*piloik>J5?ud^*Zg0Sdrq?zkc>rJs@$YIlsuGKpsC_MG8o8VA2cYu zS#3o#;xg*1pEm?pi1X%fsx=XI2l2WtaaGQ%r!v;Kw+|Uww>TXH=u$V2arFQOLy(82 zo=+!H2u3;*iP4p*1mY~R_cHMPktbbwM}4iNyE1`F6U0V~aD;Fch?&cl7gjL|Fej*_ z=B-}9n39Qe2^4?bTden8AWOuX%Na6d4Bp{npsnhC#!sFu4tD3!Z_(UyE$25Cw60g$ zCQj-CW*&%#&`AT$|s+_9q`yS0d?)6HYOS4B>Mr zHr>l2v2M6yhLcEe0*!^l$)7qZB`|T=3UWcL$q+Cs?HCtY1-4lUnz!_v<-dRXA^JTq zN{U@6y2D_CzZ@?oGDL{0#1hbBDo!4Qhxae|(Av91&ipTl+%2^kfKLjck&Mde4Fxgs zX)^>^1z1yv!km4pA)iC;iKg&3xn#AuZ1>R`GEC(*d&7$>pv|s@{VBlVibQF_0PWCi zDv@^aPu!DPvP8FAj9v_bZ^)kw2kp7-jaq{R_eJ>JmW(Pv4lvqXqxFi{INlI(KA$7) zk4ohBB=~|ykDKI|ZkLTEF#ITs_y|%Gi9?AD&=+E_rpD(}WxC`88^fYIO8^bwGmaNE ztD>=i#rC)(b8CXF_rKcHHUBx)?Ib{}I+vVPKKl5;4xiR{q!dKTavus^rp9~ zc#5K4AA$DPe{Jz?U(3Ija&Gb&YlK?r??pD;-951^iMYUfxtlFAD-kcN zey>)q7L19aW9(VcKreyR(i@SFn#nwI-aOY1W z3ykQ({MG{hrJ{%y(x>RrlwFUKNNO04P0qU9x3@4&*ybU}lr(CXf<}(qXo%HzA_y;8 zY8ZH?0T^4G*G*s|5Lpz77(qvzrnyKxB=v$b+4HAzZ7i=C|MLg@w<`h5A<>+Q9tJh( zj3mR#G#B$gH&_snCuD#T+YNv(Y$S!^9tbt$R)-IybD~rg$;s152yX!oWMUesyZ`nr z9S2bH*{+TV?|1mh#Ij;Jg05-)oEV*5-!=LKG|?VGFE^ zx7q6y;M9$ZKIb!UYsZhJB9s&k*NWB~>0qG0HiA0iU8WZ;``Yx^_)YX^*6G+-4TUkb zfi$U4dQD0$%^F`Uw_4R9gq8i>L$1uCyp;$w3mV$LPh(K*t=Bw9hkjCx^{)rFzU$~4 zBKa#$A3XGSN{nPj>yD)(5+-#zS@fLx;wZQcBR~dBRDL?Yfm+KK<@W%X(seTD+|PXY zvrpgFZLr=J_SsZGtJ+L?c`n){ENkQu*-PBFiy1)!oWItzFxP~hhRxa=KDGSfBF5=NNuU{Vbm@&>4`UuHIX14wFk)GHi%NrtFDAWHNyG7%n&j2Qi zV$J``uuW82O!}iSV&ccR1=;#NFsd4`By_f+8NgQo(mah|nK=HjEa5osrFwIWJvIA0 zXfG^L5PPyPxVieB6PZ?idr_R0 z1B$u5m!LjCN?r@`vr@Sz5?Xc0jW<}%2Nz3R5lmF57>k>jMQ_>&R!*XM^%C@`aTS*H zxbp*~4bV2NWi$`sPu5y2_QM1*EQ{n^sk_mFF0xLQ8?|L0eE=W66)eTG^4e!ps#sOji9x_X9*s)Z>udzN$oZnYs5VJtDbx@BWi~6 z84Z|^+>YP4Hj7t59a4UFd;5HF#L$tP|7&z%{W=uiO!4sg`_sW1Do3{%3Y(PWc}AN~fq`tUxc&|DQ*d-plw z3ut|`%KOji_qo^>WAUH>#rzx5go20XEFFcP>YbS7%M5A4AsAp$a=8Vn%w(;fkX)TG zOca|23>B`hiA*MLHxi-vcV5tf<~mIOhwG?%Z$N!;33)Asy*JuziP@DQFbF_C3Zs=Z z!@0#mA_9-ph#?NT1a&7K^H{0Ua|rhZDcCY(^8;F42vBc{blg`ovZa+$6ocHTwUIK0 z*Pt%qa(J#hUxM9%j{~4YyGb%x!q>j9mh9#2oMhTA^p!qMZF|0G9XyVea)t=d;I3bV z-IDS)2==|%7^^O2BH=AUuu&g{lI*$WwJJ_6Az%c%A)eMnGk#Ly)!V2COs_Xu)Gi;I z)Ptr;GNSb?T0WcOdXex7!=aH+)PBf#bT=&m)*wv`GSB(Gf24I_{J}<`Q&l5D#|k`1K6} z%70EnjI|g{eZFf2`z0unD&Cq~iI6{=H%Qg;7X%WE&|5mA#UuyeagSEKD#h1Gin;WKS6Yjxm*ZRJz6)oiL;YW)-19Kz#Rv4EmCQ&pqo+NLB064S4HeTOku>26z>=8%Szo({R+cZ?&v z+OpkT4C_8OxVSaG_@lS~?IWRG14J5nyp<+Jo}cyCwHS^z`wUH96)O9dj3q}{c+lnU zG*T=~Qs;i-xt*16=N6DRvAGnDO8ab7}<(dqdP&)XKKEV&>;V^>^a6dyR90Si1%^rw_$Dou11}iZla_rVR zO@Lp-T0}~e6A|>~D*EvMCa3-f6GBH93=%FP7SE+V8?1dfc2%DZ^udl&QPwnXDkV`V z5I_P2(IEtnl#QdSu~0SP5x%Vf7(s_};rGHe|8=t4o6OGIIAE&JG8uqE68jnJZo<;B z6XVUr=C}!m6za>0@K-GBS+_XNzA;+W8|)b`nQI&yM~ z*DZZ1pz1VbFLEfvt5T!4>B--zhiHz}SnPkDpLQe}4~)bGW~4eC3BlZIaaEH37oPbg3%> zZ@P}yW({vnnI6-)G0;8A^sB>bi$76*~8)PzW zsp^BVpq0^`f)I@F3=IGoyE9Gzgq<*nb+p;XQ!qOx{?dv#>QxNbPUv?fJ5J>b7EsK0 z`5lT>o3&9#(I)X~)oDi9@^=V^zjMNEvNC*^c?xSpudagZQ`JpX2^FmCpc|)> zjv0He-GPRj-Ci5iXqmKi{uMk7zXiWidqws|)Rn(M5~R<2rzmJ^6)lUROrpp|tcfoP zdPOcaF%fD)&7C5AyDhB$6^TsT=u!1q6S%)d@uHSNE&9csSe zW*c<8$7GQfM>BmF5@EVRymKR{Hyw7uN(Y+qd+f%%sZv6y+iiAljgx?M zl5DpZV}xMx*>)ArQ%gjXl+4sqMKv2SkVOIyu#eenPlXS zga8=pY=kL&F-Jc2BUm2iqRc#ZhO=TstK(-*H<+_rdl=#8q&G{|CWCm^_?|tY@YU)k z`T|KeuEo7JrU)g=DTdB%hs>)<9C>l7&3>W^vRg5`5gk0-*tHFy?sQv=qPjIf5 z;JrU3AnF_+l#5L7*!GeogS4aTM860Um)L-q%#4k;d!^tj&?LUYZVrE zFVM^rj3=<@q-32nhT@a(?UUHgHDOj~(JRBs763i1t_;ec9gQ>{TOJ21v&EuRAxaC)p_% z7=ug>^}D>W+Bv0W+Kx;^2(ym~-yyVmBqQtUX(mb+J9XRi8W@AA`zudNTSSN{FU?=o z=+~{9uOeDJl5ssF$4FKK4?KyU$r2&bVSbkwrFci~&^P z&OirYBC9_EB4i>{{%CpgCqvaDzl))ho!n*b;I#Bqm+(^);(zD4|FF&f#&-UZ9{yoF zQ!Mh)SY8v63s88s$I2)bAGh`@DrpN7SRk5g(;YCoP-a21!P=Kf%PJr}l8&aPq@CEk zUBY|K#;58Oig~^p>QOS!ZC5)4SNPq5s5cftl$&}xwqlSns_(Zsjci=(oW6ZKac0OLR|={$;)c3)B|$D$;V4Wvv$ zWF6~#nEJ1;zG`!bLE9+I&sjjI3Nn4)RM>Jr%rpS^zn9MPYR)rMUGc4v26 z>*4!$&$yAT`0bTln=Ijrvc^vS47X0p-zVrlY(*F_E+Yv${8ri~_GO`dD>zR8cM^FZ z3b;n1Tm7t$QxIWyM?6DCwiEG-I-!_2-YHIfzztFbFIMmj1fMEH%a7EELkCL0e_#pw zED^Bql8}`WF2nHh8QA-$3piZr3(=K2M1;T~Me3m&y#cieUvwgsU+CYw^FKr&?&hVQ zfYVWO3-S8gUc_1zf1&tz;X4;xDhdXerp1sAr2@`Dm-mXQ52k1~pE1$v;RvS0Zu3~d z@R3Xym8bb36fR@S_Jp94w$xqq zs?q9Xfhj(^{+zPhi_m+Lz}VqyZq}~!o%&tCv0FF-!!bEi%E&_ST_7QWVxg!xz#gLt zMMql-9codhEEum9;~S1<;>rFlK3Wa#K=N?e%8qsMHLoF^A{9_LVuq(jH?(t6(pf^jLU-=s(;Y_JZP$clR z?54c#xSJDv;-@oxB$iP3q@;8s=fp5jPo)#MK@x&xqmZT|z>in_awr~aP96~foC$lh ziNoZ86C;v}#1zGw^(n_r(~4mJ2;9=}H8euN)>^>5{3-Fy7LlKvf9e625Zl^-rW3G< zy?o4EavWjctk$c?N?7IAr|!n~|E8w@k@%|WpaBCHiveQPya|uOOYF8g@Xj4T;>Nmw zRL;x$|EbqV=js;r19nHTxs7JF)SnTv%7b!z=|>}WVK~EF$K{&&D@f|LU_Z{mL{1bw zXHP@Ls4d>hd_~$h$06iyX+9*z10kQGkw;qI5t-Xp%|y{y*e+kQmqWtO;yeZzq3gr|-?tS0+gi7| zP8%L!PK`V^k=R&LYsMx2r3OGCeMc6=wC1RaF;C)zShB;bxH~pBi@zN_XWmv7Y+;(! z>0aZErTQr7ivRqGXtQ5;&{?PYvTR?0aQr%C<9aHnY?PU+1kw2myDg{W=;w(?>MV#| z1NMMFw+3$|5`OxeSZ;e#5@Sxd2)?Za28c`?=uPoqy^Ji0{`!(zM}+c!%6_pac@4x* z(BAT7SVW1cuMmPhyF%G~fz7P_@ok>*3fIjhpFLJE-T87Z%7>80`8=Rb3eG-XOtHXB zF!^qWEq>x_$bO4m2VjkWpdrt9b2LRNK7>aBZ&nb*-S-qAD`kNIiu@1bA4P6uYRFKAzodc9tj)Q%raQ!~(u zr7JK|N7lF+gZnN$xiA;8`y~m>fG|AS!?3_%7w3Civx!^8fQdj+lIW>Z?10s8h1G?< z%^5G6r~l|4y%oNqE0si<+YZQ7w?BoRK7wDO+#*{H4d1VQA3Vlx!N0c}uXOK8qD_40 z2xRq6`L3(6LwAz>=@#EcSW~9cetS*+XPoea9fqyKIHVQWyQ&*b+peL60TCyb#~4QI zoIo2amKfEw&n!y+BO(K-|EU@epu;=AziP`SEbe%em4TXtzBK_8g%)x&S3moL$D$di z-NCSg-Hs6uxNYfkfH)RphnsA5wX zA`%9L$i1`vaO6S1LDd)S@<`i2YPxx(P7~jZK0q{DDW+25|Xef9f$VEy%_i1B^TKa=;`|118-9PJ*}94=lxF3dST0V9wj+ zK`;-6l4C_2-!2I=^ayLkviuv2|1Xk+_+QdP8f(8T@Eq>}1mn2vfK|7hiS`~W6LXB1-e|H~znYCc zfsYPgD_1I0gtVaXb&<&c#>zoLgxzNwpb8{&D18JhSuELzYK)5CxKx6ZAd~ZqO%F*% zo}l%d=2$gh$?9#p+1bHN6koUGBoB0Bb@!iQd=ht>__}@9lyuIyf1cF&#D4}E^m`=z)DBfbMfY~E*oa-$$V8^mf=K9Q9X-&&o!72 zgGRnc7k_xaEiqWcbjRdzXwSr!E*QS^5j7kZ(+VaQf|(j zZX%Jp{@d+sHtRbK#JRkk=3Xh4$r&v=5O#z{NR-hv&>5G`w=h(r2%vo7>m6Gizjvf^ zyfoiilZzQhcN}e@WpQ;;JL z+6#@vCMAjFZnTk)>_kTR;k8uudvJGVN!92g1fLO0c!D}T*V+NI?u{8Hf(4Ky&av(> zHUG6lS_sFR=|QTD(4&EkZgamY}&zkFy=0%f8Dsi8w8#4K$VR8LWPckMqYQ%}>3!DH2;J zEXWG*W5UuP#|0V{(_m0c~B8W93o)aGw|Z6uy?Xy8oYXV?$rZ+VXN8wpmc6=SO5 zoOSEjjK7vr<1HF(u?ctYjKd+z(YNb=L)STxlzCO#JG0x^|MP75!wKcIi#dS2k{y2DfAib4|SPCF}Qg=W{c?+;dS#)OGpVZA6F~i z(uicI=!ARov?psoG8TsW-_GKdKk252w{#cp+=dln`cN1}No65dfE z7&~H)7qlq34V<4w4^Y>FXxww9B9)x9h0U()CsPJmvBBzoyvC9Gq*kJ)x)$vby9qJ; z&R3N)g?)G=!kvL0i-47MD|9XIEBu*9Cv|(`97jLEESAeaYIbuffROa@3o4cc<=WKm z6opkeHrivEMI?jcMhK@OM*PvgU*CFIr4Y#wp1_)O7e7ZPQ_E_in^0_ovAi03P6PJ) zs-AkjV_c}~UBh~Yllv%*b9q6~dWddGFJr2&9 zS6(Kfq|s%f0-~{WghB&CyhqR?WrC31PgnI<)*$;~eJZ;Ar|$OA%g!>|B&4*)xS z^ziZjFtn$Tz@XMzIg-Ks|I_gC{~p?NTOaRk+yC-k!^gK}^@vU+w#kGf+|{&6y&|~? zmJ3<#@;hFic-QH*S`#@x1g>A{kuwnhJI`1rswcz^@%TQu15zlM)% zUO|2>KeCNu>Hj>-zf#{e32U44OM{^7*~j>IyOzk3|1cUPiRXT>EsBEU@a&|m1Uc`# zQssKHlm42gGat^b>|z0sIEpIw;J$Fy2{R!$AWQzlVlWQLBX-%#b4CvB@p{ci;PJ2p(q&JB9hJ_a{TrJ_~6k?1G&S zVJn$!S~9{yN6rvvt@EUVOI7Vr-ph`oqq=6~1J_tW5rIl+1S%?TioZQU;Q&5x20lrS z_sru{7&x%qhPSftlK4-+T;1ximvPEVml^{TqlK`+E9F~PG^ejSPh|iIBImA;e|!QEDsOvW-1l(i5D>4pw_p=XK@>!|;=BusWHrc= zHYdfVdSiT7j(nGG={0KI(-n%Z>vbeoix#g{Q7F~Wt4wg!m&8( zJKxpR8n*jC!^cHNVIqr(&hI9v23$l1uZMUNIsn7y7QO~eyN2wZvF#7sRe$-bi;y8a ziJ9dd#{-7n`bFiW6^@18>}qN>_%6mgK%9NX*8DkNV{&R-reqaMJ8kj=CbVUXbJ5d( zPTkhH+Ew6d_`O&7>>l*`SlqZ7?#pIz3*$19Au5i(?1ENK$V!bvT5zSlc~p;q18Hv})>kC_ z{(T$Lcz=7+`8X(HhBx8*M~4ydizQ#rdfRt3YnrvQ{pu^r;#tYvK3c(j`QXC~m{)1KCd*GxtF#uRo#peUiE`{h?j9sMR_jx$Taf<@e4k?0jo#4n+mS$`Z= z2k5>|vEPkQ=BVe0MJzp5xih6bd;l%HrMr6!u4^-uaPAxcLy(ilBC&ygdjgvP3kPwu9{m^n%h z4aDAd+{Gqfn`XAR8})jZapz@bI=)jiEV``ma2@Yi{G1S5?sZE7FF}%5M{Ns=dAKP) zggaz<>W)VPE{n-(oiw~FAD*tBykR(?RfURE>AW$(b-=TnhWyFPu}as@>5k%lLkF&= zkuWGXrx*?wFiT66Y%lVZoVYZiObQwmR-d$UmOGzhFVZicTyzz)>nXe87KG# zTasbMq?emZ+!SEmeabCF{{Him+Rb?D+l$ zqr|47Qm9SXs?=vVj8gLFJT}j$t+a^SCe;kPZZeH03#fpLHf*tH+PZ|%x$of#FRmb!Rb0Jyj9i#w#|5;r`EmJi z{I{}%QmtH-f`!Xa3>uR5;&1ivx@YY8k*$|R+wbG_ru80oknD+`=yyaiwx!>!jugRk^N1l#LWQK@6 zZVtq4+a-aR0cM4>e%2pUP|A6c*AxE1R^CmcFWoi9D zbPk#?bn*{0I3#Qha`Op{erV+udgCa{f5xQvo}I_5xr69Q>=xxRmiP+pFQU zS9}@#Mzxw4$q&8?z=j1^(RI$lXGyeu99*oQp*_pF8rZ*Fy zB}BQT2u=&Xqo0x>!a{&>@jL%sL zuc}LN(l2;bBp^mjEq@nF=#oiwJ!aP=P%eHGz^^;MLQEDj<2&)Z#$oB9^VmQBc19b7 zthv!Uu)n=dd>cXPA*#N|DdC`~S&ZYUIli)1?oJ=rqNAaGb<@n1>yvmXwatLhPB(3u zWNtrT^|9@5Rj5T$V6)D~{`<_;lpbcQ*N#?wro-MD3A2f+HT97liW3#Ho!$O< zPmT4^B`Cy~@hRuBSmhPtK=E%|@9nSwyw8P-pSbD1#G#g@NhwzTd;)*ldXl3DKOG{g z%h74bp@1rju}2BttN2aG%RF34>CO8Q`oXry0*YhUo)|G=1scEK61WvJ#MTYR3kYXB zCy2??Iq!VJi|MFeEw9sW|MYd!E1f1E2*9g?l2Shy?&g+Ttw;v z1-oI!6OREQQcB3vcF{K?WE^oqTsP!#TS{?Gc1kL${HfLOxQWxl*K z@u2e%{we$u=!=$X(hG!9n-adz8F7C&CTBLO&4$@bFJ|LsQuSV(QB~j~>x-vnq#wnN z9(2s})66y_h{OP)TslCCbSL(9-F-4CH*_ z9L}%neP9637?L^?WB;kA-ANv(yrYk0lV%d=d1NeJX&}!s&wS%dVKT_w!fn`BiX268 z=i)?T9SR(m@_AE7oPeXv7gIiMBOSn0ppj=XDBVNfSoYrz8Tv5%*HX;OuMQqE+2gis zjfRoU+yOWd#^S<+XE(uTzr0xU(!-`a>Zst%4KiBL;L3ZcDMh0wYitikw$$Pk0g;G1#vh zxz$`!D+j`?{DswX6EZTij&)>SZ}Bw^$1`A)w>#snZ#?$`*fs;OxV$loY0aArZ6C<+ zg~*6sc>@&?xbK|tYvyg*_su?nZ4cm>kD9Tz%aXtCCx440e}7>)+~%8{CF2+`Nfxhj zAg);Yh`k$IG%ENpj)XD(PiMAAc?v%ZqfO!XoGh7<;-Gk6_2GaVq`Jfd()dx^lW3zX zBHcnU(LCi{5{v+EK3}U-Zzf>*EAb2+$K~8aH7`@+2y#3p@g<((s?4W0`VB+H4+E+| z3E61cus3}&Jir7AiNKjRSoT*M=eyCBtfr zCgI)4Uy}1!Doe71+cs0-Oy`oQZzQ2;fYZ)3^Gld@WVr9SnTAVo<>f<nGO{NC|-BS28a*P5(yv!D!K%p z)7Ja1P%Ls9f_N-FtKUrXRz9aq&noX|zAU_$7bSFVQuQH}=!iQsWrW+;X40qVXNMi* zBE5vo7^jg3(N;-YIkB|yerm=gcsxqanhP~(_%fP2_X^#H!@cmu(t#%JtbrsP>Sp>* zZmog%Zp?@A&yE9Q1^~RUT&1*D*m9J2>*|Ee0uDNH_FNG6Of zZbMVX;9qhiwS~tAuYneI8*Ift%>rsL9^Y06!^>LTgg@lbg#G;Nh^CPP2$r1mi0c)< z1OJE|y-JPuk!%N);zr1KJBl-dzJf7|IBdNaxOUis0@Yo)2j>wtxR1+SZQi+onh>8P z9qE8d^8I=cy|&K}H>!7!#KyY^HTTJbK$(&cRTg6*z6F0`Z~1z%^oO2d*aUBXWNMf8 zk1Wpam-P57gcI}rc2sJuU3*T?^otvFu9D!1t9|emLr;_xd4L91dRE(uF-B1#U#YQH zccH@f2|TtW`tcSr{+3ay8967fM}vie-mpdGQq(E8_wAWo!QGyWy&SG`4-e^cZ?5uU z!D7uO%_Z}AQd%{GHyP_{-U&`tY$F#*vhsjcxVdvH|-sR5#i*NK->Jd2dJxavL*6#8LCG)vau z0Z9ieb7v~PGFKZ>U=@YU8z1JGC+p3;evntgUn?ow8c*$asrXF3=IEO2E#1T;vXObl%ZwFWZnW>*(P{WQ&lMq?bj!Hw z!{{W#nj5J=`*Xo(OSk*I#<=`~lwh7VzitE3g0gb1{xn2?^r?TcuC3)_K+&SjhZOaf zAly!waj8X`0S&Lc^sc`-AF>Q;>~zx@mlOHH!W06Qi(*D=bb;ShAOT-DG5A9HsoEoY z`D}tKa^j0ThRpl;U(2j?P0-kie5C`L*4(WqUGmnw1Y1)+>3+F7T(nAWL;qfj;aJJA zmvL>mY*oQ$W$xwL5p^5Z%-1EkZdDgi0=(oeX*FetZVm0YXGJV%wnodKV|D!sZ0uMsV+!1jj$6iSButRftYd# zgEma)>oH?+Tdk>VfU~r#G}eM>(vJ7}cY)Zz7!H@mXMe!={A*bETTrHLo^w%D5>Iiq zGBxG1E(JLxO^_ihmVKa9iNn;-Vag}t8SI+!sSs^VVRPLJim^AEw^H_HQSXfu#n=VJ zO3fvsa9LUrVK2F-GN!kt`L`WI_nbeHx7eu~_ltTOsE!FiuoQAS{Ie`;c*ycx*FKE! zb9kkx&4U}e=FN$)LKI^>caI)c62A9uDDsz4Ts_H0a`R`Z*oS^~2|dg$K78jJV|N)h z59IX|AUS<2B`xn_AtP1o+6}wp>ywXH?sCvj>$_LqZnKNA8fsmpJ!x=hkzNAf*e`#a{7Gk}#O zPtP-C9A6NdiMUJJ$DmVfP0m_eY=ZPrlu^ss1@*ejsZl zc7FVD9rkcj@^Jg<;qSKx!1>b+I1-zfNikPvQ$B{6Q8o&!yQP>w#cws3tGlh7!YC68 zq}AI|P3P1qR?O4eRZqdN_!xDVzN?up>fveeZ0ws>vCPYRAf3U1PPtMtlTyCHp)tx5!1#yN z$Cr2a*bG5c#(mMW6tF^*^Ff;x>L)P_rWf|3S^UT2 z3vgYiv+RtyGYfS8+V)3k3&i(n3 z$seShc21Dfu9}hwmh{dEmK_qW3$jV$21kbXJZR2+bDRqge zasjgAQy&nB$`)mMuR+0~Ciyu*9wtKG{cI{-j#gDRud?Q$KU`=>uSX?OLh3xMw?676 zwV)cXX^cp#2FSr@di2NYJ0(rwFN(qw+g1h{tdJE0{4pENt5$8$QwALE;}}zwr%xV$ zB06&WA%0V7wUC$xZ7G_UuX72Ai;{{lA|u=NG#;ZwW(x@BK^4qn%f7n0{643jO@v`1gFiFWTvMb zl+vrB%?xB*!RjwvaZ}7Lj&M`7SzL#b-DpOKUik>R;wD*NUJg;h>eZjEx%@yqlXAc< zAIfpu(x|3%$Iq_@A(;HDv%F?59jHoIHx;2w#8*OK%x+M?=NH7I)g+X%Fga`~mfD5% z8jcA{ZhtdqFl!)EHImiFM7yR$+!hfLSAL>qyA#{>yUZc_jdC z{1U=v*&jVJ1=Cn|nvM8E$agVpeUyWJ9ILNzLjIO~!r7yq| zH>LgpzhUK_-@nIG_wf5xpR~wBhO~VaOmXL&?0Bg3l@q|xxoipWJ!ZiK7RO9Bu#I!E zqy^X^Ls!}7tjx6{)Roc0%@sG9PSXS<-plBet}HNrijK9^^if=~s@d?sMvvMNZZc~O z+R9U!+Oygzt{CT@rFa`wQzE<+B*LuQwSUVM=-aGl_`SQtwOBEv%O;L~n0e{wQRvow zlF-yWE;aqpM=QgS5K#*`=y{~f30pW!O^Ngb4i2j)b(iIiH2!S-F=`1b6kiQ{ZlR;^ z?c47U%wfWD5*Oa@!&ZsCfodvDB*SE6_li**BE!-fj@e`rZ?snstCAgpNlvd%^u(Zj z`?4?s`xoxMVqF*tlCVf*X$6pdJC>pto@xLr!{Hc?_+qr5WkAG=V|tY!69biEm_nPo zqm>@yb?VMM3ywfdv08^7QT}iRjzro3=LX_kx=$rK>q8nNxYKX5%0LA-U*DhfNyT{& z;QByRuR_Gqc9b@C&wk^|Okup03$;h?DMWHs&%hZo%c-gdOXM%eV}Jjy43)^ufkSQH z#i(9DbESuQ&3|VYUWem4vpevqm&LP+Z-&t*;!QU{BePiw7h{Ox`Y635A3Bj7_4}b~ z5Fd9YMWf!Eb#4b=rrwm?JAqTNbBhQJdl9i?y4k3lzkQDth7Ye-kKJYoa(N&j!O(v9K5AaS79O;>DruX1+_nM^P%$M%)qc*Lau|>N%sC zU7qILBSZ8X+^S8AnK*(baeLtZnp%Z)Gj z;Cxl`=KNWZLp!|@6{h&uv*j2A$PfkAf6955*N|UXY31@z*n3OkjZZ#1>kMMH}2Kt7c1K zq)$O*btK2Tf2d7N@>+q;_1yU@6uQe{cR8DikpQxplnAmkJwl zhP$in69Uc^;Ovh$nIl_7E#|K(dkvuD`4~=M-rJ; z<6f-5^SB>%xX(wR14&&Al6n@KNRFG&{%)-CPnI@+-xIhnIRYPBJ*<+x|KMlz;Ji<~ zn5V#Rlcd@-1+I#_N#%dQqGheMI5_ggOcOk3X7T7JexZy{8}yplrdLjZnO9P{T4eHN zp$nN}{JX-hQA{W@T#X$$tP6s{c(B$W<>6+kMpAi&pcy7@C&79_~2@rD?-iN|$w zIU#+9{rKZF3FlHNMJ}Qb^z_{arM7as%T`WTrP_EVZ!abv?{%(&>vsnA)PkWCa!lHH z`oSA5AZKxT{&r&=tGFIS0|G@GuCvJFskiKemO|9aCeppJ^72Gb+?o+Fcl_^`zrhjJsHI4&dIz66e2CZbFK1LTL0}b;ZN3tLzULmi=60Ttc_^k$r@YWu|9BV?&+fbu z$+)RPu@sI^|JOD9Ti4r53rjNuq0~A%Uk5`a^_Ib;*Feb>ODO`pu!mh~xEi7!r~iO3g8(>rC=(dl7T+eGjoX9Fn2 z(I@8PnalJY3Ki_Q=f6M2pnLjkh|{R-8|6hO*Flt}*cVy`gPnLlL@`3Tn7WZN`ZJNlvcY^Qx!JVq~ zT(XjfLXrNua^?qn8cmF8;&C{eU_uaMGrKV(8X2z0OR@MZpjPEj%f=r-WQMH z8VSca3b7mA;x=yLifl0Dzl$s3!wWpaARO%5YsK*9W@V}@_EIgb;^}K0iQP@FmFL6% z`MZ(6w3cVQ5~CJd?IOXDH&U@VB3ifPssO)+fd8S^wc-3c&$bOXN6=4RDTkj?OhrUz zDby~rBNq_|xd9ARt7As|W(+BXB-vs6N^ud~mMZEfL^PMKh79r$4CZ0VTh#Vw%?ZUi zmkmE>C8t-wXyfYdqPGgCPybrrMUMJ7TGezc({w|B19;bJ)7}?cK)g}_$XEI{&_!EY z>v@Fo*{bpiE+Zv)UmLFxSqcj3Kwga+U2(j1#1>xmq!oo-ZH#JnQ$>d2yT-3E6w?0)jsC6Db z>hEQY%Z!uX!tRxdylUh1iUSs9#IpN0lAQGImesPoXHoO5yY^v*b;*4wZ(tHvsB@un zJH|1{Q}m;t)oRUH`91>BiX@aS$O=XBLY%gT|`Ipl7)lY$9bjg0%<35HM*mkA@#vs`6}%r z>=yBkX1Q?%G-X@r*5{*MZ5r=0S*)>CM8U{I*GA@`oapE3**k;t{uEbcIi1toZft_4 zQ3XbzxOYBL4iG%n0urHe#!FiHz9-v9 z@uTMfw#?KdX+^9Z2N>~J*?qNYXS?d)@9hs&%8$T%Q|VEZH0#*+3aV&hYOX{`CumpHJ< zAdy=BVXlxFaO*r;Q57DusuDUSjEsZBd!A$;;!@w0h9Y~Zb2-vW2Q+!rF31P+E#xjR zGzYn1BB}yNsC-F;hTk@}y^c&hP;&nqI|d8q_xY&kt>pB{$cJ0BRScHx0rmyYX#Ea` z?UZcAY=hf$XIS_ka+vPHQyg<7zg`iKL-V4Osc4{>c0e3$HOP$)p*%h6_%+DX)=KeT zb>br{Odlon{s@$VnJXFkwsuTCA5z4te~nOL?h%g5qn>J!bCufZ?<`wzIR0>1y;Jp)cg- z+7U}1PRFslOqBK%jrM`ceu!pTq!OX|q@DeOXKn%D0LP0;rB6y5;jZL}DhW)(YF8_B z2gLRoczKigv+fhgLW7Kr^=*bs?m|yOE*)VGn7NGkVDj}+CseA3i%yA zOj|Fl+kqfkihCxM7@wl!g;wA>OTb%P`c(LU!x02WJ8ht83m<+NTTX)2*o6~W)m4G9 zCT@%y+|5PAFd=hzvHxxArVnC;PO#N+>ALUigcicrzM{s!b5EUQ#sPazrx-o}zEeP*qytvt2WE5BBJeOMhMiLXnUjs&P%meThiF2|t6mdRuJP=XA(CWLK z$uR*U3Zf&r0c;VS6CmJ^*#TIvmY6-mlJ%k%K%Z5z7AgvYFN!4yEg?*x0T?KOrb*eE z(21EqhcCh?M=+N@`cQx(o>m~yn>Eok%QLZ=Q456874=z=avn6AP?V@2C)kBe@C09y zurt`VbSQ#Z8v^cGCFkLcZ; z$BZCv%Yy)Dz5iXn1QXa)nB1I-f8wE*Tts4}nOP~g9Pq1-sEB24F}#YEIYaQlU(`ggxlHRX}5baR;NjyyR_8 zf>;Mf$hx>Ym}j-ecCKblc!ttrPisX!uUN(IL_K6+g(lOlhh+v)<2-S78&=3XbPGFq zW?!}Qh5t+Fy=v|RW+=U1x#wrt=0$7Ye;_bkEr}YM-&lA)VdI7J-7i! z;D~+f55&M0Smub8tqEExj*uxTGBmF4 zZR92ehQ9)9lsH_sp*SX#%Ocn}xA}x502`9?2ex)=^=RT&ic~b%CGpCt;_^WIv1v23 z?Eir<+YrNq9@s9K>ILmO0!x7GB50a;=_o$ZB>_$o5OP)-0N z-Wl57%MRXYL}w54ZrEF|;S4go+07jrG2^UfWIY&vD1{&6JAPh9>I_BzQ#)EQZ~FR& z7mAOxlP`xc2ltL43}b}*auE8q@9e&@aRDcBx<-;f96Kon#sSz{d44C_Jl3w&%aEDBX3UTluRQIzW zXZ2Tybr{+7`}p#q_>@ptbR&@k50CYvxP?CH^;i4c!;O? zinsWa8HI@-lvr7`0V$9#Q4l7%c$AnBjUV}wNBNZJiSeMigFbNsH2IOg(3n^Gnz#9z z4~cGQj%cC>W|$6~2MUn~`k^QKq7RB@_*~q$jip!mrf2%6clxM@`l*-tsy8KGU=yRa zP=VF@uJ`({2m7%9hsF^5vN!v)M|*HD%`@QFT zx@U&J2mHVn{J|&u!Z-ZGNBqQB{KaSd#&`V3hy2J-d~?_ZWPk?A$9%}w_RTjEV#xf@ z2mR0&{n01=(l`CnNBz`S{ncmv)_48ahyB=>{n@Ae+PD4N$Nk*b{r}zP{oeQe-v|ES z7yjWd{FpHQ<46AFSN`Q^{^oc7=ZF62m;ULe{^}2aX0ZP3r+&`o{#bwp?Fawx7yt1m z|MEBg^GE;mSO4{A|MqwP_lN)Zm;d>v|N6K8`^W$M*Z=+J|NaMv00IXRENJi`!h{MJ zGHmGZA;gFhCsM3v@gl~I8aHz6=GLPhphAZdEo$^A(xgh4GHvSgDb%P^1x~AK_3G5DTDNlT>h&wwuwuuOEo=5H z+O%rdn!W1wZB@2%=hCff_b%SNdiV0}>-R6%9=NG?(F$9=+L4^lP>)gGiuYSSF>*I`Zes>vS-tFiu!i#+`4!3?(O?G@ZiGL z-Y!1;IP&Dmmosnf{JHGQ0g4-jZv8s;?Ao_;@9y0uT&mNiPdEPkJo@zN*RyZ$zV+Mk zl?C1kP6;h4HO8BaJoMcq5KEE`(KxFp5~AjzJ1JB#}iL zsUD9#P8ef#M>_c=lu=4KrDz}e_@jYTYPltsU3&Q?S^rjo=wg;&nt3LgX{xy8kr! zX7V|ej-e(HF6*p=3^FLKJ21fo8ys6IVVKZD9?Z0JMhqsDfJ8F^ zxPwUzG31a(#ndEH0|_p;^2G=-u<%1C88K5YzW*)n(l5;X`nwJ>$Q*n#&N=J+7&|qn zP(%$S?9ubj8S@Z;Fff!b1Pmzj5x^o67jZuj^j z*=6@@^UiI%{WjccMPh;zzQAGv5Iwu_j65}<;7S-IumMffPS{gJ3r{x#@3eip(9G5l zL4%CTUx$6U%xIgLSx+<2po9r@APGrS zf)je`6yf0;mnIHTVwFnZFAsWkz#0`WfkZ0&^6$Dvg z6B|-Io8&Kw9O!)WgJedK1TpAO+GUkgFpUH#>lq7baI#KB zfYX6L50eF~1bpWr#CCvqUl*w3mN;6; z1}s>C9iZ{B7^uJr^2}>bfl|)d368BBd&Xo(AULcVmaE-L+(4w7jyMR`1*;fsX;<-6 zoCY8gIBcw9&+uA`Sq!izNL)J5AkiZjH73%eMjU7W1Ba#zt6Jr10HEOsCET_Tkkwl% zYOuF%slX1o-ED8B>5h-xb`Q*O?ZWopxMr}{xLf4~aD_Y3TCTpMs zt5 zS_{Mr0~qwUw_9SeJbI7>CX4_G?s$U*kg|L*4Gq?=o6bNe_tmoI?w|R96Np~H5Mp53`XbaI`c+8BmlAS< zV3VX1u_g1IC&~zqi^48<`8t}RLKVvNZ2$7`3}Fckcf=tHXJ}d{lx=Ke18)Ug;Ilvo zr!jxq;L=ebVhQ-b?{HhN-&0nt(zq z`7%fgb&flH#lz+^P!jpcgI-{DV1W=Hmj*$f0gy+IP#y~T6Y($sSw&d|70Uz$$dv@7cqB;%x<$CGltL~fLpG#CJ|sj&q(n|6MK+`?k&p_i zRLhZ|xlkCu%#4O{m<=Wbh^52NkrfHpfi9W=FE+#7MAks;7?kxGBDBNq%#9P|5t}_D zBA`z<^+8n$BDyseO&D4M^1%5iK?q#qAO73pJ)Ro6(JMLQl;zRm#G=XlMe)Ru;B;Ln z!jd|gz&jFPG@Tj~lz<#yLL+^SJA4A-c-uCSoPp(03&h?N44(x#!KlfAJP{8_p#kn# zkxZ;!6ZH`iXo3>x8y^PuxMWq}t!X?yG`IP|&*}?J9f)b#m zTFwA&CY@0t;uo-h#!*pm%E2Wtf^W{A?7dYS7+^bOr|tOPYx00+rln|xlv4Uaa|)6B zkN_A=<|VLUuMniW1XOZ`(SWJY zAX+3X+GMRj5hNfr{(&4F%}nf62_yj+RFe;Mf!|mHlgZD=kU+#dK|4SJ-xWa*@DHH2 zDNs<-3M2s=&?(b64W=f696$jH96_1R&(%pHNClmriRz~IKog89Bc|rL1&`8d(xg?JfD=JD%4-Bg| zMFBb(8*}kMw>H7IR_Uzb0QoSh@=;@pB92R%z_V^DB>ieI0so{05~O|cmqEgae9()% z_(A~qYbXV*!2YYip3uJ%Y$zdY!7gmV0xZKmY{Bxv%Bj>FVdUjF=te?lxwr#?sZcYl z120TZO=0NqLDJccKn{q3sM-N0xGJpn-_PhkUtVhoWGfK%-v|H@G02gy607f3sY;G% z5BR_#^xU*1)2pcjI;KyNA*-^E=?|3YGt5nzNt3NLK^GtaEuE zm>YAMR^Rkh403bzB+=HxxDm!& zR7ODqJDjARhLq1FmQ0vr43L2BFwF_5fDNciT_mvRT-Okk>8pC;QjET{h0C*MLan|M5QZJOxDxgI1V9W@p01_}mBGAC)(iS%`S0R28@Z4&IlmPM5SBgz&F@PP18QNGE`bxH62ENyg|49cXh3Zzx= zYSj&ZVk$6K?9LS;utN}l5HlPu_p%iW$gG#DN`1abfH8@_@)y6h%)vrzz$$FPF0jEm zY{C)*14A&rw1O{SFb1EH24}DbZ?M2(aD>GWgQ5_{YOF_gmjdJGFhnrvxS z8G)DX-Y9r&92-S1Q9>ammM>9aQ9gXAJ4M&R@c-%F8tHEe1Wrj6{-OXd%nAWvApOfn!RmnT!0MhWsGqcR~saw@0tCzneoQ&=E-GRF-_eB#$Z&gTK+*UH>W zEBrDr2eU8_GcgylF&{HBC$lmy^DnoIN+pEFg3Sn{@GD0$$j+3_IT6X)#M}l&M?xOg zyiqqxMzL;lL~w)UvhGhLWJR{KJHInLOaGWcUh+q<^U6phxjd*m$1^`Oq&-t4J)g<| zhlssC3IX4zCtWZzFEm3pv@(~%FGI92Lo`K4v@x3yF(~Lkq#z0*@*z)H3Bq!P{R<28 zi#eY(f>5(cL+DYM%Q)8rex?+G&NNNev`r6af(GSPXwN*n;JwqZl8#FElkyF_3g zwqrjwWIt?Tk1)>Bi$YKJSZ6k7Z~wMtcQ$8#wr7X7Q-?KYi}hy*GlVU6(w)>ayPP5C z^W=<+OIesmgJkDub6(@cUiWo8|8;IF1W((vf#UQi1vSM|Mgm8+axb@Xi|}#3L~=8? zbWb+}JGW|Ek7Z}DMN9N%Z#OVkw0DPhcaJxCleblyH${_ndXM&Kk9B*O^;#c<#iq7v zQ>1=Qjyo_zNW-!Uy^st4wp;}FOdIHd>L-B*=z(50fMY~sQ}<&}_H#={FKf3$SGa{= zcriP3U&6$MW4MQZc!g_tWv|R-i?)e_Hj1CPii5USW42hgxMnwVd>e#=sddJ#lu+aN zfLho~J;OE6)H>kyg0n?~JO4NXLwLXp`A$#xVRvu`7p#+eFvD86Z_|ZRmpF)Lxt0?% zMk}~pCp3j~_m+=2hyx)6f&wIS*yH%e%V<+@?(|KpxJH6vO!5{p- zhxWktyIH&Xx%j%UM|`kP{Cgk!i^sda6GRC!`-Js!w10eY1I*6BGR;g6F}(D*dqgjs zfmZzhX~G1oS9LL`dN9ZMFjsWO!vt`pWaL~|qDdh_^bC&;{k+19lu4NhaAM@_Jmew- zE67$~5wAgPXbcEJL9}HMm`tdBNzAKxt{1zpm-fZ0I%+rkP^7WBrPWTjdwawCuS-1K z+q`HeyqqVy!%GgB^E=H`{EELl*&lnXBfAKDIp{z<-^V@T54**`xZd0SL41-2x3Ra%MwQAm-4_yN zpt*}=LjT6IsYjuaoLKN-!d7G-LNu6`VOxl2DO&t`G##26CSs7Va}i^t6E-kXt7F26 zPkAtSXyhBfo)Q<$yzZ=fH}Bq_)ZUPAg3X;hCM4Kkbcczu2_hQV)o9V_8MaiaS4EyX zn|5v6w{aT{i)`%KgTBqSZ5WZQLWHe8%FXSxY6&*(mNyN{(f3;6#<61$zFm8FvKwXdwcKi-4iGKJ^VoAA*tpm%d@4E5cipxCxQp+Ka%q$C_IXf~s?6IC;1i~kKYLLOFB9GK-NQ_cLVHIqi zX$BS(5(|KuFG?t5tJ5-q(FPOyL6b=r>8#UErg9wcKp=}Fu21`pyHB}0g@M6_EnQ6S zEI!3c(9u2X`;O81dMpn|W~iI=$45EkaY45lwGL1r?=02SQ&HtF&qX_J71CG3b8pk} z9voC7-aMM9Ly^KF3B!~~BI!bqI($i_b%XMbhuie#p?z$0=d?cdx#FdnZW_h(^n_LFp&<>IXPvPt zcPC~h%C*9ggw;@N4PA0+><-=!-Q- zrTcRSNxsemZJq$0X}1K{cp5W@F2LYqr^8C*>B(d_u-G9eL&SWZvEc`MHHF|KiZYo zvQ+}WV7UZDWgDPHcqJ2M;ba?dGv5Ot7{L_%iw%oWnCq@5pz1VFK|(`Xq;Pklf7wAc zdUL@jn&FF2)Bpv2=m{onP=gbUfCM^VMw5nc7ZaGk1(w^%2$Po)!>O(n0Wbw7U=RWa zj_Gt5X~!2%paL24Aa@e9;+HpCAx^|zhg{4;r zG$17r2)0bZBt$l88!{YNNKuZGltjVSRWx!zzICgQvNBrE7z9W#7VdBvNk<(fFVN_P5>6l^jd(6eJYfeib(zZ%SZODIVOT~|p#&75VF)8AK^{sO z&ZlV*ksZvLd}JZ=15 zBA*8yfHJRR16`vyAsW%y40*W z7Q{v|*k}h7NRWmfs*8j_afc?%pawm}VFVaVm}yW08_s+J1}$)*P2ozLEdC8#-t(hE z^H-{>=*5;y@Bq9r@(gyQE=~Ty9zzkjNZMg7RO$1DAb&Qq4Yu-I{`6GfI>y-WK{l?P z^{goafT>Jbl(ZE!ZD~<^TGghOwX1b)Yhn9Z=ygmW>S@Wf_|>(KOmd~Y^)0VlIui!s zBqyIW?s26B4ZRUFr$1e&g3MXh!G%;m%*-7#?&edWh>>$U!4?a2D#Tj)Br?s|YiO!@ zf@_|YOWv6R ztn`RVp2t!*H_PLhWfL0V2d6WT6qfIYBYF|~a+Jg+HZey_F`$9swkO0O=`NDxzu(Sy zr7v~cO=b#W9q)KQFd`j|N-(F$ato-Ljfj!s$RMNs2dO=Ijg5xlna2e&0ER=bd72Cq z<;DPJkwa@nn!8gINQuW~Mh|^k^-3iNb;))rR1d41h!No6C#f+j8zp}XZ5nkrWDuU5FiXt|QpI?%Om(Kc3F{r~RAx;z=pUDmtIR+z{ga+Z-hS`is& z6bc3KT;_Oxl8!jUAjn&?W-7THvnPUv)jCEWt2{;MT(kL$grk#Syx9a~_7ZerfWsZ) z02*1(#h@6*@Ep0g9f#g=(D;Ufd5KL<_vh+YTObkrU-LCb&U2gKXHDu@b+2=4YY9XKr3|RW|M5CJaWPf--32 zFkT9PFk+yB9^}xQmhlV;hAu?VLsMV|TpM%06kyPab~1pKEV19$BQkz z{Tv3plSAo!a_z07AqX%y!3~@s@zlrFSFtDY< zU*W<-T>g`$U;S_IeR6O72=G)QkKqt-YIecz77%a-a3|X4<7h%9GVb$0B=m$!S=^=> zPOktxP`(K4TBvM4%I~874{!o-qyJV6T402lS|cb1FC#GHb3`yH7Uu&yYd0k1V|Z{Z zd{8}r5IK5K2!pT)i4X{ja3Uxo(0Hr_`|k-&P=0ddk^I96q3{U-5IEjvAZ&{YaqtV- z3C#>I0UxEveuUui&*@0SCWtFpK!lSvE-NTd^kT}Bc%cKqFc0+*2Fni#iO_w1@DF`Z z5PR?k`-ko5MC)p!D%fog4^9UqL{` z7X~sP*D)f&LJWzqBE#(rs|UV7B&K4DCPYsqoRQ;5&Biit8# z=}{ixQ4AXh>vu`(;QQY)!4DSh%Q=ZnY`t{5${veK#C2vQmAkQo(-B&jh1t&#L}Vg$jm zF2@cik1{Xy(kQ(WK>v8fE+x_e1#>WaE9CHUFA*~_6>~B9GDQxPF(ETDC37hI5-`8= z0Ld~lp~v4+EDd)8raVakiA(fIvu$q7lTgw!RrB!(vo$qtHDNP0E5w>w^rmf zb#pgeQ#O5*U-s_`g;Ub(2O*G=UqG`CN95x?t!>zFIiC~c80R;svpTKwIfGx<#Rsivp(%}B4LphZ&5$_(-!-4 zKW*^{1+)ko&nW(D8I?0Sqf-J)&qV0*J|Q$hC3He5v_dU()%KAgHFQHc^c%-Y1Ab#WF=^_FTJ7}W`Y;Cffv@$)KCqS z;O0SJ^hl93NtJX-nY2lPb26bcN~QEDD>EZFkOOA|GBo5$Pt-}h^h?1sOvQ9eVUst_ z^h|eiHgv*7{)9kJm^iJ_KPxW+9`4m7R<4^lEPz7~R3AIoSwM-8+ zQ5AJj8MRRz^-&=;QYCd#DYa59^-?i4Q#Ex{Iki(g^;1DLR7G`ENwri>^;A(cRaJFW zS+!MN^;KatR%LZoX|+~u^;U5;S9Nt)d9_!4^;dy4ScP?1iM3db^;nTLS(SBJnYCG+ z^;w}cTK}bWTB)^Kt@T>5HCwfHTe-Dcz4cqcHC)AYT*r=-Su7J zHD2X)Ug@=7?e$*qHDC30U-`9P{qwn@yWt-|He^M1WJ$JUP4;9_Hf2?IWm&dmUG`;RHfCjZW@)x& zZT4nyHfMEqXL+_~efDR8HfV)*XopL-xQE}@hW|DIZq^-ufgXJLh>_T7 zcQ}cexQVS7h~E|#ZXgXzwjaWv1uUUvrNIPtz=^&1i_@1Wz<7+w_<5liY~7&~P<9bi zfDNkn3s4}7X?BZ&z=+HEj`6r;ml%!J_-fNQj{&)i&vuxBkRv&hnRt-3_Aqe3j!Tvjvg(cF*pcg4lTmq-#n^2<8I-5Cl~XyE zZpD#mvMNKJGqjt)|P?!m{FLPL)IU9A%{-J1^;RRWDTqa zSRe=7SY&NZhHd}}fB|GDBL-HW489>`QvnT32JYne9}Xi1c8E+!R+NuG34-ni>>y<0 zq?AK;^_s7qMRp!M0eDm(2kM!QjW{1NBMSb39-8?sz;B$9`JpFRm_?SEo%v;=`5&ga zny)!zv-$hD*_-VGoW+@(%{d0pIb_$loq3>S^8tonV4I8h9~1@%L>5MH0AySG240$* z?YNxJSp|+Dol|;;R$8HR$mE>Bp&@#y4_KK&Hcn#T29lr%eqfx1ftr0Fhe!aSL3W*2 zpa_CM2FAHDVju^a;4W?&WEbK4dY}mI0tITph!0~0#GtO(z?J`Dt^ZHptBcP$Kz5hq z*dJiw1dvXrS%9nmAs+@q=!Sp>sCbLJcper(GsYQ4Q08}RfT=ZmfhF3WzaXlox~l)- zs!>L(5!(<8;9f!;pRKr*Up&TV{0UGTr{jCRCpy9>yuyjRex3Rs;>5dUItEr6WFcGy z!n;sr0IyR+tnHYI)H;_z79C>1l403oae2ZGdXxeC!Eu?#L6#o$rjFwz$VhgJc>oq- zzy=%|!V#><@jQPo9Lc{x$>qGsA3VaNyppNBO1|OBO(V-EoXfo&20mGrnE;FJ7>%af zA10fttNa`Kp+pGB!W|8`tg>Sut>v7@Q=`1K?X;sm&cPOhC#1;g~j_o)tdl7k-svw%eV2WHx?> zB%b0eo@6s#;@jEdwH)LDzP2B|WCfh#B|f=1ycQHYj_DyaV1VOyD7n|2=|!H@N7m+F zou767?4g(8y}ROXePr3b+(CAW(>j#jd$kSS-T#Zd?qzxeh=9*aImyjlont@-vOWl) z9oNYu5x5NDrPX+LF^N;-mo<3w#pX8UmF`8b$3ICRte$1)3%q@N5 zkA2KS+|6e{`FH*ejsHU~~bsL?grOJkpV920Z@NmC3Dty*rDAY3M z!3GzxaCz|Y!N>(SKFu6;>fG71Yumn!JGbuLynFlp4IHiB;lzs@KaM=P^5x8%JAV#6 zdH}vRR|<<^Vnvc3HGmkro;}GBzwDHZC{d(HkdXyVqb}(pqrW;Q&<}BO_}?NEHo z5d#W#6y(o-C9v^QOeFL$L<}rE;s4NPCaBOu5<%?nk5L%@!w(ZGq%q$LO>{8?3?~JL zVu~uR$YP5wz6fKCGD=6CUlW0+9e3Upgrj)mnTMWw?CEzJNI}H+5`FgJw^Bc0z(Apo zC=~=t3@2<@kdshWI37wZ8Hwb4^%Zenh$EJGqKs;;$!42wz6ocXa%Lr6op#=dXP$cQ z30;j-8pH}3HJHFc4iXjA&KWeEpu!KI{gVzFL7c$C3QjomWkKwyR#O}Ru7E^F6ZLY& z4KT#8!V$m-mX4wl(bLNiHL&0Vf)mYy$q%WSjG zJ_~KM&(bJXUw;-9=%9rj`u|U&i#Gabq?1;9X+fF(v+0oic#3MMRON$-sVAgb%S0WP zFyKV+%1dv1MQY0Fx}PF}O|i!&o1e4_FU)Ym4nGVrReVlNam5y2Jh5#Fi?#7pL3srl zRv&AX@W>ILjB?5!Klbmq zACG*+Hxti%^UfblkMYng|9thdRPu_wK(B zfBf>#Pk;UP-;aO(`tQ$w|Nj3EzyJzxfCMa{0S}141S)WW3~ZnS9|*w+N^pV{te^!i zh`|hMaDyD|pa(w)!VrpZgd{AX2~W7cZcuN1ENr0*-=aM`sqlp~tf39r5T!>Ani` zM>-yIk&KiYAsY$FNTw{229%^FFX^;4Vsew5bQmQI*vU|ea$1@kr72I@rcV+Om8@*# ziyrC9SjuuPsvO`fZ;8v>z%rM->?K%gc}CsPqY}}eiX?d96=K|>Hl^6aRi04^s&qpS zMD(RKF}F*fHS?L#T*)-6dChPp4wwPlO#saB463MM6kaKX8szc}H!$NLdT_*5+)0&Z zpi`Xu#A7L!^$cXdC7$!F=RM!K5`Ct#pA0=5IRSVUI4sj3tuVt@&biQuxPhHrDTN!R z7?uFsVgEXe(8E1Hnou!Jr=cwUTQ;pC(TP+vH+NtJT}JwklAbg_&roSgf2y~K{x70p z=;l9Wsuhf?lcQY0Cv6OB6{k+5rz-`jRq6K9s=%SB6tyUBYC0RJRz<1?q3Tu7sy3ne zFQRZTLn{h`)T6q=3~6R4%uiyrm0tF8e|i5g#$gD$_O`Xgb`{OgJCDa zi@CbNnde02KYIF%Zn!}WZs6-bl&aHz5SFfQ{YNRrkXOH6)~Ekq1{~A?hti^TwpPn3 zRp8Lp(#EwFg|+Kn^}t!mn!&P|HSK2M(2RnNVzaPa=pDoW-B!?pv5q}VWX&+y&knaB znE%x*XFUsAlZv*mr9EwGIcnC{zBabBZLcHM%D;$SViRg8CQ^N2hDx*oxaT{k9(*y1 zy%uDh1%ZPbS`pw%Fhmk#7zHm%VO)C9H@N|B1ubO!w-TX|`Pn9Ohf!qk0v;`sJ^W;XsU0$hgBF zuBe0W;D&-Q0uH`tg@Frl2R%UJ8Q!Syg;x>)4RctqA2zLsN&F8JrxV30X0d5s9OJi| zwZ@FNagcY6=bynhzGi`gB>r&+I0(7G;{Ask6l@TszWFJBhX8Cs2`+R_GLwOLtR5Y^6l z+$!7i{^T3Ibs|*_5H5%wCVJO>YFD5HCTyb3Kn!Lqdm^VbgJ3`FhK){!y9U;)f_Y&E z72^9J-AF8F1+7%5TA8zAK&D3>;^J1f)eIcxD!D&C$$yUJ$LSu$y4y`pcuSk(EEhDw z6;6zQu$<+-pr?4dA%>VY8{h&@Xu;KR@Ptn~<_+Jh8z6pSdmi<*Zm4*KLH}$~vu?cO zMg_Um8D?(%MfA@{C9zk{fO2rhd!apzxnJ!aZ(<;0-v)6fa5F05fDhaaddT;o2V9B~ zyW0)Cc!nO3j%}0bvl7fuw#55p$TzIk2Dx?DH(U^~cl*_%RYNl# zzfyMVUFMZ|d@~j+UV1nk4x-KsVHK;c^ zyyTBh%B%;1i@RIU#Z4?ok&k~hQNF;^Z~pT=YiRukSMoLy>8A(l*Z+FmHc_gFOxb1- z;>T1tXIba>4+gk?g4KRWH)sVBf1CDqtA%ZwmVYBSC(4IETW1h8=2aC{2`hGI@wN%P zuwmUqcPeCH0488LMPd);4HribboUQc#tbfoVQL_ItjAp(Cs6dncg(dIzzd8Z1~tch3Wis$;0ymof@PRWA}1D>_YW#KfGr4IIc5+6*9yEa3OS}v z+jdl9zz9`VTGlogPDfv@U|~W?gcD(e`G$m-rG$IO3r@IuQ8P`ps0;4vVZ0$ zfTHD9as^wdsEWWvR~bc&6lHXx=7$WJ62P@l^fd;+Xb`+0j7*h|Yj}*Eb!}6xDinxQG0qe+^jX_}{rnyIOptI3+J z>6)(zo3SaIvq_t^X`8o+o4Ki*yUClq>6^a^oWUua!%3XQX`IK2oXM%2%gLP0>736A zozW?s(@CAxX`R=Jo!P0K+sU2X>7CyRp5ZB;<4K<7X`bhap6RKc>&c$&>7MTipYbW5 z^GToeX`lCrpZTeu`^lgE>7V}zpaCkN14^I;YX6`Iil7OqpbN^N4eFo|3ZW4yp%Y4> z6>6auilG^*p&QDf9qOSU3Zfw@q9aP8C2FE4ilQm1qASXxE$X5#3ZpS9qccjQHEN?b zilaHIqdUr@J?f)B3Zy|Qq(e%iMQWr+ilj-Zq)W=AP3oji3Z+phrBh0!RcfVIilteq zrCZ9SUFxM@3Z`KyrejK`Woo8pil%9*rfbTkZR)0P3a4=@r*lfDb!w+~il=$1r+dn$ zed?!w3aEi9sDnzVg=(mWil~XIsEf*|jq0e63aODQsgp{nm1?P%im92Zshi5Fo$9Hd z3aX(hs-sG(rE03DimIuqs;kPXt?H_;3jeFIDyy?ftF>yYw~DK|s;j%otG(*0zY46u zDy+jwti@`q$BL}Us;tY(tj+4I&kC*4Dy`E>t<`F+*NUy#s;%3~t=;Oa-wLkbDz4*7 zuH|a3=ZdcBs;=wGuI=is?+UN+DzEcOuk~uL_lmFis;~RXul?$;{|c}HE3gAgumx+d z2aB)?tFQ~punp_54-2soE3p$xu@!5v7mKkOtFar)u^sEN9}BV}E3zX?vL$P>CyTNv ztFkN0vMuYfFAK9VE3-38vo&k8H;c15tFt@HvpwsxKMS-$E3`vPv_)&QM~k#ctF%kY zv`y=@PYbnCE45QgwN-1iSBte-tN*oI%e7tWwOl!aVxiTOSg4vw|9%Td8@a3%eQ^&w|@(`fh)L!OSpw=xQC0liL1Da%eal}xQ`3D zkt?~AOSzS6xtEK%nX9>*%ekHFxt|NVp)0zhOS+|Nx~Ge}sjIrH%et-Wx~~hnu`9c? zOS`peySIzGxvRUo%e%enyT1#(!7IGOOT5KvyvK{Y$*a7}%e>9&yw3~0(JQ^vOTE== zz1NGq*{i+V%e~#}z26JI;VZu5OTOi6zUPa+>8rl$%f9XFzV8da@hiXcOTYDNzxRv3 z`K!PC%fJ2WzyAxs0W81+O#i?IY`_PMzzM9t3(Uamy>(O+Y}@WVFfgd&Y}s-(owT`D0+NOvgRNcYeULpKcF%s1Bk+|Rq8wZC`0>)Ct1`}_RgKdv<+ z*Zi*IJdX1KZ8m@j8i_a>i4_`2EgQ*E-s8MRs_sUb%|<$cZwwsYm=wO8gv^6h5c zH}>vtoSWac37U90n(inx-L-5I2x+>P*CgECB(m8gM$jz5(fmN6`JrXAOi1&iyk`0C zX2s2BWrCK+94${2TGT9CG(uXQ<+W&aw`gy+=n}N*bF>;Nw7#@#H3?}o%WJjhZnfHM zwIOJ;<7j)W&}MJh<`~lEoY(fYyUlI0&4ZxblcU{Rq21TA-9Mx~Ft0thyFGNXJ)EE; zlB46jLPwNkM@&e^$GndC?v8}bjwFK4&m5hp3Z3beotYt>*?FD0-JSWH$j(B7u40a^ zuL@nImR;o`U6pxV)!kjSn_cw;-HjaGO$yyDmfdY3-5q(|UESS1o85f`J^dU#g9<&v zmOY~(J>z*jlifYjn?17xy>lGB3ktnUmc1(>y=!^B8{NHIo4q>(eY+fe`wD%BmVL(| zeW!VS=iPmmn|(+E7#b%Gs0hQff??gJAClBqvp5IT^ z(@(S2PX`)c;2dC59JpaMz#2MmGk<`+XMl5SfEzT(%Q<*QaqzCypg`#0z5GGpo2$ zM)WyH3>8OST8)^5j+o_-SoDloZH?G~M(sF9Un`E)x+Zy!%jd^m8 zc`J_jT8;UKjs@nA1^0}FZjFV5#v?h$-z$zsS&hepj(^M_kM9{z*cwj)O?>8@NL8Fj zx0=Wdoyg9g$nBZP-3+`XLB;7|tLf3u>GAyO$)4%yt?60N%pB*;g5u1Q)yzuh%v%1; zM$gRF*31rQc9(N@Uvc)(YW6sE_B4O?yl3`uYZeKDqj4eOKqWY)H5@w(j#~i7?}dZ5 z;b6i!BCa`Nr8!dTIr6Z%>jiUEy>m3%b999B3|#X}O7l0Y=UKz%Zx+n6_s(-}&vO$l z@NzBOQChfby&w>_aIauNxOYKhdqIqFQG#pnfzskb>qVKc#YY8;^1X|S+l$JCOOLsh zo+vG;SubgXEj=q((&}B(-d@rrT-N7WHdI=EX}xR`wrp0gY|*=HwY_XZxMIh(@>*%d z-g?C`Y{j`?D<%)2-Jt!`8D4)^mH2>-pR3g@hZ$TpM4NHcG8G%ELA) z3pT2IH)^*x>QS41xHg-VHe0MW+rl`v|wpx50x-)LT$sE(8$91MH3j z>*-++L=xTy5cJQa4Q_**`|-hgxchJ{5ENzaBV>SJHqQ|{Bes_d$k(iKo9FP~+Vbuv zW0AuM785@&yKlENZi9Hh2vIaM#P(^yHkuy7`SVT_8iE|kYs&*%P(qLk?Vjq8SIln{ z>>!%vaEQ2fn?CP=lgJIAyA(}(G~myhqG$v>XygsJLxj80$5jh0EQhzLqYoh?7Q6Pv!Y;Q z+e3mP;8-%2E)w9t1Lm{Y7t|#WgP`BtIk4|P)J-~c?BB*u0?Y8Afq8)ET>H3jU{PB% z=VWXy2;h7M-`NgfFuQ%4jI}NbK8?VJHsG+?qFps?gY__+Z85+Q%&P`GVjiG}?HOUx zw!a=uuK3oYBp|2=GX##l9tN@%KYN7$f*NpFh5#82xMQMt&MLT#^?1RvC*gFv)(C(z zJ$4}&0YYHMM_?}zp9CWTM)cSWwrIRbCm2;HXgo)2a5SsJJ%(^x5dEct9%67CXS?7^ z7y`P25rUGy+5iZ73i7mTWkC`k1LnX)j21DJW1CSPsmg*{!c5^4&SRcE-HiZ-hhN_6 zW$Lv#uL&Bw9;7#_qb6UruY@V2wL8aB>RFHzP2LYMvnIUpd}#G?+M_70mlH#4PqH4T zE2TdlURTR|R%*R8F}$Hsq~8*DLu+L7S&7+j!OO{!Ev*W>rQUR{(QWM-=i+ZQ;@skO zboAkjcyPZ)Z?-YYW#QY6yWY>Y(9<4IaY=n}C-=PTvk%ZL#QH=bbdV{}@BkLzBJF%# zu%XSFVWknbQBP8;f&MkQH6&huU%i|dY;~{i;2Ypppx)r*)amQh{&eL`o#`|C&GAy3 zm6_>tN5ou9cpo8>VnNsYQu54@;@X&^0Qd#lMfNgU{9|*<`|lOTb*pg)l3Ad27a!Z+ zW2t{OWf-{VU*z*3!-%+jKw zzm@(gEA<7NcQFz_cCF1HyS<%BRK94f8%GRPx=^G;!Gj)gKj^L`gwVzfig(YQ%}k1E z5#Wv1Nzq~Wp?4tpr5CH_de5`l6u5KfM09G7+76TU$J$N{QI0w;8>QvBUMx5aP<_%n zLA<__2mVn31NY|9CSf;LrLZV)=2ShXvwK*9*X!t$xEvU>ikr9q#)+;|u~1KZbmzbY zMVY#ucV4s=Ruimrv4X`BGW=1QxB+lZ{#I*Zw92>MWG<$ENw#ZFp$8VUN4AW&*0U4% zB1Cei&@ZxNMGjcuWIM-^WXHdGu*WV?Z#(~#KfRkQ+bUzD?A*F$f8?!A-TBeE z>4W{0#-NgT92<5-{HuvIn^r2v3%hov7}r;w?7G3+Rk^oaMf|7cgV}^Q$=%)z$O~M) z8G53j-QJx~`PP2SFvcw#3IH&iVqoF{&;T59fGGeyfD8b@i3I?0sAVJQzj(Lfp~~D* z)pq{qZl&*JiBejEDR9efl@>>dhvPeX8jVFVw7sXe$A2(JwA2*Mbl3Pr`xm`#)jMK% z&1;WuNXMd?&p*W-OqWdJv%}JU);ZdcC@zyDl~Y72n||Y^D9GhYdXJ}ewpZxsK^k{D zbBtHF(AQP9FE`~!KD;Hdmh)HKx|UZVV9*?R52z&-3o{V^$6-IC4txE_Vf}s_Hczk7 z6NmWwVfCTv_zXgyWQ+B))yd`66r3XUr9-ZfdObZUPHT!lm-iYnaRN4ak?f@gfV6T<8S>-8K>| zQfu@>2TPMwMChTm_psht)Ldtfil7iM_YoX`H-vK%$K}cH(<=mVa2gL?-%V8trgxUB z`8e>h%S%$%>!4*H-WN)w#hZXznlqeJgU1*gV3|Ao_ir*D_2IYy+0ni? zLYU-GAI|r0@{ajbdvR)8IQ@U3A&gf%yXynWC#=n{l=4j)i{v(&$E_5cymlsS2>dQ}0ca;~;)(M6o>sD^m#@tR zDtceOyU_lgi8k@MrPUHFRX$0y+`e%hJ&Cjd6a)9;5?txl&jlUCn7&s zSC3_5I$e8tezLEeS6E+(x*;16IarRR53x~C(7(Kt6%dtk$-y8BWvX_50127#B3$O zX*qi((e-F;B?*dkYc<)2%zX88087qlN(le@YHEb^t+lid>gH?dvBo)T8J`^1*D{m+ zZmnmfMVqf@XQ3MMa`JvOE_?q{0ft1-zphWx7aEjGtS*An{wFLDu?^AZ&xfvTWnXZX5?;HZI*3pS0h^4cWMqs zEOu&7mUDONE`BuRVRIlFFvu+tjX11%h;IY}n}{Ze499LWiH7BF3x!GEZY#Cp=58Ck zKgV7>bByI)2U}*|UMENS=3W<18^?Y(->Bt&kKjt)e((L`&HX-cY|aCi6uH&+hP?cP z0R@4rgFzJ;&cmUn8ditHnkM;&BQG4c4oCI;IgiGSVyuqFO*8Y4CM?UhjwWr}IFF~^ zj9MK}JFVm&&$u2Vw~l9_*jy)YA9CxHxd7IJllc&V?UMymyZq_m2Mz1frC5`K?+tm| zrz^?+TxY9kG1g~mS(&uce1lnuY#Vvi?rfVyT{>)A6_d1V+m##JtUI;mb*zX+{BYLY z7AjrVy$*J6*8Lt~B+EewHmvHvPE}XrXxyS+Fq$_-C8Ao@1xSHBgbJ$<5 z%Ys1cb9tOK&m!+%j%S|(-VBgk&*p&Ee_$srtle0qx~CM6~dP%o=`Y*Nh+4rVtB`UnroEX(Xg5$LB!whXw&hlIHWjQWC7@~->~irSn0~KCOqvJE z00V~5UNb}k005ASR4QBvLqDh8a9|M;fI1nQdP$Q(Xks@E0>u)UZ*Rlyw+nT|Xh3@| z0X8NXh_F{+rovA4_kiq00)*0-cUJv81h2jS7_!JR9_!;_MD*dRf01om)|)%=TGYwz z!mS-aFYa%*#d1E~0GK}t(VCHot};qxbu&wP(K~>KgoAk-f!$Kl^tiTTK77c=U_}gg zRF_c(+qfoF4hu@2PSL_f{n_8L%%A)~oEdNp0ySxvBaGKmWOp%BI{GkAN2IramfX*d zCph#(EL~jsCKeCWc}9YM?84+N^)3N7<3cjt7MQwytuNpV`+8D{toYx43BTW;hnUBI zyFZJ)o6)H`e*6;t*X~boZ#){g|Ka|0`HH$W>x#_NJ47lU;-Ag`kvf7wZ00s;xTQ7-M$`1D)F+?ZBIYk#x#+ zHQL+7G$}T;$s}*~5er>O%Jp?_A3td!>-v7&pICylZ_(4s>1>N>*x+8M`xEXB!82BK z#3CD`^bOBG(($2o_;G(akyuRVXT=5lx<6z4J&Q_XW&_tNSLT1-pZKp>Vdkv2PME@f z-k(Y`j1uxJKkv_U)8{WSY1B2InGO3r`FVc^lJoBHGX1(g?=$#7;{V*A|6T6Sg0uC! z^6j&YqBgGc&5}{;^R0@Ng7fW~eq&+1A@}yCJ3usg$Gbt^8HHp|&BZ)GM-W z-!Dz4?o`_2_O>4lqXcR6*pJ#JDfc1*6w(;UJKCj%_97!f(wJEvb;v#6dl%cC#v;(s zq2#jnKA9k$P3BRjYTVw3EQR!28XcYLReMoIA?X|@kGh^u>_t~}r*k=Wbm^S!#WWCP zd~X;vpxlpbSID>>)6s1#wEqznlEIhxsK@;Ie%x4h27h@+kG0EwJe(j?uQ?%!~3M8AmZolDre< z_s*Jf(028Z9`A9WdUy&+>FNPb=i^Y(@Kl;ZwtaC*)kv1`v>SJ`cU9t4qX@&(Iqh#E z^eCTvJPOO80EtUkoe6(h{*WnhxTfq`CH$E%Dy#C2xpZWXaJu@NOodV!1^efNS-oLt zPcgIA;)4eAdr5MI4s&)&ZjHccO7kQh<~|?o94V_h%(o5AeX$}pS~YQ4;M9|=bKE&v zcXn6^1?A~s%a47dJSqxM%rhYG8fz6gDvk)vGh&q=?|gprCAKHeSfFdX*X8JIGAQ3v zMt))-?x-Y7G2dLHYhtA8sI(|F-_k^WasoMVR94ZGZ|&GMIdgVY-T*4F^_QQTr#!A` zS1foH(>1j$bX*AwEqIeDKfV6^xN5AYz@fZrdfVl=8V)LSYLlPYi#x7aRV;KF?V35N zI<7^87P_v;&z?;j*PZkfx*vDVUY#A+0|<+t*a~oTs*?sBr6Mo#Za9|kNh2hz$cI&7 z4o~ak8%1xCpFsB<;oFlYdcxuW8HM?4@h8n}O2t7M-ScGCCoMc-#UUmN3zU;5t%AM9 zVUFDkwC5*n;)Gu!{1p}%sZQHvmA<@->0V?JKJ8Em`|=@EVF}f<(5cz`CAz$OiR=af|?nC^Eet@7j`>H;RQgzNiGVKB2 z01>Vj@1mBdRGK=C2v@BaQp<`yOn#353(iui6^|Syo9UV8BIBM`lJNti^;QgHZjE*7 z9En|otXiddeIiP9M0CYYW zkffqo2H>DuIpUVGEK=|>% zQgPFt{!akHw0s$gCi3M@?JF#JW`8`ex+{u*Jg}BG%7+%aV(ClPquI+4J5lkh^d}&oD#&++N?+m&TvY!A1l`^mXN3V64VJ@CK=^7gdsX28=o+NL zLXLFS)|~FIey<=GU9#<(!K|ahsrd20+L-N261dp8#AJg*G005ep09j0U>@jd((&Hu z5&m!=5_Qu#R0VlEk<&Lffk|;{(NzXJ%3lvGMh9lD3YaVVuLl;B$u1Ey#RD+m=zQp} z2UcK|C9Om3g1;E$?+4bc%Q5C(53E7 zjFp5x53K)V53E0c@c(f@5IXn_3C)sXmFo|Bevm@ZlO-+C*&pU|kV+5AmX(nkco%n& z#-^Apr_nhOU3HMo6Pm4HA~zT}agZU1sx5Nt985er$P@?VsQAkbrBEJb$tvck#&iy4 z2pwjtgyuZWlpD@@ewd@#lcQeVIb7g!n5zfM)oha+`4V@SXR7!I5dJp+!ZLsLDYC~l z?CYl$g%zR6(_W|EuZhRqE28J8eNgEMz6_$(Sda_M*M0mmIAJxK^Q=EqX*`vI#_YXf z+&~oVcsg%_S%g*GV8ZrTrUZ?7XlUF}dfiwyISoKEQ^+va_%K(W#^PCDl|gCQVLofL zh4fCGL9KL2t_P_2aXIC1v-MbdxX79j@at4ifnqdnUW)#uZQu=1J}#T}Q)^O=+0^19=m9ryF|Spc}A0b2>-MRftk zQLgw#-iz=PzLExYV__! zS6?jggjaT(DDB5hUMvgtRrWge?kApKtcZh!?5of~pGR}3kjZUgA)@TM7ls%>3UrZJ zTJ}ux?-bh?- zX$q|fzdY>&(jUB~bj!GJ_rgFO7tH=Y60u>Um(Q;Kq)O) zwEupk<(E=jWl5SG#f_?!HUAe%OKus-^mnD@w^EIdQd)Q(RHH#+z64k357e&e$*0I6i8L>ufS3t)N8+m zTdj?uK&tnLQtjV~WT7~BIz{{dsk++p|A5pV zkop5s|92}b|5K16EJ?;zT$Q9cg9Rv+q>%TZd_rt+RpH-jWP~o;9Vn zXM@RvrCBnH>#Ff*Ls?3tIT}6d>eXk%MPa3RCW;%+C(lMIdP@r&dp2~=&qf;v%ZmIJ zHw~!H$J&+3zQpuw8VjF~!@|l+G8MPXwazETddtend;ZHH_24z-Adgc%B+S;gFKCXy z(J7B!$u6)wXr8pZB8Ml@E;K1So)jX5Q+XiB`cu?P>a`Y=1aYuoSi|dUbz(0^mq%vsF$nduGqG zJnFV_--@DhyLm6^Y}iC~FF46<3ru%DtvsM{^qJ0m*CXkCNoL?gE6M#(8F_?hii~uU z2w!1^U*9`^j3O=15fo_&yz{uNir21!2jl_dixW7~Lq+Cs`Vh|TMNonoGxO3?c8!8(|6-~YWOW1O z3ixN%n#v*3_RPnlqx~&f{c;7cM;l~0y`Jk%eh^Hn_=~loEh-Kr`trpJ|1s6-p+Xo3 z1Q8woHPzDXJuequ=_5Zg_)E0v6))4yE-c7d^a9!#Rg<8P#XxdPIa?kW9t1yH#y4XYEWbu9g}D}cqi`FjcV zA6LMC16RNw*80O*e^~1eYyEd)t^Wn0RTa!%=^%yba#dEjY9OZfAVc_aO(pz{DU%nJ zEfcu@wEXOSQJ8(cK;XuUj;N~-d^Kh7sed&3?mbHiQ#DY6h#bB+pL;ZQk zUcoWNZtc#^Ghw(g^GRb&7Qzn+nJ1q8+;YeLL70cDA0xLjL1gV78=mm;o!O(Fw^#eg z;M!Fg<+FkKtAi}%+I5ZDeQR;=L&$oo*y=8LOt_sKNzn-K69-S;!8yi6ftWZV+?u)h z7%<-=Mo8}pu!YjL%DC^v%(?=^0h9-Vbq5=%7jO+EAU_#9{Z5$;-O`=KdtB{1*I%MRJx*giP#tg{h|eBJtWcn)2c{`>T>^^8gyIK4d&toq z0k8>UX_2F@m__br4fv!1v^C=38EgP91keY-b}YiBaiU#EfQaZ_u>qibanGAhw9^fE zYdn8p8>CRcd-*di-3hoI<}IE@8%mEIM(-_?Mcd8ej{h&nZT2sf%u)|vg!RMhigM=u z+sTdfKRIsPD1FL~@^hBV%m3v^`Y~?AjJ`%)`DN)J<3=mw6^Q5h-^PujZ-6mMf|S4f zNGO%eX4Kmo`8RU=sgiy7Bb_~*8~IBmvm!(iqWnld#*H>%ZYY)P2e~1)R(G*jgRw&D z|01`zr_VhwQ7YMYKN7vu@#E+SUGUFwW0t=8d7v@Gi|Uu3lkwTbE-S+W+Qi>}&O1HQ zzx|xwt)XJ(m}zqjkBi*;h4E-g{W^_HsiZu9dGLk+wKEVRMDIdc0qUwzN87Yu4_3a;}%AvM*tK zy!d>2SBa)JH(_UF_k03v*0C`>0WrvXF-Js4({e{gD9q#j8x*;%sMPIsSKeQaVmbkE zxbJ_8sb0YbyDY(hG-D9AwT}y6clZO^F=#7Xtlnj@;%cYVqgPZm>223w-EjlB4#6%P z?o5z$@B4VP$j)^UwHb(sq5-nnAG%ECPl06nJ|gw;vE3OCu7^!T zgfIKCAO*W#abcNk;UvKfCu1$0I#)ql<9i_G~KNc;C&Nl;hYDq zru!oV7zT%ePr%3Io&a$m5&|GK^~^={q^xjbf_c(Ycu@#?EopeMW>GR`c>(miIGreQ zaJ*3!Zg+4fxi#I1A~0-=aK%r+H4)x?v8aCu08#*IRPa)H@WO1drA#Ti#lc7v2l9Y^ za$r8pSN)oAQa>INf;Q<0fa&4c0{rsk@SA@tcolHGX#j{W%P+GL2bS#G4)87Z_s^Zf z32VSn!3ikf1-BkU{ysE~L@+rw5Rh;^T%86Kf;+cf&3NknMWI=HQ4ucpmrtY(&&6m z|5v=nMlqJ+6}HY(O*g*3;yny#W1n7Ol(8B(-oN(SeFmWnoDyfsk^Mb1RW(VoQgCt9 z3@Y{g9q)OiD9dPQRwj@lsrc=8yywmh0fR*7uXqoeM4xj@Y$^xsiYeg5;|Vp5;QtWs zQFnwqAiT~D;cfn~zMfn3C*Jd4jQ9M(!9O_o2M7P);2#|P8xHD$|7}CltG(F1nq`5$ z)82m}wph`hYC^f;fc??WE9(YYUnAkWWZrBC}_Uu|XTUT&}|pI;V- z0~CN)yWqZyS(J#kQGbaN@~#|d&K*6l0z80%G?-3M^jPRXF{=L8gOGv-Lg7h_LxZ9~ zavbXGPEaZeY8q20T?N$v4;V<~1rqei>jxoGR0jp27ekLSQR}NHClO#K9>4=}@H7O# zrs@H7t?5C9bqpz1?_u~mEk4QK}{UMu8K#Z?~_HeUc7Fo*yTio4&8 z_08M$dED-c4Z~A!cQdM>GH&;MC(HZ0GU$ z%;rmo0Q;y0MKlsX+kL+StO{;?5iYU;KOO>r!GqG!f&(-GuPH!vDtN6FxQ_7Pj7EY3 zai52=fI$EVHB!zHC#VxAIEE2)YEtGSz(guO-$hM0&SX1f9h4xQ2LOcOHH(KWwNr)w zKwJR?+b3apKLEDFG#mx6EV2LO`b!410miX#fxqJuMahlcKg0X~F90i}n(K{D)MCby znf~d~*NmQc!Po0k9bYs1lK#hh<-=?@v(wudlBTFl>ks@$DVt;UUP}H8*Pnbj*U+0M z+DaG}5Xw77%>k#BHNB%%_SGW{3obl)hHVc2?D|9HD~+G{^7b==zdD#0#Ikv&Jp(w# znm4ycw%uK)R;x^!Wy*S06xQ62Z(ts-{4ji)|3+4H(mJXUqDmxn%z%U->(q&LC~zMO@clfDT!yM1+_6EAD*#*H!n^UgcOo_WOdLQ zUZI0u%|qTlo`SW+43?MasBO++LW{ z-9l|F4s)-G3-&*yhF@D*E4lVqQj&!K*~8!YbmLoK0*)yaDZ+%b4lfs;F$gQct+7#6 zAr*v%56Gz$=AYA}F-=N-vTOtAkv70nNfOSs}8U>$^|P zbi1cQ8ZzB?fILJ*x|22QOEg@Idy%SWF|4MpX;<2N2Umb&ss9C-%Q2R8BUTQ=TBHZ$ zMk_MZB@nE5kb%pzDGuSx&W2;$DGF%-@?8{sb0H|W^FWcBuK}%_KtV7VbYI$x(=5sn}WVZzFiI1$qeES1yef~?MNZ zV?|#xaOU+hUzxZkKfjF1R}S!UX)!4^5R9@d3_)CoXqP!*4i!WZ&@{`q4FUz`+=7~W#|trs$P z7zNU`{17qIbN$2G$o>m~JuP-bwVwl1bht4#E57>Di%L!w6aw?`)&KX_b{@IhkXul8P?u#F)cmOLI-_iP!DCHGCKK2o0#zHE?V(#e&j338QmqqY*6{ivGmN2#C(lxcZ>x2<19N-HkVlCwK0epa_O)qWYLEH|OB;P@+F$x1S$=cE-h#a}@R z4lDAe3DHU5t037>-0{6LrE_#GtsG<0R5RlykM7vr%3DvVZy)4z*Q(}6h+r^ynNUai zqt*5n<>J&x>|O+J&{z2F)Vc4bYFDOo zlRW{wx|X8)#!tKl_uA0| zZx>4ukm(hxbp~6+*=<&=)1b$Ng~q(X82hirF+$#UF1M z-9?CVZLS=}o%N}@D-7gt%oZ?P^S-{HQINS%VRnC^MOZOGy7CR8U%5gQgr{w<;6JNX;zs%rDGBqvm zqd@Jk$mHAbj|&jAH^~x8ukw$oC)ki0_Br4$R?t*?m>p+v)Qwwq0fki4-Jg_(D9br~ z-D(ym9?4iL*#cQLkD2jrFmf2s`*@8JILfR(kVNdY%f!)RD_y^TZA`yep4`iR zz!Ph{$d5eyV+!b-qN$O?}gyq z^2%o4k0;yLWh{Dp!*o5u9DC{QFb;WjJXO%OJkywnSTu=RF$U_1KP2Psy*rc0Lc=6w z{*0?Ai+(NEPLXyEd?d5#lO#&#!b)(?yLBR|91l?=&qL?fNJ}I5_RVFqN$02}=zO}5 zeGeQ{wKpd4dFIL0dc*$W1ZUq7a`yGPm1LU_g%2J9h{$H&o_IeWbJ9kGm)B94LKNhtOWx3E|>-X3z4j3Le_j$tSEbdG}m0aQwbCejw& zLIA{G%*fl1e~knfM@j~!@J#UcFja#%Wq5JPN=}wL0vxSo+r0AC^|2y#ys@qEGz>s( z?pra|J8cS2fFPCkJb(;4x&yC|o>B$Z06lQVc#Uf~PyPR7xz`%OU;B#h zMl7r?>tZq}0E7F)NkLZTxUfCRH8byL2!gW4{H(lioU$-zVG?yv2Z|C`J)&Tj6R(#` zI-Ko+-DCW7ONLi00SZUNdBy>g>W*Nn*WnT`0-{O7$}B1zEZPnP&^gJO|DnrXi(^l z2<#d-VIBZ;^c7@}2cwnPrGyW1WQ$cx?7|j-9lra4@`NcBMo`zFbV5YdL_wxC9(ArE zUzO%HT<(R9rr)dn!M*Y(!L`uaPPS6KvEJjB-wr95Svc-ylLjw`24#@8m70#edC;yZ zI|&Wz*^jY9^S56JTat=o=DmIBgr1F~7zqJ38nWn&hIe9u7@g3GoV4eg@UVuj;hV(` zM-tk@9vOR~(~uC`;gUg#YDH zAuuRt3mRpHDiYjh4D*a0X~Z*6#7tx=!}B{N50Iez@~x-F5q>ZT3@n^&aP z(7lwwXDQ4sDJ<)bP;&RTe){g}DPgkgPt@7?0$(O{fhz3yBwwge>W3S({9|XthgHk zl^JZ0$TpT97vW{1^IFFoDRf0r_!UzGzt?rj$*fWFI4ZZxHYfXZ5EXV7q=0)9^Kts{ zF#drdP*Di74Z}mn0PTCCBP;Q{>BXlIswsG6YB>z#rs;`tAw_SrU)kp;3+6$}al>`%iwzHzw8Iq$EN zV>D)=OMvdE0hz?ng(9>u%&^hmSt1Qltm5d|t3CoSpg@!7YHao(50{Coim4u5pc+&&O{R=aD@h8t<>J!CyFC(T*_4FK_hh&AgZ%Zch-{no@SOuY4#C4Z7ob(+SNd0x4 zCIp3DIJ#3foFCYTqE;g_y!rC@O-anPM*@&;A&9C3P{0-gPcT9SN@tORB?kerCZmf+ zU`s9HGIpWggGP{EC!Ra=w?Kdc<##;06){4@Imi)c$Ds^NtnMToC^op}|M6*xopD*s1l7$21K{2&gsABYtwvJ3zR(S5p52qQ zec^`K#@d0bSJI!Yxbcj>=7a*gMHlfuge{N@x4n!QmGn7yK{B7|Aw-SA!BT|CdaHlq zazFaKs~|=~Y2@{7i=#%ai|bfZwI`h<9s=rfxv|YW43kBr{&pO}3^d{|?4G{9`}92p zS`phMJwzfUB8H`&d-FzKGPbFc$iNW(OE@8GcYQQD!1%OTXVZ(65z4U(rB|@ zaVB7-iQ*kkWXuB@1<0zmgJ}qJ)p-lstNLrj4BC+`s13+a>UP2Y$|LjGJ1$d)sg5*>{C-xJ6tjvU@lNoxUj(?RKrmN zWeeMUtPb?8bMWxE=qPjTs!%X2ztOp%M&{$Bb4JN&f2WS*OK0n5`$F}L=$em}Q{BB6 z-8ds^UHm;g(mkQFJ<4{q#t&Ah%)FBTd%33|3KdJ#2h z*L9se{v zo_IN)Of!)pGLfb|k>NIxl`xT0JCQd%QE)j?L^JtCWU@qivdnF=B4M(scCu!AvhH%S zfoAHP$W*iTRIA%md%{#_?Ns;lRPW^!jAnX3WO_(@dc_LASxuN(ubtVPp4q;fLD0scClQWU2Pc?;6JEg~v~$-)=SXzs$lT{B66YxE=BQ`pXs_nzY3CV5=b3fpS={H@ z66bH#&2!Amb6w5z&@S8-UEtGM;CEjTOk5DETev^7AbPbRPP-^6x+tZyDDA!|o46=f zx2Q0)sC2ccLc63Yy7W|MN!@)(GjZv8-O`JhC7r7!J=$dh(Pbl@Wn=ec)5K--x@F6m zW$UYDTiTUZqAPE7Rvg?{oDx@D>Q-E5R@|>vptP%AqN_eStA6gQ0m#JFpt{wNnbokX z)d8xkCuV*E$=hUs|&8!z(tryX5 zd=cF!(b*_-->693sH)qjnc1kj+GwEN{3g1I`s-|U-)v9Z?5x}Dp4sfZ+Jw<=4Tx?H z>1>U-Z;d5xP1J2o&1}tFZNX``=S8;{b+%E9j8_x4*Xy=7XSTPmwh^>Dd!joBIy*=1 zJ12=dXLUOlGdovTI{-Qax)=gO7lGx0z)3>j)guUI5rjwtgl_km*e;3gE}6$JMba*1 z{Vw(FE-iAGo^Fp(Y>!!YkHuq;Eotvo{T|2c9v5+U!3khQtUuV_dwd?KsM<>uKqw__CN`Fph9=3Dt7o(_fXyAP&4W9dHvyw*+U)V zp&s3lf!L9e?vb&_k!jMAdHs>)?2$F{$d>N-mDuqc-D3xjW2dBJm-=JZ*<*L)F_iAa zOYFo)_r%ZRBp~S|sQx5m_9P5>5dscuvE22C9B6eP)dtT;oUXgTORexSHiwcjNH_%;t6T4{Ey=e8g zXivK6tiR}My5|vzMs90XW^&yx7&E?$xr#)oRk! zdi~Yr?A12%3PFe56GI;8B9Ara*y;_h0_G=F@uqLe7&c7;Lt zbX6tgVJPjxxTkBX8Hx#_zLaX~Pjl4rbz0)oHq;BWYuz!a)HgN1yzEJQ7_Ywdyv%C4 z&X-DK`$d)g)=W#h#*R*%+vOD|wI)LEn=gn?`jh6aL2D?Dm>>1CJ)_PjPTkf|&-RUb z6GS|)Xr3RK4x}q4Nhdr%G#|;=uJ@zSI~L$g zElC&Yesy_?#O4u4!)7iL2ZE(|BrwU1iX^bh>C-354^chA8ZS%LG<1FMsy{PHFICsFX}PPR?YLB`q3eOkuc_~U zqfFB<{2~9dmoYEPo|z>1@;^7rOfP$GQP{$-WmUdZre#x)De%Is?MC^F*L@EKwCzV< zmTNoC`U>bcucVjjyxnOL&~-arD%bTuVhZYcVzX4}d4r_|^?k{WEA;*8`~(dGSu-jO zg1K7-4MPQ%D-6TMuHTmn%(@ zJg|gJKl`&(nWlzI3z?XBIYZ5y#!YiygqGWYFT$W3bO+UWf6 zzv^Jkta;VN-FE+VkHAXJ>pn4TkvIJ^thH|jm1RWihc!%U?MHR}MI6RWGHV?sZQ4W} zryW;n9cMkTMV;pSS?in@!evCAmtsunoL7?kMP1f1GwWP73fn~AZk4apz1^wD7IWQg zW36}H?~@U8I~+BscRQZ-7jr*d$*gxj-)R#=mHDpJdmxe6dH_s8ada{m6kAFUowPz6 ziy!6*_R+(*At-^T4)Y>!*2B76AwlQ>LweI;=;J&Tl)M%V^I?_J$5X42BrAjYa{K5L zycB#uIRf(&Xx1mRuXsRv1oIceFo5_9N->i42gpbnT#KrZV&U%(RQ53-Nf&&0OT9lx zquGG0wBjL`Lw~R?h9O0Zp!DtN{ty!>L(1U_Y5ubQP#YgZ>Lo!Lp^^SD$7Vy?!wMPE zqyBIY3?q8Xd$N*b0}=jGMvSDDveNtmk>Nf@%s1{ml2ade7t?ISa<}r4lEc9JB#f7A z5AVsTMh|?*lzMqftx`_CY#^%8=OxF>d-Bgm2BOQGUvk-3%Ih2r#MEOL^Z4FVFd!R@ zZId#-9aX7d%s=?C&&QZA{hp$^`e58Bve}rwv{KR9VK9Cc!$h#_4AQ3vZ$qshdf3^_!F&?WQ`RM@4c8T)^DFDEzGjA^{|-KuP`hpXO|Xn z$De}p3y{@E`3i=~7hB#RtsJ$o=Fi~O4wy!-o&{}Ng?4HCVFGJb^0zqKtBdrLM{A2= z+b&?1u;aRNimM2lMprG}gHD5*Q_vWXUzaYT&VT|y7VC@~GNYFVY((B_^P0__#NM}{ zEvG2*secOxoB-=dTVuTH*~5-T)XAz|B82X5>O*EN%HW^|p&*G#J(8xz?#pfVsbT}N z@>m_g)&dR#jWc z6<*A7eC?`bo$(7bQP6tUT-Ir*WB0Mgb%wG_kOGaTErf~y)zdpxY2f$u)3C3OU3+Yg z6PNToIBI|GiD7+@;_Xkx9Hy^oPpZ*BZ`pW9hH6!K15s02J{|%iq+DFv2mY9V)AJxR zOTbWb1ZRXdk=c_S!|E)u7$|wSIRP||93AVGonr008~pi;{QyKkCJ~y-IXcw_MJuN} z@8m0j__4u?(&GBV)w4ZH05G5dPz49;C>TlcX2U}=Wv=4w0>A}` zlTSSk^5|xo-mpF{A8Fh|(#IH5&W^S6jP!4~jJ^e+&PKhuYh#HGokr_Z|I(-xqR0#f z@UCKs#7^73^QH$}(U#@&UU52}oK_Vg{HfNpp>jN*77yzr{PXkoqCD6so)%b9WfYsf zeQ>dSxc#lUaO(9N{gb6Hrru)Kwg;(ApQ+?cgsQ&io6WtdBqK}~>R{&u-7pc+9)$Sk zSe)bC4=^Orw{c=mwln{l8cfT}h1FZ~j90D7?~tjjHfn7aIye&z5mCWyg zJG>k${>OHh)GK*dnOy^#EAH>@F!D)+ui1AOnlXDber|{PJ|_;Z_yV6??fAaO{73F( zwkAxp`r_wyn0gPyVx@cp&{Lg)~BsM4E?fOJslO+-|B2k9yT(m|z$Pz9wo zL$4|zy#@#&K!DIg@4bjf6I6EezMt~$v(Gzo_RN_%`^@u|PZu-5T=}zpYo!j?W&hI- z1GfmHYQTGVHU8LP(jGn|n{rx${^j9ifkTp)uRSpNR>Y2_eGjR zC+2E>>d^zL88ONoz{@ikFkTiz03cc8qQEEOfF{9S4=6Je03>*>>;OM62__>T+~J}= z3Jzf@332~QezXWWGX^9F6A&nc0zHU!uv8-G&=sg)F&@JPZ$z&tk6;4IROn{0FuM5GFjWK*Jf zDN6%^0BlNNPnJkq4k(lVK#cg=O`#b10GR4U8Y~URgrDHN$3;jj;+B9xU^mpUVf?vg1A(dMa#k_xl7{ORLd z;djK~5_y#4ggBtUQvH=)fG|zxQ5tqtC6R`9e)jAD-WMU_UrouC&m;slI(HY!z z9FrjBv-`yOxHn1odFC!P%l1Dj7=KBwZ~Xf1YAa^Mz|x|FivoNzt@KDmtC8^wdU;#W z+^Vc%BvU^!g1*=fVdU^8Fc4f4o!+a?_IdZ=1xw)H6^wsSnrc#hj8e}N5YQXm4MGE? zvJJc#QVCpFI7zntw+Qm5YIH<2(!`a?_^zoPAvJNSAEV^xce zB0A~zERKx>J~M`O>wi~-I*HSij31~F!U?Nzgzg?MqlfPn<@F_$5tCi2hS9T0Bjf2L zBqz5%=5xqXN!cag%ZDeA- z#ul7OdNo$x~=)~IdB>fCq|G2R-zJ6TpHxz{lUB_;`2wh)C{GSRno*uZ0oR=udsN#%N+%EL2QHlgLFEpsacIHU!B?&o@Lgb@d zNmM+{Ao?0E8cc9+Vopc&QY8X}v*`ibfWw}4ECUrtmA;-36KyCbyy9=ME~OjJ{CIW) zG#i+?>!_8uL0KS1oj>}OU7E^S$&Cv~ihy3au9d4j=w()=;4s7TG*!VrvS`24jmXmI z68TfXSXP}Sijz-6klaJ79-Fum5qrl0@5vGhHVlXH0SCzjz!2S$uf*ZKydeI~P#XCV zYQ^m;df&Gly$MoOx(pq0qKC8;0dOGeJ)+yBhXjH)v|)^6pRNjeEZELZa>?^N)!>&^ zrPRlg_#(N-Vo>@ z`=(D<3ru+ z5WC&Dm>3=urSR)JP+sJ|{xwh9X_fQ53WsXttXV$6Zj`anUD2t zhAV}xza^==FX;&sNF(@aA|^HmNWLQ`7D6Fw+Xrz@p&x%oAn~zJ1)UL0%Bhb0pi+4+ z6hf)TD;pAnJ*Tz46yJT6VwfYAB1z?PE0@EA6p#fk&$mbXw$eVYs-42#qz6_tVQ{V5TI?~(an*=4qRucK*tq?z@tTN4p0}bVWle8_)sts}W`v8`EmlIR zo`1O+OC?Dk^UJOq+3BNhH|D2Z)e2>YD~0mHbGsr!2-IHlKlWOMlL73$QsqQllb@zk zl5KcIhH{6}!cQnR%&$v`X;c$-@W}B$dH?C*Is28DBZ=yq%G5}AF@+i;a)E?5RIv0t zzDipS?~!5BHtD$S^k5}9t`hIp{yH*waInV#0lvX;t)r4l`5KH6RYXM`o;&5>4Kt_LoEg~gewl4gl;X`!?qj&TSV>d^nNVPs{nVDU1! zL4UBwkJai1X(tffwP0BnzNRX_?H-!Zj-(HHbirxE8CJ6ar@2gFd(Ms{t8+TBx|Vvk z>q>kQOcTbH^TzLL&~P*$=@nPumDX+TJ09Cfzm}y}vo@5B!4ai~!-X&M6o>g)S{#1Q zj9#s)u6weffY*iq)i@hEwaQs9)&T@cGA}WRn`hltA!y;gd%5#7R@~4}=D>cS}MB+>;1xCcA_e*(& zo58OsalzCu8$0F@I@u7%Zh8j45C|-UhLqJynnTAUpg1z1NG6o<0XfJvlt7>S13Xlu z80510m%Gp(n$14{tEdTV`%iflM#T8IWjhTAh-ry*RVnqK!T zTwhjC4QHcANENdLA*2mestP@I3gE(pTBSwsr-cfbKnvZW&VEpf2I!d}e}v;J<3yqP zP7jrEXCjk8BWL3$$7(J0RDv=Q!4Q}yQv^d=c(@-7&xyktU@Sr~gUb>gqY}N+o=^*| zs1%c^G{2~fw5Y6-DASTq-jWC&NT`EJlzkf185Z@OnfrOet7=~lVj(qYA+W=P7$=XI z!pAJm-1j%CsoFMR2uQ4bH;nd-8|fEI??;bri0u=MrJNK<+=RSK3mqkmzN!^hWE?#O zi<@+go5sbB%f^*B#hsEw+d4-Jr^R102`y=ey1ogO@PlOD49IyAY)@-^YG@6AB-tp# zw1aRanvhKT8aZf^01{TfIGdx=5-43%Wrq{Mr4LEcrLkvR1*vh_X^HzKiR_JuNs#C( z!q5V(xE*Jx3mTe)iy#2Mu@UD8t9yHQ%;PGHydNmxrG{kT<)h$|e0dV-#M|HBandJp zcX6wjCTTU$X_zJ{Z3#09M?tm{Iau%?N0K3?;8N!3ZCL!?X8bxXdU8^(4Irq~I?~P?zK>m-^*x(#|9|Wb*}=4`dGuv>~GZ;Gw0CFjy{Tb9UF# z27rcoV0{g+tzfcS`atyqFPo)1+E^k@xEr2Za|;GZlhB`lfj|{5E&Vh*&D5vE?jvVO z52rF7ZDkm1gN<1-iEN{GVbSSnZ)->+{R9J>wV7VlI&}(Xjfl(NHZd-J%zYf5ftkv3 zRS9WD$o4J~wH~ooaOu?NruAziMG{aY@d5QzsGKi@L4n^re;lT^FQ+RYsqO$M75H)* zJ^U2KXpp$HB^DB~gzGPmvVKPfuqEo8(g#romSV%y+WOh0?!a;Y(^vxO9WD}#2jfXF z5Ym%LzLk|SkyK8W`DrSbT|O!emRwjLF9^#_DS--+zJn1^(e*e-GjV{#vn0;tem1;g zq)UZle(8?O6i+0Zknhr5lgsRrGCyRoZKKI zr?IN=MhA7BeYgTSW0`;AmF-;EbcT^|9{;6tw1Z!?P)4Rm?_05sy!qw4Q$J6MKIcG3 z580_O#|JjaUt_||%8H^n8Jx)}`dDN1g(7pctOK4wrIL0NUzu;1s=;{<-GK=A;_Xwq zJpf28A7lh7^d+b71iu$sDjXr9=d&lA`9>7zu6T`5P|%%dc8$-b5TAdOYM!dVJMN4Q|6tx#-UbR{~Wmry6%qe>ta19E9#p-5__U=K_}VUf0vFMuSLh);Qv&>fo&kgU8F?(%pMWUF*# z3_#+*1`+{)+yEpVMs#|fe5DERdsei40QB45kTiJ3Ek4S-{h!*)*zA<_x3I)2cMAaJ ztb5ap#v$U|2|?yxi5vo5`75swa+lw>sp}rTstF5IwMvj*<>RuwnPtS3g&zUW$RQ5n2yW&_!(Hzmw)Dk!GHZJYw6o zr6o}{{mqwzDc7Dv5OTM4k|hj#q-d)dWAs0lsg~VT>^87qX4axgThzEFRvf8J##?DR zT3H|J1&QG;=!;H;4S&2_&U)ewT$U-kG%Xt9reyv>H=6*wKiM$e$bU` zJD^MhfNU~>_mtAn0a}`X+)GmuA#Bg{3PNWDFN*;YzaqF(j1Q-OOIHQliXjsF0g@tk zZP+5hxD_HD+a<%EydqTnxiPNJP<7S$aizqvuw*VYDulzCOJT0R{0z`#(PE&B0`WNd?zhA7a@2N5+gsri|)!oL%)+iJIV({*d%>~R0$g1=oGve z&Q?D?#`!hiZst4 z$gWa$<`nVx6!ms5+}g%0fxqLn-S7=%GSU}UZvrBlkAXd5R;2mvwx>2R*v2E5ktSOA zDt^n=laU;FpCtg4R+&l_1d+=Ql56F0t|Xy}o7!`Eg|H(6J+|0{U+Dx~pO7*DF7aA& zi&S4$%~@BP7TRF~<|gSqglIZ523zK%`@s7q%}PN7mL|?s#vJ)Q9x~3(-lT)jlt}rK zn7kG02YkX7cJp{jQ^g~E5;aDt^T@1=Tvxv``3a#syiK!|v#euiNW3BWY8$H|)~18% zwmX_*ZV1psVy>ep%JutxmzQ)sl#;&W8HxE)Z|)qn3 z8MA9fJ-5_4tjL-4V<5q;vjfje1?b75TS&PP6q`0C52)tn;>3tM^sQ(C=5ZL(HgFjW zfI^5d4-LjI6GS&d32DQang(o2MV-CFPB!&wN$*RaJ8xvI<0(y+%#esnLsMg0{i2t= z(u1t#r)Q zYq6_D^cW0u52uz4zVV=yn?(a+WO>z+x68?G({=D^nTKM`gV!GO`<}n{_c^~D-2QTC z_2uZ*my?_?XQCU!nB>FkxB2xGw=f<%?L#7 zF1j%u(#i$0!xGitq(?hPjIIobI(89U9GO)j zauDmB9_Uw71I=xNMng&=Ezp?)=MnDlxL3MwyhiO26@bIvS_L}v!;Sr@(%kho4SW?A#ftLoob$t?tebnz? zL&(kt{mpW)@M5|nq8Pba?WQ9Gw((w)D_=xtBp0WxMZxue1oRJoiFlorimQC3S7~8$y=t#!<0&O0%2Pll2;1<&;sg$QGBP(?g|B0QVt-!gI##3@o^pRRS=OjqZ zG~C@mc{yM9X|1D0K?!kVd;^XoUA>YicNQCziSmJLA=q7U@g*Yvc?n=?tHgUgeFL^_^9s#_bGh1VCzDf7?nR4jx z4l*UV6$a1!O=*UMSbP&qBh#L-&N^m$XK;V|pD0Zf3~p*4E(lNjO=->!D}I~KrG(NO zoQ2f5d^C?&@fBSE`KWp;{5Y8<`Zn8onf}QwU;%5s;+_DS48_2^HrK zNAkQ4z8N(tOL#cIlc4m0*L7Rg76>sxca-huaUJ#Ea)12!GWDjNm@Tnb%}q-2Zr!7* z{GO-i>z_CgCZ8q6dka8uJV9tozR#dm9;uK*sADhZ!aJ= z%s(v~>P*B3Ji|QB6H9sUI8B#2{c%!#A-KRvjVWD-{DH-ky{~JIK*k$*)D$mH_Tf~y z6W?f~T||>9%;{~aZIwN4_uGtSBV;;= z2d4Oupv3R>3$HFMBfA1-m=cjie^Z)27I(BNi8S+x@Ra5=563Kz?z>NaQ<@~D>U}qQ z*f)A)HWd4dhcA3uD>U1iyhgU3IN$Q>61})x6SfXhtyg78v|_)R#CA$0w+dDZVSB3a zW$-Ub^S;S-26^Q+|Hzw>uk1HsqVbgGRpYM#d!OFpDb1_rinoq&H(P&<4%BCG4fkGm z_O3FQx$wS{o9mky!nRwv?=7ST@|~5!Bq*Guw%08^W5)l1K4th#X&wX~1{2UwZzz#B zIR_?|!!<-3I(kjx@sy^Q1xOv&dJqvGw4us<{?PAzXbs6z2R61Hj8C0!2emUbYg*WP z@+ScBz`A+#N^6kQr)LaL!~am4&L4b$!So<=JN(c?<(;+%w*hjwP9jBJG@v`$8fZb* zQ?J69;t{c{U>k%uxh;O3?BY*KEZsi$;+3>L~BM)iqx@2TF+gq*GaL1y}-f@t-%c-p03IDip$4 zYX%n9uvP;IT87B~N@1NM74UrolE*;4rYT~wL zQ<{brXRs>C9U=d z+iLNYW)fsx1=K>BSLLjzYAr-nqCF?gv&1r1^ly}A$VHZtr>l5__jtJ#BdNqI`H8CN z<6?LEZDrjendYy<1ysx3SB|Co%m0nilt3nBJ=XX`Y1T<{vr;G>XaVbKe?Is~D*p7+ z`k$2ME;ns|VYN)03pQCT;DxK>A4+qg_EW^Or~D%ZVFgBY_1(|hyqnGE zPbTWyiQU{I6fBmRCL4Ox-8_?&(tK(+tUni68m&ESD#ELeA%s2#z zhI=Suiw#Hy*Fghy59e33B{#-(GUE@5QZ2UBzPPR{AP<lk&kul!lC|y4_@ErOgYfaHfesXHi7xg0N zxWx(Iy%>58@`4lJa#mrU#zty*pKSlYr9k)Jzbvm5p-^{UT#> zc3q~&f6%+_Mb7E$#vKxT7xA`N9`oF$re?rMa+_C?%-q)f$bhlJ+umiybKAx}0TZ=t z-XDGEcC1MPaox9ls#E87oiqcd$J>1B>gV>JM+VNW-S%yooZI*937kJ}^KCnw`|_G3 zXp#7iUnleYL8NBTGGn_RN@o7+o5-M5{yYAC#`A}%JwfYI?f!$l^WWZ+yxdg36EKoG ze^jjba@(*yV4{Bh_*3M|U57h?)06Wj^*t~5z1sulPcP?B+euy>MBE8lW?ne!!E4o% z+k@6+7QPQfzB($r^Aay-KcDP*byC~@a^H91Vu9rKS@)ga?)4uVny=5t+h3j3FZ}!x z`TED&o!94+3%^c#UjI67fBoxp0nf@K01OBMJ_Mm8f=CSkG(ZsBBS2mVl5hlR5`wG% zL0*HPKq4r|5LBxO>LUaVumjA{LCe=cC)q)-*1=%V!D!#X^4oU`cy$VfcM2tS3Kw*W)O21&c3vCn zyuR8gdenIX*d@l$CC=9+A=!0PtxM9NOUl0MQrfFaCcH~FsY|Y)OTMN{0okQE)^%&O z>-JIC9UxMP0jbP~RFOogsv*@3km~kG4KJi-I8rMKsa=57sX^XFB6Y`*_g0a5N67oY zZheOC2YlTRCA%M~bsHFT8`^gpd376ycbg=2n-+AN)pVO9yDi4LEmym(j=HUZC>sWp zEg#BG67^UOWp99Tutzz1p`5}|&Pgbj0+eeF>Io9{bPV-u73Fq>dJgPyXXx?Z>+zKA zd7;+hWzgeo-{a%e;~U=Nm(=55&=XM86Nu~y8tZwv+VkqD=QR)=%zzHzLx)PD!_?5> z2IvTTG}H?n8IFb}p`!}W(KYDH7$iD&3>~+Mjz2=b0rn;^^d|E4zLo5Kr`DTf(3@=E zo8r}*8s3|h)SF(=n^Du7iR{f9>&;&6%{l6Q5A1_8^yTvPBl{}G`aZ1oeLU*>1njS5=&$1Iua@lptkz#+&|hocU+2|dAKu@P z)ZbXp--JJZAp2X!`de4~+m8C%ffxh>rh^aDDT(P)!ypYX-S!xi7p5m1gHFQq7GU~n zF#SjjW(+g1iWxk@3;_qQ3rnq=Bh|f$5ro8RWq1 z*udQC!2Hp`0&wthkzsI&Z*W<1a7Ar!)nIVVesJAua3g$hGih+EU~s!;a0fZKJ2tqt zI=Fu{_yst0z%cZcZ|G2R=$qQmk-^Zh{m_Zm&}sP4S<=w=f}!)8p$p{DkFlYjt3$tz zhAx3v03()wA4@2OB~r%%4Y9-ySdcfCBmzsCj3q0?lGkD>y0MhwSgJKF^)Z%)co@t$ zOv^t^CpAp3KFnY^%;+%8!tXF5;5{N3F(Q;aB3w8kQaf_Bd*s^q$n~`m(c_UD#G_)2qvHId5>lf# z)kh@_M=zxuMy0(+WgrWrA&l{}_hIHprOcDH*>cYN&L+L+$)*nQ%0ea7(z{NoR$#viGV8yJoo zI*c3P2RaesCduQbh2v(m!6fR617jB4)aKJ&magh-?STZiE z5O*0}i;L;T#g60R)^PF1xHrU835-*T{8MkGrrxPfB^gd7J4~f`o2~+j(vqjr3#T$_ zr!u>zvc{*f*QRogr`{7!!x^V@`KR-wrt{UO3k;_V*QUB{r;8(|OOmHc3#ZF!r^~yi zE5@fktWAGBp8iBUQ^`0}#XnOmHS<}0rp9ol)?ud3d!{~OrXhKzvCy=*aHhF?re%Dl zb#12Yc&43r7Qr~%!9NRrThysOi!_|=c9=zZ&-O&jqLXKP2h5raXZyQnG2^oXYqNvL zvqQvl*n4LEjB_JWbEE2WV}^6%4s#RUb5;CvlgV>ag>%!jb2Hs@v*VV-adUIWbC(Om z^NWo0OZ@Z8Qu7&mb1R1PYYy}4-t!v~^P9)BtI6}*wevgO^Sk5oduvvCh4cHw3kQq~ zU-=gfr55&n&VMspICfY#@m@HMuT99u9KI7rI+Y6 zmKcnd7$;1r9G94(OOTW$mK&rJlD`HG5;y!#g1XgZptXxVOtw^2F z-gI1%fv(7=JQns@k*ix#K&>c3m*h2OX(v`h4#=ZqD(bVA_(^0g=tOK3l2KEk(|yxZ9-eMlKSi41w`m+3Q<5 zM$7L$y0|kr)4934m)_3T*cL8wiG(_moJAUOZ5O0$mlnBly1BqPVWe7wvbgY%zeucb zKxvo9(I2;~rFTAC*;a@G{BuB7LJFxH_?ew9;ms6_4#?#(mpFA3}~zT8{Z z*k3hrL*Lq8^V#2k?zcVL-z?hSuG_Dr+TWSj-&^0`KiU5R`U03QPF?$yNJjcErmg^h z26Tf+|96-=2H6jVcT=xE?)z`ehYZ5i-aB=B(@eFva^vdIIXdHg)6q}=D|L5KMBT@< z#?nWV_i9&H7J-ZF&wTl=zsN&@4k<(vZx&X+au<5eGPz_x;Ab)z z-pO*doS??!@@LV)=iNcZtSip(QB@bP55z7!(%;KVO?k-33wJNRF1-Ga2HzIegFl%l z$HxrSs24@F2PsS;ID0sFeSvu5eMT@yWD_3FTVsqOOc(5nhDpPJgQstkFD!*EF$9k zOvC=Q&dkR18`0;~N9y$oSW)Yak z*jRT+-rZ|HKwbUPaul7s*Lt?Tv#IhKU2?Z1)2(Ys^nH_REP?|X;AELfXhfZPta0rt zlEL`tSM_tp3Xe9jXJK>WqxMCA5;E!fGxuEkhLN0P-$Ti6D3+@z zq3AF#c?2;O2H7f%jC~KNI2@X)E7)S3q&2S}j~N*M%o{XOC8xS39*)4|UCd=9z+R>^ zd3Kr+?zl9nWSZ}b5*#T~N*hf+JDu~};INzsx_OzQL&e*Gh{>z035MZF{1sWT8`7L> zHw7|dsyjVL1UZobP9MLoXZc#0EWn$QdQ>t9F4-l1ocNm$q{ADZhlMZ7999W#1W8X- zh3|adZ#mt+_@W#^9UyO8vq;Lo&7~?!9{Kr|e!l}r+>x}-6S;w21;QZ7-L6>FwS&dT z8^6w0AM+_LVAWJqSWbj!I~SummzBTO@`N{AYWoW5fO%^wNoHF0L#gsSp^&rZY^|h( zY=9!+Nk*azG+F{@DAwv0 z8QBe$&quN8+jO3^zH#T14Q0cEiCAiYOt#@f?6O~>bioAFu43F&G=u_s*NKSpO-N)y z^+6>ioy6YYWeE%YXmuP{$kPOiPKKUxf0M6?bm1BzPyi9rDi_Uz@B)F{i0&*hUXYTr zHp!g^MKznptVuTWlB>S5&bTl#S@+01S6S9Ak1&v)k_|YkG9I{}OHCxH$>EF*y$h5u z09+)Dh9>sjlOOf9c%)1R1qUBXeM`ql8VJg0#NDW|VZ2_Y34R+XuTZ0gw~lsAJd}Ck z)UZM&0w9vLR=JCa6%i=|uv1OcU&hI=deW-I(usIh-eEkDunizXIEN+ zFE~dlHHp1`Mjl1#C{YEWj)}kB}tsI%(FLVJhNe#&fgPFNhG?S%R_n%y%v9a z>{g_5W#d_#(S?=`&`v3g$vff6E0Q1Zjb%*CL7=wz1CeRNB5&3$cU#>zVna`;@yw;2 z@f!oj=ILxa;;#$Y$Jm+@X5tKo{7?&O=fiIZmylu_2_aG^v6La#ACBH*P|4}gYM<2% z^e0QpVmgslKc8@^hMUKxR*0l_DpR^*yO`5-9ZoQ3eMu@>U42loaL*ESfSl=*`eNqo z@%kGvAF4A5*4`S#QC|{gfhRLIX0wFsz7NXRRVq~u-yBGyuzkd0qL&DjwiAD5>ZMm> zv{j}~zo;}69q;vYH+Dm(e}fAr7Kx|inw8(kp>x)|9hgM69`bJSSIDX^$`ZIdYL=Hu zj`uDn&Z}rctFWU~1aiqV*17K!v*ZY)QXYRr7_()nj~=Mex*n)7R|ZV<#QQv!oTe7S z9P{t%FX+gm>@j)e6^$X(sB|ksCG1(aaho33+&qlc=~x0sXAIK61Jn5KUd+>Gpm^-S z!Mv*-6=&29kL$LmJ>E#Z5;1vF0AnULEtCgcPu3(`T!h^$L41f?PIa#d)2F@&2psFD zJAOMZ=z-B+#4~jX5xLoiDW4oNH%$m2ZSH$1XkHRueRzRrqK(#CKJ^_0Fst~%qv2sV zB|%W8T_R=Tl{Pb0qM;@$l0bJ5lrp|)iiP_R;UgC$VoW(EO; ztl%8~CcL9AIVZuXA>Q}{)%TC;ZSlE@78>j5fXUyz<3~@+=URNu-)eC2v?|S;Yw34#tGnPD*>)9jx$Qk%uyyLK7Mv^<|*Pvm}SY6^!DaJu` z*lLr5(~>fL4T>+hM1Hy#2YtEgouf`i`2I6FQ-l^6N8D6--yy`gbD>;jl$IjQ-m1PNJhuO-xjH@Rajqh zL-Y^14<^~%Z+do%QJ(m|{vi`4X%cp~+OS84ij59X2nJ*zG|2?0M8Q;BV!(9f$nq!YFn|cD4e++{69E__qaC;YXXYDP&tHx(FLOCb zDvwvZ)BAP)tfr;9rzI!`wGi*1lys?bnE&<&4ZtbVvAnrI6i zj5~csFGO?TO|%YrwRi8%x!09CrYDb9lyD?c>A{ z5{n6D+41wHsAc&g`v$<0Kq%*a>}&%`{P(m3mh`uB=}D&PDgNnc>FF7b=~+|hIp5RaEE#!n z83m>pMgAEj=^15>85L6*AHQc*vSe1vW!9Kx*7;{Pq-QoYX0}Xawtde;uw-@0Wg$(o zQ2try^sK(dEId;;_&p2Dl071qJ!YCc;h&95&z^3~o}J2`|DL_blCyj%m$Pb`v+kd> znVz%Vn6o>Tv;RHkfaU$6-1{Tb_b2}E&(hzYH@^Qd_5RoQ_W)Kno|yoe!9f9V(hN9x z6P$7yPJIpsv*yyt=Q5b(G6m#9GICj)a@nVIInQ&sSo3(~^Z3m21OoDeGV(;4@~%zi ziJs?)vF1z2=S!O9O9$l3;+eXpe8uVf+voX8tOY9a1!`sm8UY1b83j5`1-jD(dglfD ztc4Hd3k}Q)jRFcyG78O_3N5A!t1~lC`{AzP!e)ye^=; zA)~yhsk~*nyzRUk!CKKNUx75MKm}BwGb;L;DlpR(gXa}k)(<1{AI8i+Oay$uWqg=! z`Y=2FVgCHXBJ0Ox`H!n+AJ+puZf1PkZu+=8{c->N;{ofZL-|ifW}i+1KAmNJI&b>) zWBSvt^G^V_N+ZUge z*lJYpuT17O8i6%hnKe4iHM%o3dKWeNY_$&+Y7NY5jRI>;GHcD6Yb|DKtuAV9*y`*Q z>g>(y90Ti|GwWQN>z>Zkxn0z`v(|s8)9b~;x8H!*c#s|G$xrhrUW*oWj1CsH)hQ==3F$w*_!ecnhMODiUONT zGMmbpn<{3SK3+6cvNcyLG}o9n*9A5=WHvW7H@D0*w_P+N*jhRjT9D=~sK6FvXj+wVk1h(QbTc?{_XJ=aHFIpEb+1i#B+E&fm)&tu%GuyVC+jeK# z_AlBF*xC;j+KF5+4%U_q_Sp{3A01rmoji)2d={MoL7hTbogyur*Je9Ke{_nmcS$IA zNm_JC2X)D2b;-AMDb9A?{?Vnxj#N=Zs#zd4f{?krO82U4HC`{8eZAJ+FTJHXnv6qQfoF&>%D{3mtvgf{vX<$NxYlu=l=I>`k)hO$q8v%j(T&>CM8M z4}bK++57So`wA@jih}w|viize`YLAoKK|&dWbdz5?60xt|7||Z>Thc4Z<+0H`_YeJ z$8;)UkQNwJ5MGv!>1)AY{+JK3>;of;17j8g6F~#Gtbys4f!W!C`5yy|?1RgSgR2&U z>p_Efrf$1saCdg_xA~BL=umO!$YSUuXy`0!=)7g<$L!FrA432REa5HeZ}Z_xENM2D zycJ71ho%0B1#=A3-5SQb4VhjJL$ZfiTZh@_hB^P34|#5l;LV2uFGqy3M?_jjuFZ{z z{u~keV?LC;v>cUwIVzhyD&IP)I5&Fx=cp3Ln98j&wSPAs&W-8)9Mk_}J~Xf#H~Rm9 z`Eag(&fR+I8PDA10nHIU71GMn_K%RrN;c1$CH?^TCn2IFxbu^%1>X-jnE%}m5nDKZ zws7`g0RSMtD$yMMTp;&c*uTDrFNYi*2$JS-on|i(QUi$KB$w`swA5F~t(MOBK$n0e zwqKm|`%CB9z)Qp&Z#*Z5+wzxtM9UwRuiob1-(UW6i*WhY%I@&8G&MkWURZu`nP7h9 z3&*l@4yU5q3O*U~!F^RlZ;6oeckDP*f9-cKt&Vr0MD#~;Ktl;YP%AN+3KeR&DMV#(cYHufHnqyN#o^DW^5OHglOmq>@e z^XP=l*nLihVv^Ulx4wi>^2Gml=0lUa@gHxq?J)1uir=l#WApOe`B-)1FZp3Pf$(4Q z!(?t@lpf2wx4Unvb`^Vf-MFt}e?e*__F^CGO?-sl<%h5bdqabJOz9qV)ccVkS7TN7 zqY`&E+aMd%(L00t@+bQV;yZ_Wdx|Vzz`u@nN%#}&I0*fhjs}>f;cliNB#=XD(STh{ zXB8(*O7E%6>yClkbe;V(8|G4tX4kGR=zS;jdk|Y)*qn*H37 z(Eza&*SY^Y9gP#6S1L@6is^olkry5ri*v*>`9%Lu8RkFPk`H1KMV@SZ=V7Zjrk4@L$1mo-)+QPr>ssno+E1ezPZuKe5a?y;9~ad@^%gZIsF2RrCI zpv6J)@}1t_8RkDa8g!}a0ax133l$UIb><|JF7?l+0FcmPFp$Z45r4e%n|>tZr2>BC zZzxIiN>;Mln)Fh*DuV4D)T}&uCHs zfLtdTF9N&hN0Cb(Mkzs!%%(c|DzuhlVC1rf*U|K{OXk3aBU0^zJVI%-Djw24<8(j7 zNRB1yYe3CV!W&s$6D}Lse&=fXs)@jZa0N!GF3cNQrJMJHsMbsWalD(xjfLeuAdWCp zMJzfNWch4w6~mz-WiIc^OkCs{l}86v1y{v}Oa9s2E7yDAb2Rgz2_HcsFMB1b@79EdNKg1bTJ9VKwE+e&c3Q`92>1+S&j6csKFn3;uXl z{-y25$f%}$(CdsgbV5Bk{hEtnVJqF#A0(PowT}Grw^dAm|SXKbQkCOL8KC89{4g+3(3R! z6{e+|N0*953dnzr(vQvibG-XAh|OcFM|X<`e2uZ8%!l-7p(OAZDren%*2%xJCGN5L z>>K!O$@JHFKTJO7DZ1zO`PVnWlm%QQ+GrKl!vvUa0S|L8T0{OYF(I~qPf)v8$L#QJ z8m2%%rngrw;P4%svQS7227#opKQrt zDuS{|EK(bvE%}y))-94q?Zr6Ce@n;47D*Os54f6r%fMlZr0aVJ+ycI3E>afD_Gk}! zW_-)q)Gd~u>>c!J`j&kVTdcUDJrwX~5Lk*vvUWp2G z-=Ay=IIcuZP-i&W>?jX1P@*BzHyj^ul+Q&~s->wj@;2k>KRTNK<8(CNX5uX4{GoGa zB)5KfII^BEU;bU_b!2I4{Qbs4`fHfVerzhD<*bL`kP<+R%^!O0XbUH~=S->;#`#Lm zi<~IVNrh+;@Byy*j_+Bgy1D!i5w`0N&wvZv!}HN_pRPO7mfPR})^UHm&O4Y{SDGBw z3I8I3Oc%>1RBf!200=>5;Ip0A2CTS_UnJtNfevflz;Vp{;)le&;MX-g(``(WWxNSM z*{91hmy6v355xB;Kq0@(@!g2UE^T+C6 zgV@2tl;$66Vs9ETPI})mW`3;84mA$?4t~qI__3ix(}a!GJIZ7Gxv6!(X(V;md}LUJ_}QV zgyRRXY9`?te&JeazX!3pxNtrEAQlqwP&UHAB*Mrq!Xz!itRcbz7h!c4VFQ8M$wKW- zppJe}=QOBm1N7;i()d4X#nd|U*TZ5OW%-*M1mU>!#KBg@zUiQ_qpCK4B(XyDCHi+kS?H|rO@SQ0ZQ3tsVyo%V}2?u^=o@eefom%d&B z{zY|}_$|R-eZ4H{4|B{6*+pa)=>Oe7^!*rMpt4*DcxEVH>z#;YUrp)%VDGKpqW;_M z(P3tYff-u5yB$(mN;(EaS{ekTyOfmf28kg>2>}sMx?6`(xffAOzI|})vAm}01NXmb@C6YtXtw-w%PhF)_)T2v&CDHU%fWwhZjnhv_IPVbU$<-i z%a++?paL{|KOBC3u+kMlz^L7Z0jiB&Q?K@E-<^ET%;2>s`lSrc6RA~KN3z+vqfm3?J# zNCp4QLKG7QsK&5m87+iy{JmxVhi~FPfvWUB1ET-2W!_BF6#GX&^xr`BPSir#Ob=U$@L<83s=O%-8!1sD7<1%X0eqva;eITjqZP)xDaQ z?N@uX{{vKOFV++8e7o34Q4zS@%rJDl+{*cD%lz%~3o=^ZYNssQ^=h~B#edH?F}Uj# zygGg|9YX!&bjt9xOx8y*ya%9&O8<4XsWJBi3o{`V0H@yfZ+>d~hR9t3+~stB9)2k* zyFLZ2hAMoUDgs3Jb>7e;Z(-rB0w`xofoE=jKs-4SDph%UiMkMgdjyzwsGH_!opGm^ z=~vnzk%RYPB>ZaiT1oXT%{424L#Y^g|FoO90se&5e+}w~QKv80g-EdB5TT=6ifL}@ zLeQ2tB-#GVj0_VYXgMab7qd*P)^MO&XfH?81(CTkFiIcF8lIoi0o+-8*2dM(rMwn{ z7k8A1c|~NLb3y^qAdG}uiT8|4N&Kq*?(eBN>Gx-9hG^P`s)r@$K`j9_Oat^_QpI~F zdUuojab#}764cC()33u6X-Oa>dS7v=)kXdXsD5t9`%&$nRb?_W^112#j~e&HDzh)@ zGw6b${jS&7+mzTSE3D(vo~pm`Zl*t?#Ok+ab|t~ZT{@7L9@@< z*&cNE!HEH*c3CsI!};3*HPixQE)_G+L4Ysfy`#h43@HVz`y0~d0UzzfgpgHyn_Vo9A{ z@#)nYD<}SaguYLs$rJimp&JiU#QF4}gj!k5jgIbXBN1!X4;EVePrhQz&t9)|hJx-s zqXh^^z8?qwi%<*dnb&%cBL=ZN_6`$Ki&TtfsPUkwCKDh{|EEyvDMX7ZfdZ>=e0jXJ z`TGjARUV{GcUwfWS!!3{LmOs2mpj$%)pF)p?T%s;6d#AL}7nMs9)Q)m)z6eZ(In0!A`1-dP<5C z8J(xYpgYCy5n3vG!4yd>r$gygU-B60Gt{E- zo9k)CY$~GmzGJu^D1(p%&Hr#cKV~1j{&lwK_x;!TUqY>Hx0}nI7nn8Ne)IR6pNE6D z(buOlZs=bZUtXhcuFt0q zaovxN90LCV<;j~nu)C^E2VFx+GzvA}HEfM4Eu7I4TghdIblmtvB)t2JbC!)sShju* zA_WkiI8S#DRsFF*_j}-Qsx;L*H&f*S0O0^aOQ)LuKzYtxm;2!$Q92dz5A=YTwW&W* z+2+#qA+8COmHKV1Gqk7f*+n{?9Z&jDtKd)@>32aJ4@LY|esj)D#_rc%y&+58YtvzW zEYRhbj*>x821AH2x$p0&>?9>k?s8iPGhVA_e*~kd#Fh!yiV?8@@M?U#_;#gDq##4e z8eplhVFM!=r8eiSaZa-WE@xP8rX!Sd^H|4Qit~eLQ5$8!dcRE=1D3i+g){XVhB-J6Jz6YdpU$;9CQib5_!(?_2LePlE@)8Uj83mQ`7s1 zPY;?w_aMHS2R`0Lrb~!f)U><;HPO?WCRZy%nC>hV;sgicTeAbuB30T_bU?_* zfkX{2MV4kYWAr7gAG>9sy1kG`tq(6Yv*?p9I+q!(N88`vUK6vn*~(Ad!bN}rZ?b!n(T&AXPU0?cxzIRhH&K|bVzP>gdBI~?9aTH~g zJe^KwZy)S5eBG{J^K<#^Y}Ll67akY=nRk3{4(|D_Z$Dn7ywlcc)6+GR6X}bwp|;$i`TI~(_!QNHDze+VyjGh_@90R zbl~*#YxuJ+g~Sl>`l{XDH7Wh8AN51iHqhA6APL zT~Av`f_z=(8$1?1JZZi2edSH+lAUs!|MbTpc8Z0JRk=jhQ5>)-f&YxoLcl;?mZ>rm;aS7UF+@B*X5AvK-@Z zx2-DdTF0@1bLyOHkz?`%!tp%rKld>iRleCfM+RhEw}%RQHq(eIdjh;I!>DfJvp&tT-` zcpRZJe-?6$hGsehiJaEn6HQd=`Kut(sCF}SsOr5PX1Y0ldW1#jO&@4! z80uiZF;dVSI|o1X^p^Rw)=!rGZbIPUD-6n3m?cT;no~0Ks{%ady&8G_wnx0k6+`>!A+>( zgI_}!D1Her=_e5W3r9Lc=d1j>q+E^AQkxvp9pl2&iy%~}WY!C`ZC+8M@*G@@V%wh9 z@?;YF!WazdH(ZbR?rvUBVCiw#FskoUfCgcRD%NDfUtF7*>9)I(=(UA6F7k({(lWvR zvr#QiR~`AZnP#X!z3OC~)nDfHP?mZ-S0i(mJzEVLCOw4@LzDFZ`niV!V&K4~n$QG2c3543!B zVFKfx@^9jYPL%E!zk8udQ+aD3TKUB_vgVo@T@dEviR zd=MZH;Qha0ZM?tCCjgi z!(PS1AQ3EgWb~UC6Qz@n=W-^89sCFegU?wT$q8UEi6%G;?9m{$3>8<6hJB7g{f=0Ml z{<_Q8aQ8?ycd^(H`EPH3nZn(oprRc&WS1$W5>(KlT@wxzcH{^h*i%lDt3#ZInY3QW zS)8(3G2;weTJO&RX|8DW#@nS+zwd$YUjUos;&{N>2g(Xg zs11zr?LKzu?G-~7%9WjLoRH;-6CyqU{smFk)_A#j_RSP= zip$Tb(i|^0jWy?YHq#&8qsGM9=?&jXmmR2N&rs~SU^h`sqAtt0p9iszbNcFU^W3%P zpBjSym#EpQ-xV(;HeR!9>Dl{aJcsC+G|f_ zCx@x`4RYkZVk=Oz*G;&%qBqhVsKp9}3hn&dWnrB~8cAANKK6JqHCq}9;T6HEg+b9* znoG#G575%>DD6#!s4Ut`E?@pbX#$*0C#Kn!+Jc^amE8K1Z!)tsrc z%Mhhv!JnX&uU+?|uz7jjRv8wN0H85&YS!!Yiuv zZK0E8bmDoF%k2AdpFKhhZqb(LO1b+RoB-k-(LEf?Q0s@AAK|<2USfPwP~$#s&Yd1M<6_BmfR|Ll^vShapUJ#~Mmk#fr27`flXa(_ZGS&kvF5_K0VWr#VFt-{~>3HQxLNi47O zOi4U@4^^qFaEB^J2=lqJG)}rHt2BunXLvqEY2IPe0z;0l*=k8%Zkp<1Dmg>-0XKV= zuH!rQY?XjZ>r_ko-Hl|2G~)_glO~V~zv!!X+ZtM*W3&mf_Qg4M?lwgz!#Ju#`|kK* zA{ujUV`_xZ(~JdmC)#({TGp)vF&YgjCgau@7*Pb#=9s_Zem;q`jIJz=;@0}>fUR!e z%m2d{d+(rm_5WG}BcStLwwr;rYfg0Rau8lv0azyEES8KZpDghhvI?qt~W$oj6$nTr-Yys z^^13kgs`Bh0J#LI``=hzEydT-wL99S4oIZ7$VH`94 zdSf}?@80ns(2sMZ25i$Vi2E(?+0$caCk-1{J?+o-pQ_$AUHX+aZUKbW$b)<^QL8Vy zoj-Z5N`33nppSQx{U%~pX)u19z*$rSlgBDyGmOSxt%u3b%+b!A5;6qTfab#ac5CIL(2hu*i?nXhDSe;l-^GH&P3;IOmB}~~%9jlV3NaD}2rjpS} zu%*fKs7>ERD0ACoXjfpD%jp^X?_I04{zvV{ky%#flk`G(Lnp>Wg0&+Ep={(Nm|%k0 zLuk=-a5s<^1x68)v(?F07oKsgb@N~WC}JTxhiU|5{HG+fQa-i|oa~r8$YGI}T}+B^ zQ3GN7Fn9~Vwdf#NSAo(hzbl;ViQb7aWL~=$pKNaZIj1DY5P+(_C&99}@S|-)Z$B0t zFR18vI#hu%R2TF3OCRojVBx4lD3Qr0pcFWR6*dStu9NC|NW{-C`Q(>QF@i?sOE5M* zCJct}H^&VH5Mk;$8^dvjW;x+ODI|_H$+(COptsbbz!+OPD?pICIyi@t=>d$q3XD%{ z)mzdk$yOpl*WJ6`s0z;)Az*?=B|IW{(M=Pk$fQiF93x-)U=W-P_*fVaDQF!Dc_9bZ zex5~`L?s&70f)kU@}$D{Wj%Z59x%PV!{OetKk{`9k22pZkVg*`1!=ioE)Ykus>J3`mdGtfk?9H-B(9fpKJl;)}wiuyG?)ic-92o=}N0I8w^ALQ?G7gFopFy z8(qdXtMb?b0Xyh}1`HqnQbDn)_Oy0#FNWF6F!fmFJ;&|Gt~^;@8>kkqz1kt?r6!jx zb#$rW`^E1*V;t|lSyS_AG-z7P5!SW7|FH)SLVr_!{j>9XZ|f9VDv59Xc-vCL-d>Wv z_2+rf5;Y}J$Bp07X~OHrXNS_xuhnE;7zG{PazI*i)NB+yfqu?r3V~fdGIsV|%bfL| zrpIV64KJH4Bbb(($0a{1?a%~Taw|XAnDiJ}i-&*Pw2Bc4zq9-lS}>#Rg=I}q;39~_ zy?JlQSy3lZ^=Y$K0D;<2X)=nxX(mPAV2)UXl_R&z4JEQWpF}p&q?f@jWWSZ`Kq^w2 zrJ7`9t)LxcTx<_|pR-+{FJ!~^TrFvaJ>Oy-YdggE9_N;^KL^c5UhwA~4rE3#7p<`^ z@a0ZHA7ERXy=vNaGd1z^?6?>{|DBx>M2Gjz{p^x=v8?WW==ZU5&u!z#M5>1-pdzhp z62wAF{))+W+p2pu?*)d_yeb`}DPs+kX}BmYs3BAvqpueNKCUkB5`3(9;9P~v8Nw3z zm;u!&{7tUI|D-@-F~jDA-^7I{NhuS~{e*_SMWKWSSQ4J2nHF9giyrEMy31Sno~r|q zzH7F}io0FKD!k=pJ;Rw(m90l2HP;*3qMaGe4T4K=tyA4p2~S4gH`i0&M+Kve@?K-V zGdO$+sL-#B%Mzm7(8(re6Qs)wB~al6KNwSsZQs;2m(n=bQy#20xlR`6@N;PpL1*Z> zD($%&Zb%9cz0rna*@E07rc%U>klb=rg&Mk1_mC*KB-&F&zOU~qjxKp`*f+Uv;^D6A zf!=wKg>_jKMLCn44}NTQs!nN5ka4$^j|y}ruz84+mAxrb!)6-3t+AVE6&2j+z`OcN z2An8SpESuW(!QhqV>0%8cM$cr%F!X7J@0}9>FYz*kAe>urs76O0}40A8rn$5qM6fV z{mLq{%dYHw1n63$`pQB`L{Kd!Jajtumtx;1?Wb!0nmS!hkO=IOPv*Bc8BBejo@|$& zz~H{KxF9DY+!RZw<|!5PVIGAeGFt*`LPQ9NImD-fEh$@PqXf(w;sN@s4qv1Y%?md{CE0Qcv zZ(WMkN@YF+5q4n)yhJkEm_CLyLgj>#TX(ILgbTYBF`lbbN$#L`-aCYTNUK{)*U$LD zs&r{2dUW65%EMXp_-gt(u}m}rtP>nbZ|v_Gx!r^vtpSJ8Oov7s`p=+S9J@RWonn;Xy2{CxH|tcPA{@f$6eQNp zHpVl(spQ;>@^_4mCr(tQX?tx3Y}DmXUTMj$!tOw^VkR>CLC2 z#SayU{BjZ)os2dVKNqVa!z*d`9A564*~Am=te51HPviAV#G8(>0DSCBB}O8-US9by zXH)e^(^v^bPDV&AQ?m&y%17vVyXx56rD(88sLgcAlNTdb`KD3##K(HrdM^?MY>%F& z9%nuX(c)JyA&KxgO|3XEN|C_1BydGLhvVqKT~cHnMY>e_)R2o(eM}=I*s1*5SoxsF zQ_C+RSni{3V&#B@Ll7#YuM2j|irTGPxFlOUqjGIDVc8_@!`3TbGC@RyRAKzPuN-SQ zN#%H1i}eq4%RgzPuIE|>+g2Xu-FfT26H0SzH8r|hN_XnegGaaaG0yU3N5{A>lCYvs z-)g6G`x%b#G$5SzBAoHeXh1NjKA|Bo9EY__K=~=n+&6XcPG60OdqX=G(4V(xTr06~ zU00(F03fPTcMwcmE~3*8OmpFs5pSUhUz2@_s5Q;J@9b>;HA4X^Lj@#ok^|8!BJrHy z{aU2tJS$A~`WbV%OFVr1XG)pq5xobzV*bYoEC&V}tdhcckkakute4vx}V?mg!Id^inz44Wo z0?ib%!ndY)A$?ecU%2M#v$$haEw+$GLcTy-j&4qkhnB?{x3&fIXuozPT9xvs|KU5S!?#WN&``A2DSCcPgp!zD?YDiH@CgC=8<+V z)qWQ4SN>((t&pj8dW&Mz|MBAY@F}+WZyhO(c&>+0gEYZ{XT~8b`-gEFZ1d*=qCKI? z9`QkoO2if?&rS=zCO=&faj8$>Ta+bJfAqh9C;ZuL(V*#$(IAIi+*9weWb0k6FW9${<|PiA zHeXMPvp$^{{Akj#k+>}qIC-b%)ZkD-RM%}_`aFpd*($R)x+Xc9`N6wBS>`KnF6h(l z+Ic4d{Ws5|pS&;p{A`()_Z?mbZqSDucc~N}W(@umKCQjzuqZf@j}D$u0s30R6zs=- zY4sON4onVu`orRN(AMn`-){~#r*F|MUvWEAcjq3SmSz8%*I)=3eX9Er8+%G84hOKwCfpI2?!xCQv~^I@Squh{2plplluP zlqHcm9H{IMyfp*@%fn)cK|G;hI9L&!mO!}{T(>kr1S%ZF{7nA>q@YS%CkM2%^aXX} zJ0L*%G2kkAIQvDoNEZ$$?3oEyIGZ>u6$Qk&-tQBGb>J|~m?)k@kY>8V4doL{N{rIr z>6OaUTNC&YbQ$sVA&vk7k`@YqTY?fOqV6JbliYCxSHbK(Q5;4f9RO^X7{_ZzC!QFD zIo&2Eg>W(9I`o0ryKo+o#`3s>69BkYi1nkB{4#YcMKszQ# z5-Q$Z99Gu_ZBY6KBMU zOQ;~MsPKmk(S~FYofu`;)$lKG=mZV}`Y8j?ai6yP8G$dJQcmE@z=00|v2JjX?y@Q)7UdtWaz5M<;0F zqzk+x1Tqqb1R{Wf?jpguT?uSEgebX4b*flN1g_8_hjUB@=T7<*6OAbA4KQ1ByEoH&SpA1_0oGS`#}q`OL(wiEu) zI(36nxD6L3~Qw>IHTV7tB~9PBau4wEA%R5L*raJFZLcoz0sIl6tGMHujTAm@q>+`9^n=FTn6U=fV`_Xj{rc)I)bIS(3Rb%YmOvNC=ljwS_XAr zgDaNyNYAe_hj%#VBNNO=ocOFOXCGBqg74~`UCDL4>T1H&?${Sv$TMvHakYx*+`&m7g!D_ z%{;{JqHa6@VCzt)@gQ(2>LBIrIlgt_^2Wq$qpy*UAXZ)cw9rOvsX8w_0?sbrr6iJ& z=0*J*I=?zF1KdAV4rp6PK!6vt_9CT0lI%fuJvJ7s&JtsW63Zc1X#v5>1|$So5@`a! zEL0%%n1rr8A`b|xjw@cqD8AVUmkk-;7V{1`QG-)oV^~m8BPIl#rMi&P#CQCXDgfPJ1?qO>!Ao$B)^9x5YpOo zjEl|S?){v&+0L?lZ!mlHWwU;FGZ@|Nkk=K)c^Gy|1=@E4iZSIXSmt<|;u;b(=xaoX zwE$I+X^wFaQDP7uapYFXn#4sUvv9-T>qMy(&C~eiJW~PkFv{!dzU@a=t`}~zL-&3c3Qs2da zFU^4gX3as=Rq;llA^EQUXtNL_=O^9?L$OfT1&JZg;ARB=aH_LQeDJ{e;M3%>l-%IK zFwGEruEC_m>>Rq0zNXrYb2OW1)o!BS^Z@j{_qy;)5;HRqv~ZOBbKa z^M(co551!sYZ(|74jz5oHZamQ+B;SdsoC4P-%hDZ2&El|_KvN58F|v?N?s4^Cf0lH z^)H9h3Ct@;A25g00v<*sVx!?^qYICRX!eK&Y(AXQ{e3u{a72VtfSQ_sadCz;jtwv8 zhWr{M-iE6KYEXY2P79EHq#K_&MWHS&{|X{ zoB_nJDpHqTA`*gnCW0Tg{^fA`A*(1oWxXe^UzbmpxL-N^la@-eaJKBLFerZeBtgcs z`L9ow^q;L|KBs^^Be8J(R`Hz1@!aU1=*xK3X3GwRVxHnY(_d4z1v7!a!$@XKgy?-8 z1wCdyKdcHB_VasXmG4`CGCP<_OvBecB)SQO*rB*EiAF1{vw7`vRH($PUo(L_`~^M=tG=03L2OP}yJjvNG!EhfvW{_L*@|IRfHg*~Gj) zk4_;&q}oHI zc?ibjQ3@Tx(jS8O4nbM&Y)i0()n7!MRCC9x5j?Bl++RcotFi2GFo`VRRnUfn_S&8k z5T3NA-!Vt?VfuUH=g4;N!G|j&HS_9O>(+(qeA=wJG#lR`gpS5I)uC}+azxN?t8!gL zR+b=@G@@)cevuqV7qDQJ0&1YbVMY+#q-2GC1G=G?mwdl?Jy_m(@?^{ONc;QrTHpES z(z>Xx!6CGd_DP>b~36h#7)eiBt16SOm|t>4h6je<@QYXcqM0}}V; z1s4&&LOvudqCPAxl(Jv{TA3`n_rrq#lT_Usy51kkcGuN!S@8LWz&r6nA}GUxECM8! zGIz~XRBDNDm9m$kwmeC^h6zNWw)l3by?>3CJuyG{&d9tQ@o@KH((bv|`eh>9P3U?z z)0{x*z2?;w_y&={$;zjL_4|YjN_hLSAEMFj-&WhBFP?@wGtITs?y-q~e+$phLw|j6 z8}v!*#DURR>@GR-057dBkdbf(jNVFETAYbkU#&ky*T}5Y6LOhvLp_&t7&ji9cQ>4-(+j{oHhH$$=v)ub%v+8*qMj??%ipP5?@ypD=EtTLv#riM4edyi;M*R1-i(^mq%r(v_TJ}U zxqtM&v&su_sq%v%)LO3|DYr>qW$K3c0Gk%hLt5V1PM_Y{thTLW^UdJ^nGhU$lT(Mx zgr5}Bm4g#KuZRgAE#=KVwwY%_590{`^qO5TJ>98krQU63SVl35mbp}vaiJ0%H2Sr=82jz&f1(!nJ4eU3N6q#8IwBb zhX?Pq@Psva``(-EOU=95)V?^IG49e3~AheR;BD@!7jiABS6$f5KQ;OaM;1H@oY8vkA1if!~*-2%r zRfpv$lF~ZYGj=2>H2j=9$GYjI^LXyE;w1D95WZ7nuqanSeMkKgcjVD~tARw__qxYL zcOO`T32lK6+l+!Hu@$6ufH359sm%>LQ;pJq?FF5W_bAH|zts8bsNZUQQidDD;=7A2 z+Y_|1-N$d~$_8A1NzypZAR5Uazrsz@y|LOP-5vvovvcpJ3Y8lG-ZoBZppWb}yzf?c zE`^s&Ht+~oN!3XOo=>J$x}XRIsGfQq3?&*M8r~?1Zck5rkU~>qBkr)=L>sHK*XqD* z^X>Fu*FEL9PMYI@cW9?u_bBk>{BRXNX=EDK`e~L-PWx3@2b1AzTQtLF)xu{g$gKjW zsj%;6G8w$I7L6gvpbC8YN&GM*t?~Wguezr^Z}nCVgti`Zo4>Lv-I-J7H+G`2r)`YZ zld_D~@aWBU$YJ&-laqHFKuE`nmZoKvY|+v@PTw|#n%_J%F>8X{Y4hVf&r6bwY-)P- zc+f2}n6LhFFF(hA&p9JE1n6!*7X>9@-qRQXgp>l-{OB zu$_5X;)q_nJd*&T3CjYDjyCVyCh+Yur&Y4TzYp$OgA9o#(po`WHh6A*Mn#Q>A@zZ_ zLL?N3CN#lm3 zmmE~LdClu%&6HSa5~-N(cPEEi2-{FhiK}p_lPC*$4iT89;?ZPaQ48*?5{T>e2v1I= zD2y@F23+Lb0e2;{UXM@$;i?2UM=9BD#U%2x12H)zgiKWO0ROH8eeW_bmiyo>5JFY6 zNhm^(t?+SheLNrmiL1t?sG^D-xf-6x>^!w#@rxcsgvCH6%y@*hh8X;UUy zwR3PFu%VH-tZ4yi(l~nToWYoV7G6sE3T4D)rhUx)k`_Tn?E^i+wr(ep=MVXF7QIBKYOOM}}7qHyv6<Ov&{8vf@Z#h*;?G~_amFPgr|n@rS2Gt3mw&j zKdVNY;*VyHt!7qQ=$n~9E)1znNE_saRp>&Qrc0cY1j?RhOYBG*G-UKPSaE-qYu3j% zfOkX4p>m|au7qR+Yu@^$Gvc~9UIt0Q{tGVxiWp61nhg`Utn4KVPfLQUo->{ydNA?J zKu-S+#>tp4h!MsJF&M76V$E5Ni8&!p=2PDJ2seeFkHCgm=C~J8{F)xal)4=2y zKg^}-FHg60Ka6z@3MZt==z1qruR_ScmmSd7{a)rMnlqdjwAT=Ud7=*2jG-O&H*N4d?{vjYCMDrcRtY{d-{gB8NiiKnPO@+?E zJ)ELN9wO*2&$$z3_Cpej?(z_G5{n?PvJDvJ9v+P5OCa^#l>3Md?a$HDv=8L#!MUUo z3(X@ycvl>R~RMsb`6hA{Az}(%~(p4e75m^(t{RO zLLs=D`u>EORU?w(RBo_ltJ^#Xy4Kz63lxUS+if>=)5pq%F;yN#QD65m4(;IFaY(@l z;iF-1I}@#YppYD(lszHh;3l~Y*jGbG(96$;$;e)(kM7vMDlu2rm1!A9N7Y&5+08zC z1&h?Qy-*ltq2IM}e6qCN^b_*lK&|0%ahht81fH%#ef_BOzPAV^d+N#DzL?VT~!~4i38|NG8%MoT`!%(&c^`CefazLrbyB%I(xzOSY-U zmOpeVjlfv=K!Qgk>xvG9h*=gP2`{XBMe(q5lWC)P{pN61QlcB?Mi2ll*`HQS>q4i|NG9*@XJOeE~i?yK$Gyuw1hCE1~9YZXSLDd}B22C+>XY zWPQT(s$K6MnFLp1kIl(ol6OH-S3!%qGmK#CYv8OivDd~H7KmaRZTW$#4lG$IeJspK zgRvw!B{N#~J7WnN%c#SGJD9ufEz6!u`Zc%HYY_<(JVEj74pJ)brPIF{q|4wA@%a3P ztY2sos_n1Jt6%h^rj?+Gfi?8?O|4Ab{7MS|%B9g8$azE#8&Z`%Iy3P-2EwQE1s#GF@DAACvNE*cJ z9Yx4f1TaMr62jHxri#rjj&-Z9N1v&U>+zdAUIHCc9i6mq(7vj#sf@4|ZK9cp8y_5k zdB_zhm1MmhH{zbar7D|!_OS27hbd~nhx7PyEBP3pEw|J7m196`;Lyr50NpZC!nKY4Mj3|%|AGR%@P@km? z8yLdLRk!KU@Tjb^(z?d9^3@v}8Q(A<;SPZ3La>KRA)!U-p%Uk{@ZgJM+(P^)y6cyo zfanY-mFXA|v0RvQaCYFe@{CQCcuRRqMEjFq9$h|iv>sn#_*h;97qzuIPwgQ-JS@tQ z3VxL<8mBaTgHm1N9(d)>D&)k+o%PTZj^YF`jA_c}4%^2-3SM z_f*|7?p4twUEogC2n5FmivQg9W%A~$pL-*3H(5k;G3KhUz?>>!t9zwm)zy*eG3lxu z`kWpq&wQ!{M^c|T@{NsN5zVc$wQW|H+m2bV5cSQ+eykq*`M!cxD3elhw32P`<^I_9 zEVj%h#BUY+u?2es9A~`)wIYwL3`))XS~ab&iB4Y}{!AW+qUkI~C?b*VZCufoUD88% zJWICtDtw~Zn(B>an(K}6YdivlJJEOWZr>6Q&(`=*WTf53l=fqiz!stEm;jjZ?I}44Hp(0{56yyoGhke%Ql*}4itC@W@idx^nYhRD#Yks z3oY&WJ{oH{JL*cDbasuWw>NY@R|l7Ua4i9mNhS8hL`XON8?O{@l_(mWlCKW--b-REI6(oW15mX;*{J@&|DU*BJ#BP!f z1-6+I)s*_A+&{T-70$UTmDSt+o#u_?EY>zs#>%};fZ%@In}}}$g4o6L@% zxgljjLa}Xlo5Y^n4_Lix?Qg7+6K!x2b^B9~&*KlZ(5M|vqr0~<0ax{c)>W$*p+n+q zfqOk&qMqw)okDf14D=)2PUrJm6S5})+~8h$Q*F8U1}WPxMxm=iOc+yNwdO4Ba-S<7rj{l@%T{;Hp&e0!q&)rYk|XN zyz^JiH{LkwlxHW3{ln7mYO}KT@jjPwv#Pn#3@Xh+M7pKzgAU{emM6nycCUu%AKog- zH+3KV5u@>T$iSD)q|;~?{hWMy=$2t;!4gimLI^auo^5FPULvC{Vm4EBj^BLVCFMH! z{ZiQn_>}?bhvYvCtPxFG~`j6NjQ2a~B<)umr>O+YSE9@*RJEh%fh6`t(-_446AbYPzH!E@NA@e_c7yTG`38ATwaZBoTqrH5bg50RA6K z6|_jdEnk8P0%ajh4-td$&$m)^4^U|8n~(Aun$FV0y&Rxa!nXt&9{nyFc3`pjYu z+}O0L6+y6f5CAYvYG@v^_-)O119&EZM)+wN8xj>8tB@28s)VW(JQCX|P<|BqZj|dG zj&7t4_02k5*)~wEQ5!&TNcx(NE5hM|P&n!tH8R>41+pZLKXr!E-%l2=*g$X$uhtW0 zPutY2K9hZllLp<~qOx7#TH{pTG`?XbGsU(>f!Y0A=aIJ5cmN~9`0?4OdH-Am3WzR+ zom54fv_X|YC7$i!hC>dH8=kF#ueEW-ma}=>*ROcAx{pEU~4cAsraSSk)FGrCR|R!`{ApuP3{!|vFCMbq}abA_?!cIyIxoDJ<4iWWO9yzqy$A~a0E5F;?f zs?O$Q*aNw!g*8-Sl1Q1bSU({o@WOcBe`D~*Y1rQWZG+>Rf!(bx=eJqIirU|taYQsI z4k!zHc%;7q0I#W-uvD`)lZa(yOsCH9Lw)VTnoFaf+nLg-I#*Lb0&`3Uy2k5v4u%LH zhYA$RFnoK&wX=@1y|KLi%ig0-^~mDsQG1tL{l!>&rN`^1-*;DetB}qP=37^?J5-b- zx7?0czBrGGM%T^aeE4y=eDfuj!u7$;##NNYhWTTbJI8#FcDl{trVEZzF5E8&cNm(T z$H!0zB_e%#{}8j&Gr^Ee9Y|TIrP-?e$ug!n_Ypyk!q1QHII6rJnC-I$<(aJ zJwJQpM_by>$Jff=5HAw-&@l;6GL!R@``-yx89el}j^8wWH~gZB$o^r%=#$#@BdN}Z z&iKsiuD8C9x6qxV(KGMCJS%b@-xO8fTObdt-A=R-eybIKi3w;xQmPdm1}oIm=y z;-0(W`VAcVic+3esQNe-d8$yJHBNu`P4@Hu^4XNg-$dK*JZe{y@v+nwZ*Lt37Sf9< z={&SOWxFi>JduHK+-;v2zw-h9#H8j6xQAsu+vz~1UwF&;z4Iflk6x;N{`NZ6aUlT- zkM6Q1`Nw=5D$Wb=S@TbPlnH&RL1>^dz;zkMbgt!gYT0~xMQ;5g;KF7lpulZ0{(JKC zFP~rF5+oM|W|m!5f4swT2-FZ^G_Xl1=tjE)`KPU+AGN_PS?U;7rAox_~2+Rv|2x_IAz zxvFCHuRZ@+`z7Gzd2l;AiPrGAU+X$T{5kuM=S_@q~jO(-zl~=RclV1l-p43TS>}JNUzv(FWsNTYeanAE+*4aPzvu|FXpWSPE>f>QcA?H4d z8xw*C5|eYmyY4=f-@m9ji8Pmz>B48Y<|I;vlF?{M%R3yhcBd?kP(gEvv|ps~7?RS7 zjWLeqfOp?Ta(mHy$d~l--Q7KT!JwSL^I*QwQU9euhyRiF+5TLk!_<49Qo~FJnOu`* zr^FvTRPD*ugK7Ma&-msWoisc@;R}@+wUEz!kTD{zeVB~H10>c)FQd7`w=6wJ$QE)OONnq1zDxLCgZ##eB{`Bfk(l2lak)h%c9z7!c3 z74DV8T z2ggdw5hLM<_2cw9HH z2s-F#g?14XqOO(5lvB^xrebcwYTDL|++q4IyvwaSFH6)3(rf6paX*mC*(IOLqG-qEU@YlWF|pr2E3-k0W7iuvoZvb&KWcy0Sv?eq!L67 z|22d#5lfyOCz{17am$mphqiVUY^}&f8&p%_7Ad`4Q5rAONEf|yNM^=CL;%!DgDbJ9 zmPb%{`qhUC5Cta!1U>axqSCHmf2n0mRVTJF5o^=42oE8L)J_;cP!}E>0Za??N-ovB zRZXPbjFY$DiYTs9~hB1RDz5 z#}Yl|jJ8`a<@`<1;DqOVIM0g$eeTeXTdva4l&de}`Skub1GEd}69)|^94M?b|1rcs zLJnm_(=@dL5Q#oz1wYvtQ_RRs=+Xq_2lwOv@I-D8?}!rx>y@2UoTs3lz4n zdoTpw3{6xn@;xevIo#nF^Oz$t^0AM841hRdQ4FmV;}yj?VgOt?ML1fKQ8p|eBI(sd zJyJ3`f4n3nA4iXE+#(pm*al3xv5jE_LuQ*%q_<+2sBzUWDvZ zNGXvomO_l#AcGmuIA<7T3C(q~vz_j&+-9x`2S*&Ln|W|$M5vHZ7wPiUkwas{2-EicnC zRZ~wDYjMh|S3YvmliR?GC`ow>D7I0GsbnNqq2j2j;Iyui+9_U%v)9RPv5+VfR)**i z$U(NHtvwkgiF~Fwb9vQ{I!$S0*+<#cT236Y_^S~&xyB=U)^OnpUtCcnG`dz4MyZVv zYlB;~CvJwg+q&5zZ#%=>D)yD8bt-CqD_P-ES5hZT<_Zgo+`}rjm|eYLQ!NKs>8fS2 z=c%rytT!g?{qiPm{GM`?g4B`j7MUom6nTBP*O6?cA79YVbxyznA5@1Pl+eh4^R|+F z5XgQf+s%PZFyK67#lOoU$gM2lk5nkZemuxfI-aAIz=4J)|1XK+b_*p_++MM{S$wW| z!d$iM3mlUgKiK56RuOqe_D_SVwge?f746=gd7cD7}*J@ z`vMEt5Egd$iXAj41C#fVOHZ(@BPBGY60aAN+JbhuEvBP=|B2rjuO*W7um!_H5Zmn-2UvrX#|(Ws5r<@ImarIp;1d=G!Jt9};ju>?jk+e_N^FisG8{Gm zAgx?WLx6IxVO5M+=FTP>NUSt zm~RzrQk@&#$K5s7vR)TmY8{hXA0$l!lUqof#3}#om<24@pA)dM2oq$a1ZD8A2~<~y zxD|^LwGdmb76J=!*G|Nk76|HypyPamfe5>!M>^0@0~eILSPmhzW3zQ!ZoRR)iG}l5 zUoB&MpQUJT$&0+0V;8&3=?q%#GdqS*gUs1s;9=OY4Vu9WRs`V*{#b+!crm8MAj1~{ z0LLQwBM>fk0v#D~7z?7HwmN|A;|aN)j{m`BIMBcn2FXVv&=vrV$pp=yp&5#44qq9< zwZ*N@`48*O^Pq2PM?yEKUVhFgT4~~ea|#23{{#;5+x-J>i@CbLcx;&N=-tb|3id%Z zJ{Z(N$L&+=MBJ^DIlCJqJj@P2K3*;pzww5|>`pa^R;(yiFSc=xRrk&pPZpmyrulfD zOJ<;7ksq;u2hm{_=7G_U3K=BsJZ=UuR@OF5<5}2Y$Uz>M(Tf(qJ`7PO2JNVA?OV)M=?5a$pK3lt9a!n-N)0U1d)SvDv%L*LOkEeJLMWFkg+}+Ver5Uf`NQ zpo8jM--57A4EPFn5L|jt5DTn;l_lC)^n=P7hzGHUdmPY$GzjX{L0*fP5gKiU8nF=)dWaHM(an_>9Tp9&-Qg4p;^QI51om9y z<;VmIU0s-BN%Y+EiDHXv79klDA}JsoGGG(BTH-}l6f&Nqf}O!dd}SI;KXO6<$pt zWKFJ%9daU3qE}yi5@1Q=V1WitS|U46WN%4MJj!ELPL^dkPG-4cXD#JWE#PSJV^rGJ zOylAtbV4HI_zl735d`^ zt;GsOU_5yXHfa+#c@sV<|3x@O6f%_4IZ@OTisfYT)On=lSg03jngs$H$3bc4Jk^tD zLew~w(?mrkYl_uKq~L#zpnwq=Rzz5WjRk|%fP;Od7R5J#ratLrX@+HQ@|^b>+K&Yp))ASJHQn94fs!$q zldXl6Q5g+5L3C!>mUY>eg&94t0}}88D=2{wkVR)~!*I~dcy5I}DU?t&RC-3DKq(-ddLZv*4{#NUvMsh!6BT&F`NlF9K=bS_4$XzVH}@v9P7a;$d%m4 zmE6gt+{%@kf5ZX&lm(IsN46}fh0-QHLFqjmDtlIG;;|&7I#bad9hWBE(!odfJzeKT z-PBdx)n(n*(L>kq0@#V2+|`QNHB5f+$C9w!+ra4Cy^6BVUESSX-r3#!a9mk<#)NjG zF3Q9;IVmHY|EEKJCWlVxZh~mAx)bGD9`FLU=e^x(pg73s_RV07 z4lE9UpZFE2S)dO3sUQ2H&iln*5LDZn*5Cc*pR&ei|NS2?1Yjr7N_BWcSyYd^qD5?e zY@p(5uHKfthUUrUtD?@?%0iO{;-`Xeg9m~j$OUJBm7stgELo^vfv7+W8jxV%=nPUq z4JKGv=-)r^AcOp%mPrG{XheCSrU{{tWd;Lg&SuCuscja;LrH1ZChD(FZP;G!G*OdS zT<31_LLhkC~E9HXZ>kcpV=8-e{m3Ri$G}f8&f{4ft zYPf7D*ZOYCR<7+qv?8YZR)B~Jn!WG?rBYL`tGm#Xzu4GFXMWz zywYprnk@OcC;uk!MOl+(qAvZ4FY88c{wgp96BGl7Y;A&;{T}dXR z@ccG!{qm|8cW@ZT@mabHcW9O%q45>#@Bp*%63Z|OPjDQE@m*diR@Cm|GU@4#tm?XP z>jLi+2l5A#F6}bWA%m>^(vt==vIIky{zCFa`NQ(Wi$82#aa5*STxRJW@$x=yy=rn3 z(_0ODvZW}*nZApV#0bOYCv+5tYX(QvN?{)V#0#@=4Bzh`bFwU}&q;K`Stv_UvdxcYVQIu94B)nIrCaTGg+|1LNo(3Ji{4^|D~}M zM})R3=jO05J1+h9u`z$KIm?KQLaLBR2!-IMK%g_IG@X#(9Su|;#P~xZ*Z`L`9mz#O zhTR?goB+W*5Z#nu4L}D$SBSuo+%Aj7tOkdYdUH$&suqXu55KX>{xgmANS6YMkmSjp z^oclk?Zgw551TEhIF=e8jZW!cLcmq$>16Cr^b0HCosUzD(-Mw9L!2|8;i?$@3UA z>(qb@FsIW}g7tZs9gOCk1nlmZb(aFjKV-Jc07Gei>R7O=a8RfbcXc9%v!RYNOuH^+ z(}>e3#`Dbe-f%Wu!?mcu30=!U7{uz2Ai<=P9e4P(ePkFtqzT^iZFw{VbsMx;aBOgF zb15S7C18 zZ-fbe8{J44_u(CO*np7mAU*sClE@wVEw@1IjtgeQ4a~I&sep>(|F~Hk?t7!MV&iTD zn1JECMikW3hQ|F?#NKd%t5R|GDs9FEsvx z_To07ceoC(@$M=*0z3Ntt}i(vrTbcOq3dyD6R@W1E2k&0|GxLFeY2tab}@5sOsD!k z(JMfbeJFg%7$_{eW>gD-Ldj{*e z#9KSX2kV%FdB2l6ljpg>pL?_4`^QUHD9?x~r-hP}yh^Zi#1H(U_xU5QyjaW+*h~)3 z7qO76Cb;+VT5Nc@<2g8gHMaw@&ZmVx=z^G?g+F`(%ap*$n1wJj4B3;Z7|gaNgb1z&K2Q%3{1t24LyC#A22 zZUeK!-#WRwbjNr5+kXTl7^#<7!Xh9^NQ*b#a7Q{Q{{eKQLW7XN5d;f5Cq`^{{^!%f zi3VEGb7>^NzzLuLB*b_?o3y&8G};ryg|af5Gx5zAe9lumd3*;f*nsF)wNBr+eg6aK z$w^>rdK=e)7~mK+yX)&|VNb{0#nsCF5Tt90C=}_$JN8 zw}BT2zJiF6W5;fMWjr%$0ql{%GbRjXIAO7&L<|A@UDowgGBmnuk%8XO9dxwGfbphGREP8}t_Ed@s>4h!G<{vs2X$c15J&>Q0CwqlSoD2M) zUc-E4Pz&SaTS3Bh;=-~4ab6uHL+muYIBb~ssO0@sR*gBcqs=g7(!7aNr!Ss9feMAD zn*ULylxqu6zyS%Q4IvtM(@nT<&}pHTi~NbCg))3$X8e&kv2IiXkM2Q$mKms@om6LHG=NRIvB5ulxAqPw7!K4LC z4D06%7zX<%pErPbik@4<(J`cAf{BX99#PWFCHOuItw{Sy%MZ0Bm;BGrDLL)bQ>Zq5 z4Wdw|qQ_8rx=CeDsJ4kEGO1d9RlU?0ZPdLWg_KXU`tCDLKTI)g^3!37E!NoD(1~F% zQj3wroT#>urBFrJtCb~}9Fc^PA$K*>*GqxrPt#+`E!W(0uM#JgVxB7Im10Q!XHiBS z#Wg7M{CK$-5nzzTz;9w-kd*>TX-*1!b zSG92m4q4=p3Cyh=hnqw=$EqY|@7kAgttsDLE$!H2%l;)<=bd@B%2bSgwyNdKHtTF# znk3bjW+ZR^c&T`Mj#}!eBd(U-TOrkT(xv%r8mXMA4qNQ8of_IlY-1);=}MXQ`s=63 zj$7`q%}&WtnD0%RSGK!$yC|^d4qWh%>$dg1m^0pcUvT|43FN^ckG$N%yLdUWSU681b1mXT9~L zT9e&)*bPQ_8&er}_!e2I|E1O1A5m9ZagK*~{Pp9x7u?-KHyTx4cBL}CXj~%&e)a5` zEWZ2q9tS@GN-b7XDPaC~=dvGNj9dMy+5Z6Oz5z}! z0J)=zKX_r0sidP5QXv%S5EVVk)T(lu!CU$q*tY)#aDpUThz61M8+qKRHZy#XRy-7y z8hs;oINFr{4u>)I`EP|1Y@yD$Q3j~=BMdPxK?#n)mZ&tS7VXdjn5be8C79p@H!#yb zY&aFYKm|RcD`VLbn3)A?tAT4<9|T!=#IW__3r`3@I?kY`r^w?sLAwe&Xkdo`^g;qJuC@FFA$G`96IDQ+i!d=oU|dXuV!(ng z54G$bt@{klc5+{6-UnMtlhNNXA)P*wr1~ha5a`!G~pn- zGK3OeI_ys6`43sbQyQOj$W{yqMj1LNshwmhb{tC&qXN^H^sq-DZOBx3{(~aGsKXB0 zQU@a>qL)q~j|I~h$a#8QHL3r zhekGk_IOTWo|sN zMK67$q8Ux)Vv?F%qh{0uT{?mxJx_W{ z&3YEAq(v(J)Wi`h?t&W}0UC;(cCB_69-h@pX;nXIeC>Kyyf%$5(2N3KcWZQNHwJ0e z|4Mq*y%tcx4wkTmHSA%9PVrG{BJ0qI+7T_O9*pnYxjg%t+OaVi%5FD|l~wJ~%l39D z5WPmsQ8L%7l`*fYoo;J4#gE*!%(B7#Z5Q+J(dI_AiPfF&W5pWW@s9VXpKWRg67Qw@ zHh8f%9q(_48*O=m_Nk?v>Vr>QO8f?myiJHYTyrhjjhivWK~A8Ad)wOpH*dXTd~XYe zoaI-EwX&COIX0FWTz1ww}48?{4pi<9qMwJ|MIP?Pg??{Njk-pTh-yNxe6| zXB>pmF0d#sLw%4cZ9cY9!rt%DkX`7BuX@oL&v^m8@G6@cBY~2N5u=7=hdtc3q2FD3 z;cuqyQh)fsaW4DWBrz&~fdLDCV2Ln>0$W=dfMjzhn!9%{< zl`o~@AfZKFw_;ZK)20ZKjO?sUjaXOQ{TFo!aT0E+WKQU=JX$kD7y>+$jw}N~Bg~ z585FNy22pnVG&Tk5@ae0{^Y=VAqbqX4tSs$kRhr1>7U^5B23CE*03X9PWb4K)KX{Y zknievuqq;9JB;c%aB8M7Vz6qU1@b_-;Ex`1YAe1fL9UBR*6B>+4C}F4+ zsgsBbvC=RjR<5j0uP9Ql2C?PvHqQLu2>tF*Dx4}39OJIeAg}fcXjti;vM-`uQLmn$ zvp(xjxab7LtQH(X9&8{IfWS8xqy-mZj5_JIYLO%I&wyfYAS@0J|D&#ra1iZu&<;~k zDtwR%3WK(6%MUvu#DHM>YHPk`YAd$O1uS7J0tF)oED_4l1>~`qXy`C3Q7n`Sxaw~c z|KYo=!uv#`=V(y;8V-f%@D!tQ8>a#mXh0(3BEHa(ulhj`IDr^6Aqbuzs@MRZ7D2ua z(L+Ma2EL)6p20=>N(oBfAe11G{NxP|EEV2K8P9B^0*snEqQoo+#rO~G;PC&7F90*o zr5F-63PPt0V+4CFtaL1|O27$n1DRCd36#>u^r9f5zy!$z$MEYxhKYwn@D(SdtMt(y zVBxcp%*aS`Mo4ceOs^-Atsqx#{4i(g%I)>iZXt_uXWXv#|3oYDAkWyo&*}V+*@6fm zudxm{Z!g!&(x4^qWDpeP66kEO^{6ffy=Dg)Gm~1)Dcl8K%E#$`((;0mChYR;6mv6G z4cL@t*c|2=KanUx(IA0@`~+}?P*F9H=-PrIWwb4Krg8WH5S$Wo0CjUU*=^;(FZEc@ z8ZQ$YGcz}jbK>5R8k9XzjLbKZn z)AA0J=8AJW8B~t6Zh26p>jV?{2CqOXQz#P>H}5k;|6`}^0F6pIQ}8BlG>K0RPtz1J z6h#H7?@k5q)(kFZP((*@-0D-tq|-(v=kZttFnJUdDewO(v_DJqKaVIMIw&gq0W7Zq z^aP|E1XN($VHM;hD%z6G5+yQab26KAJ^j-@w{&m}!>OoGkpc}WI;lI}kt%dVPr}Oj zz_2zpr8|fKPoJXqs)8p$^hMM2I$tbJ^G=RnVZ^K`^vLoL>wvGEBB^>IJgA~kLxT1S zBp={lN@ZjEI$|qmsxNdzAO!_g2lUX)lrBm1LdkPY1;;Sj4I`>grYJQelB!mrqQAy5 z4C-oUb}uCSRH;Y|Hlbx9gR}C~6Y-uiHIJxK|Lu`2@QMUZsSZ@2214l@c+mtfqzDs% z27U??b4UYefCI;32w;E&qNqw>-~_S|B9hBoInW2o%sQs<$#CqLXdsmQBo2xU2bS@c za%q=hpqB)K8;0OuJ4lvzOr#=k0xNJ^NmU>g0lUPi6WeewS=C496Z4)_O{0||Y^V!Y zffm4!sF;Bjzz|nE!b&*77gm8>#iF;QkO~!I%reT5970Psf+BdxW~VSv2rLldz%gbh zW-F^5t)vsU;Re!xsR9fb80(-CYM~m+%*uco;((Zv))%_={eVFrgs=(6h*LRYW2ZuG zd9K@F6jAN&S&g%W+H_7Dj2>vVtAK#M|A<2gf^{FWWD#fU8*pQ`YDzqUP;g%;LTErn zDvF7oK)P~c5r81~jH{_=s4wt~ha9V-Y^WLgFc&9PAcobNIuRsd(j|pJH=_&+_QVy)Uxa;Z~tMd zx`U=LqBz2eX5En^(1dW|f+M6sBKr3a-{1)L^l>)}A*PEs5<;mKOv-+P_I}2D=cE?T zF(mP~FSM^b0&*DABT38DV{a`;|5YwphpQIo2nU)Ws=`n#Zr3ejpcyh~AgZJVIN=$1 zfvK`GhfESBL8K&E1SRWFwthG*UI@Pe3PFhBEG?lMVD=ey;TskVf*Xs5s;eNJpc#n4 zOTG6_IMO3QG9Bvx2Y3iEW~4@T5<)pP_}-R3XLLN>SB~r<3~<1E7j^|^QjxYwx5DgO z1uGF!$p*l|iOY3Mwh}ocLQnXFcbN#Dv~ner3UcLv25w*}cM1(o&=RQkIvDJFWkg|N zz+WwxP9i~DsdC3Kxsus{iT*at!Za%0vQT@pAPv+)<<>-FH*my*8Ad`??8(|jC(Znq zn5Kfjf_X+trB->h&*Ehx|HzjkjPxm{*$vZIIGa>F+jmi2%a$HCPt7GAh5($MqM}Ad zPja;*923)al$Irvgl{mHV|CJYxm@ZwN}#4i)%n#T)Qu07KiL=W@Jt(t6n0os*#4n; zitX#-l7vaogct9g3p#9g(>K|eOjUPD4O2!T+T#d%q76DZU5-F)c|>s;L7`JYhZLk~ zOh1_wZY>j~p_QfcEk0@Ua$J{2=b5I}ZCRNVJ3IR3KANZ3%cfcPV_#LCho@CmnyAak zrGb*86}5BBA^Duzwdld9DYJD?`gPf-8E)Yf-uJ54s~dcJqctw5y#^b~;TC>@tXBQ`?4{6t~HuMrTVZl`?Eniv_+e*>H19PaI!}`wN-nySsSVgJ8osVwPky@XwuI^lS@TN@fK!3{`2 z0x*CDeBctG`@5k#xoZ3g*c+`w(84eomZB)}IS z{KG{Xz#qE8|5LjgBw)iAAO%=M!_~V2INTptyB*x16F}U#;~TBxIkwFky;A@W6g}Jw|37mi=v^W|@;>>yAD4rYNvV6=JfJ4`T$5|u6 z(R&wE0~T0-1=!$bv`7ftyat{iN#vZ)arn-STg2;Pf$ zJO}t2DHuuz>OcmbVI;WLiSEQ5w&aN#-5t_02lAj9&U^$$fCTJ-7MK7CNTSkV0R>j! z8Peek|8_vYNI+dx012cV(|7yEIoq&xd$mab#TP&l=s^wCfF5?i+hHIzW~dKV#LffY zEajjQ7@-;Dfd!DD6eNKY1WX0Mpc2ji%GaC+N?{b9L5qQc88U3nlf4tLff~MG&V_*m zj(`y`AqM8$+W)&%m0Plzo3+9GzCoaZ)F20_qytcVzmWnOKJt#(Jk}+I#slCP(1FoO zf}wC=B%&$`?19az-5zQH*rE3X5MJ4t-PxU-MV^5fC_&jRp0*`i&vV?XXBz`heB6VE z9{ha6Qveu{0xL(tN>(8N*ntU301Q?E8q(nnRNx4re9eLV1V+FFoJL%ZCE9MAz`fxv+& zV#UIfLg1wU3>!LR7oY?M%R>Lq8>ry5S$>DlB6iVSS9SR9cfNl!C(by~=A|HO$F z6Cw@MG2sz_oe~#}GzGyaOa+)qaacHs<}j(!rA(VTeF`9Nlrm zh>@g9m9my9)ykJzUc`tYW5x`c+^cl!K7}SQ1s;P52l_1->s8;Uuuvd!C@)47ST#nd zu)(ZX4Hztb^a(J}ladpUpXJ4d+O4$0gU_j{rJ1V02K@wN8R6{?!V5U!I9( znpf3_BYt3sm7H?SZ5gDAYF6bA9Ajz+MgV5s322~!N*NrL#lcyYk8_gP<&c6p<<2_W zbiqOgDUhJXHo!c}X{TS}$dy-bf|%u^TngzaR1>a~)Ss!o3hRZ0_GYMY$dx7Mh^N{a zYp=fkN+X*&I+7ov%4x|cs=zMAz4rbpY^cSOD(|k^-V1QRkGkujvAJT~sK5#@tY*04 zippcW$oiWu!xmqhA;HwbyGg0K*7+~SB99#9Z*3AwF0TE8TrtTmzigYvhk~f+$LPY0 zbI!auJZ{7tuY9x4LJxgoa2&pSYlx<@Omx#uJCyHzDEnM9!pb@e4I?>luz&;|t>fP&@EGFsrJo35Y z2^*2y)e9I%k$z&`chh|M+Nqk67eaq+N6>c*sBt?~^3Y(rL)0+gM6 z9&()a>WIIZ*DDRePEcTib?pveF`Qt63YIO!P7-W51dUBVR24}Jt26|Q7I4B<4q1c? z2)I28QO<5t!yD#;1}nv-z5r$hn;DDgxMT(FAUnWi>W zi3AD(Xc%lT#efJ*V3fL7sDc6SfvkH~69^)h6zJg_^eAEx)vzKwG;RpiYlsW~7XWFH zzzzZUL=Y(B1a?3#0Pes<4O&x!{RMy(XON9+7}AR{|D2!%5(z;6pcsGvc7hpouz_c^ zc)vxIU^6RdqYX2UNredN87EyZDD2RMY{ny)gi^cmoQdu?P}Mf)jXz zMoEys2sCgcCqc-;OKy^r8tC8>iBQ4)`Zq_o6owPfsDlg`(n&pnfe~>a!TtUyAHs1= zb@K{Yr^b{6dhDW+|M(Y$%id*8H6YVfQILM!!3asp9Q*!YQZxmr^b|oB8KH3${dpj zQP43-#33~MJRmNi*A6C#QGYD?r#s#8h7rVb{{%L0BMCQAmPDX{mUX=27PA=uOui7H z?;#x83fjChJySuU_y!UeK!YNR$uAvP$Ap;TLQcfgH0gwCPM~KGB~&i}>G*;Z=;+Zm z;D9AA$WkeI;YR<#5E1jZCp2JLiW&8(Ac0r+MvV*jDj8vEn+h{ zn4K<&P^3;dp4L2nyoCzLcYePSj*bD9X>LvcVm~h`|wh5`-NDKpr*t0VBMaPIlD5 zl#+nM|0c1?}8VHw9;X@$ZOh8{ZudAK^{R6k{~s#gSJY^DMM;lRVT1VhAn2wGYUi=Xhf5( z8m^1);JQP(mbX*TSjTz|;yJ$ZIxDkZ+ z<-m_pZkWl2WjU3B*r1+TuGCyH@zk(vbPQHU{-r$NqaWM!j0~lo>^bMCg!=*4e#9K`Pin8cD(P6 z6`lc^)}_rhzW=?HO8>gn_6B&mo9*p-8+_sP8TYLDgHwh}99(Zjbm}4wt#0_WuScD@ zw**dFXgjML-DsxCPyP>Ybll_4LU^JT4y=-+eC7;YIm_|o@M>L~;4ybYD^TtRM*RHb zjIjC5W7TbAh5X&U>IMiL|BwJTnEdGp0Kw3c0QDOj9asW4cjjW%jaDSU0S*8`GZ_JO zs7w7HRChotq(co%l;Iie=z@f|UNYaDjMVP_I;_0$4-AaG2+efE*vHO@X3_xa8(`>e zY$9AdPQ@Lp$ZT|5a2UJ~F6TD`XE5p*;?W z2(Tn^!}npu_bOSJf>>vIhi7_8zzBzT0~-JWZukYCG1d%t1X3bZO(tLi z9c3Qc*ANuQ{|Qythv?7(c_0t-L`^3!MeM)?B>)C+^lu>Nd9Q*2oS1g>00L2$b`FpP zRre2RR|5`!0nxM+G$@R5WD)pOieGSjyHy8Wn1g%=5^plWbBuAtLFgzm;hKuhH1BqZRivw5k0Z!O0p9aY;b$f@PBMD z50jM?7FillAY6esK-BOCIzT zj_?#7NdidV2#%lzd5{iafP+B*8Qqj%QDp^!6uhI?vNSFVxh6!*3ij;O0a0AWPD(0~}P2nDdwiJJ&W6(ezOcI-0H-e9dZ3Rap z-B1J$Pyq?B0eqzfX6Ocd^^#-Yn%vhgDoJm@`73L=P-ka`l8|~~(F)HAooV8TT?mO_ zH*@Fb297Xu-uazq;(eh>o+y`kD5sw5c_zInG2_{EGIx9`N1v&}g+IfR%L01MR7CUH z|DWusZx-j9C3v6*mvbD0mi*YD+Gd{sik}lYZU6awSB9a|7M`U>ogE5pUFV)6I&2lX zp?qedulAo2%AqUDq6zA3B?_aV)}8@cqcN&?Xm*w~nxl92pZoZtKU!uVnxer*q#8zy zIGUtIijS2uqfF{6=5P+jU<tDdvC+=dcdP(5K5# z4tRPFwonb+AP%-r4ul#kPs%`i=cAnB4z^&awr~#q5TDJo4wEVl<{+q=I%Qr8|Dp^U zE4DxmYbp;XCl1x%4yk9S;&7=3;t5FLs&pfxJ|m=bDk#!WskU&dCRYv1U<=~l4<^^E zZOUV}fjz?tHHNyNVA^!dnx*l{s-pS}>42-hunxiCWlv!mH7FI>6Q=Z8p>v9)fYJ`O zAPu%K4<|>eq?!%VN}%gd4doCF+n_y!!2&Emeear9+8U&v8Ys(f4ih`A&9tWF5UQ-Y z4kkwr!5|K^FbvqhV;-pkwv=O^uo|IZPacS2>;bQF^hIn$s74C0fby&pYjW=J3uEf5 z|N0B=U~cMktr4gO%KF^8X;Iq++u6(5nZLke(@C{CJi29^Xt8k7scwI420})tJ?C}Y)!2&VR zw_6LVN9u2B;tqYsSfr~MK1eE~2wArC3O1)|ls=1mC|B$8|YeYq~ z4!ZCS|40ouis26Wr2=J;4x&UEInW8)qy*WMnE|%2(hIBfn!2icsXAK@ z%jZ{-yA8uo486M)%0?a{oTMKLxJ{fU^T55Ts-H8r4Yz;<#qa_DpsmE_ndH{3BG)VP zU=F|VvA-}2$EU`{unr$U5AoW@^uehE)p`5-DA0hm!w{-?jKJGa471P%w=fE7ysm+) zK(PA7fLtR*_6v#o$hXi2!H@&Ra0#6-41R3MvQn&Z6{5?c{|m1W46~pMyMPSEPz+Mg z278cesca!!+se!G47)H4pnMBCpu4%I$-Mj&e_O(x@(im047Jc`tES8tqM^;)DRx=S z8MDeH=&jpqL;Bmb;2bz4Y{=w1HZ6+H=uBzd%x`kc&ZBdq;~dYEGsxmKow8cbbMvt0 zywB}ymOg{J$^6d%}IU%oX{h4&HnrupT7(&cW?t7kN^WP z013bWH{b{az0cyT(Y*o(BLLDM4FL%7(Ev~YBLD{$eH-RX&z%B>0{{R6pa3Bt12QlJ z69Ccx-~dBl)4Z|74vi)^(9#1y03N^qA&>zwfCC}W|I;9?bxFM@7;T{`ohn7Z(>#3u z27mwz-~lpl0W+`z7*Ny$Py{9o&hre`odO3A@YV`|00>|JQ(e_Numf-H05vT#?J#|X zHm^(#B`lrQKCJ+c-PbaZ0W)v`5b)NWNEZQ=c_-oyo1`0(2c(#+*iHfmE#1=!zyOb} z00f`_9xwxJJp+wB0C4c8Owq1zJaEa0}_ZEodl#_JCJHE>Pj@!N095YLmuc46E_%B>lV(4E2oZ!O)< z{Q(Ma+A=`ZGl17w%>htBvL?%snd^X@z=1RX{{tj2Kf^T{`PU5Y@eGG}10}Er>=EAQ zln6CY8AF^Hz>5Ny;JFbQ0O2i|6#fHuXGK`lNy30#N_x-wIND7T(=GknsSVN!@Bl$w z*>6n&*Lt)`o0Su27*RzNJdOy1Xk4051CT*?$dSQw*AU(91O}v39u9@4M1{`-kFB6Z zWzY@EbxR((RgnOUjt~ahm`NzXNzn5=P9WDW6VW++C(K~lJRRN9J=Ra{-T-g_vLl<> zI=2%jh}3g=ozP7?V3nBV8SKI34#BsEC0QtRh)S-A&tnIS6$a_lTmIANhsZ*s5e@vb zO+f$#N`M1tuI66N#G-B>Za(Kdt?G6D|J03L+euNm3}IPH2#ZlvfqTdh(tyKc)R!<2 z>Pub&Wsn*)PzK?R4)P^Wyl4mVpuz6(+q|#{M=%6zV2g;r?7uXKhh--05Cbsa0$?R& zD81gC^3ii{0OxK11klq2VAgy6<`qC_zZ<+G;7fcC;C>F|gGiEh+Xq$U0+mpJzs?Yc zNCflX4*e4cD8%gWxE#$ekqvRTgD?bn@CHth2nTfS4AD&`_1oJX1U(P~Xddn<-fk;S zB<PlEuzy9CulmI#@LhitoUI`5uCl1k^8=WR z>rs@IsSq2SL6&=41NbZWAs~-v5CoUW5UEfDF+k|~S8k?!4 z={<1Y3@h#c0$>l-p3m-~U-p;%>iE+Xa&)aRRUqNtEKGvuP4Nun-~Qs^qzj#oJ=*nY z0tXfF)}3Df-M`nP4-fzb{|ul2C%{332^A(>$102qU<)Nqq*&47MT{9WZsgd}<42I2 zN{J*nvJ@ORj`TQUB&kxRO`}ScYURr&FJi=yF=GbJCrF?{CH4?FKp=qv1`HrTpa6k^ zfdYGc6lS7DAE8;bZspq5E7wh7#g0vRhh@u`VQ1F7nR92)pI_xtoHt~k(WFe7IxRZj zMo?^X1rH`%*s#KqiBG0nDTz_qnKW(s!l{#|PiTh;4@nSJ>0YQt6ZDvuS@UVssa1Pb zJ63GUl#I~AlqtEbWw@qScXb0IY5@WSevdG?+xT(h$q_0piBcs?moP=zrtJ1|T;)OM z1-NsALjr*ZINZ3C|K0p~^m1LhE_*imbn2DgVoskmTC``_p4I2y-+xBtOmeQV$7ow? zGWo94?LP(^bnwBAT3Zh`_tKh24?+$*^svDIMH&z~$PT=1KI}eJaYYtaTaPS9 z%#!WB+AvHow-$5MaYyw`Qfx%WEHp7g6c^<2NFZp zLRE{CF%r{^FHj>3b@b7}$_%T$8B1K$&PE~CbknR3eJ)4?FD)|3XG-0VQ&dmm(@Z~w z6jebUn_**y{|6X&pn)CIs3p@>d)>;@m7t7FN=$WJg@^_kU?5p$la0a>U!(0x(oHDk z63}8>w1e4Yn{D=jG@_Aq+@OGU1XQ(NrE*0!lI@n+4AMv=iVK(xmt1^1T6Iz<(WLW4 zSXbObSq3s_H(4P1U^a><>=id(h?n$n;t{V^m(31WAs7gHgh)06dU)ve0!z|JmgI<2 zrifZ=-@CBRQB`z;0S0CW;F}DRWrLoXnYH7ah8u`t*R0%Or4wFN#&Y835Jh)bm;?5( z83sORpc8tUA(#j@o@n;xTIy}Ul4dSS2MQNfp(q_DoKPCieXH%a(ol(w*~2|PU;u~! z=;_2;|8MR1haQs~xYr_BSYUw-itJHBh_~M~7s{6bUbjOj7}ULIP*ZQ;w!2bD0wfT6 zhtNBMARP_8SCL)(XlPPIqzM)@Z~pgvKkxJI zckey3_m@4hzhx$q$xN=SIo9!8*L9wmW!aas2WCG+pQv6fxpYqwJeOR3%`j2hPxC>( zrZ0K$-g8r40#s*?u^Y;Mn_9sa&E5h^VCT&!G=4Z*@!MOh@-tdM_B5iDPx^GC2INcj zAJ;86g?npqK5p0*jEh@lIlGBdL}6lHerZCMrCJRL9$5Wz3I0P%OZZw9!Ph;Zs{L=leL%K9N7EfJr1-qVqj8NkiB?9CT#@~01n6(LLlyF5Th)QD4^?5C{ zb;2~Fd~8qdefy9;;<`vbf#aY`BmV-`zoZRdZHbnwM;lW{8wA7IXtruf*x6xO>>|`T41|X|Wm%Ba8)3WA1X_q(F)Q z@Cf2%(HPRA7_!;!C#aert)wzrZiSAGdKFJBo4Q%SRvoFouV+(N3K5w8xkXL ztFkFkjk#~sh;hF~rSb^)6Fq{=U2l?j9k8*Sx-_yv{QVRn$pSWVrL)SF=?3nn?oS7CTDHps* z4Q5Fmzw58S!DmYWq5aWz9p+IUyP@Y6%D4?#TK3qDE)qGD^;z4Oz6)IF$Abf5aZ&W1 zCeJAcvIn<``Rzb~Cd?*0Y#c0mjJd2iUw|9vom_S0N}vaZ+oiA}Egz2Kvy1el-|eD? zEK+KN7^(gZbEW>kd4cXEuY+(q7|mI1rJ_cW)BQ%WOY^g8zW(qx+I#vLaYhfCUWwII zit){_nnc^)3+4+U47oa(zjuCoZt8MFLB&F_vu(%7R+P8A*FjZP3;n!S%;fKnUATvl zpQBFP{Q99B7#L`|^k@6lpU1-Ytjvlfzi(fo-N4n?X?QX`IT0(g#rCtq^m$k44p9+_ z>DtVB^`w3$Ua$QbpixuL{P`*2%*~|-7k;F$SDi@q9L_?xzpv1Zg$deF0f3VNlExV& zdh0rH-3hiiJ>rx?5`_U0t^*cIShkzT%kZq|V&q$U*@rh*C3z@pb~g87-7OcnRlq~% zZ6ZO*-K#>q&25sm_pa4h`Jd#Wk=O%mE@v_}emZhE zB!K~G+C;LQUP?D^Ch?6@`_MR&eFiTo2+wl2h{JeE_fFtwA`AduB@>xl@B=+q4^wMW ztO!mL$Q|os?8d*gtgaUqT{$qF8rD_5V>XGk?ZVNw+|b-T=t?G{HEvIwXjy1Z0K5bbN7d;%|!~lyQU)f|0T8_dCnT z*`CaSaE1>xzvuYb8Xs35i<)HffmA8QXioeEcy1d*aCxsl*)4s4G7)B@Y%E*ckmP^4 zEB}Y3(JP2u$A0CdTePnNnw@%j7Yyzsjlyz44gu6y6&}j`H8s#Ta!Z)Gdp4@dCz`E= zFV{E@$8~=PgZS_+lJ~6hwanC(L~1)Ay8@&sx&V12X|cIxVl2Io>uo{G2Sl7MupL+w zRACR+AsaH%<33)LZtorc9i^W8X4jSDL2!qP?lffrdE#Dd;+w4e0o|Rg7czIBb9hdK z+=0MY**o<7c4qH{7DRsY%8Kpw>zY3tV*?fY;}{Z0(IL?r5`>M zxWzS!UXhDz>u|j-+eJhwpmO0u7wyLji8=$- z_NUAhT|I7wKO8zm91S&Zo;Y#7Q`@gw_=>3RaJT00%i2xDGgc?KthzM>jW!&VHR6}k;ZFj{8JFnv~&o*35aX4nu zsx#s)O)!mtYLkerD{!aKUg(!D6tlYc**G_(iBARO1|`8`sy7pD%)g?;mDTHW)5N_I zK5A&v-Hf%J>VcKQ0_RP%Nw8p}G=n%}#JEY=yve2GzHU3Ch%4woQZLQ}%uEF{lZZTI zyktzDsix@_iBwaMH!zTij~W0X5m97QW;B4H;(Z+BzOEz8vempZp`IV!+$9xbKW~=mYQC=k$q$72;0WTlI9nV@4BWE09A^{>>gSY()PaPWBfiz7!zaRgPfgqac4RL> zH3HS)3XE%q&|f%2tVVv}VSz?K4YH313k22;UVyaOquaKgTR#|1rP7!I(fr|U9jy-o zt-)~3_m&oVIN#%UIFbC8G}u9-5uYt|09=*m>TdedgT z?+EUoC&miAW!uD@sb=%9hVWFUIZ)#R5N`p>=%k~GG-fOpFLwKRwIi*Xz`KL-UoYovCiMRv0kA{- z$^E!M}q6V(ARbkLdt>G_V_UgbgW(5q=?2m7(Nku$Gwdj$|9GJhD z{$0tuhPC%;G}Y$qC&Za{TEU;<81$NzTt#Q-TjM5r&63;9uC(8kZu9B*Pqg`aPC zzOCDP%Ch&TPSQ1UV!I>sMnC+bq|5E$v$y#gLj2Al7%Mv%@d@{5f)y-I_nKskOix@viqqoXaZ=^J$WGOE%0t|LX25g9ojH72_a744xWE z31N0b0@`yTYF=bq=a!xYV%L30k@b|_0>rKn!ixth6M^bDXftG`)iV|C99%Rn?6Lqf zgT~?^*MB5-MnI7}%z-KX=aHY1xb5+CDd8Rp$`V-XxF;f-5{KET7VL-Iu$BqSPjdW0U}aB`Ouidrq~BKI_BYp4H6;N& z19;)$-k(ZFj1h@Hyn&a9;A#ka%&-oYhwnm65!Ze@m%2#EE0P|^Yd z08Rzce^w%>8N&Y8d$$PTgCU$uN?;hDdf`~mTsV|{-WDl0bX|2K<4X%0mB4cw=2<4` zqzU0wCJ6k{lJOrBDVP(uni)XFM}ELVe z{uj|k5Jbe{)8^Wd=e1NpzCIhcW?v{ z2Bzn=dZ!xUS!Np#*fXI@Sbg7SyHn=`Ma3Cf|HLGmU|E*;#04XWec2bR^cwE2OwkL>GOkvhQ-W$^&8q{ou%N>y~aSYXn#^Qs(&D%f8>6r!Jng zRbRO7lpYosuM`-62jRuBGbWq_;ve&Ndbt@#Ew|Pq2vel2R~`FiFGyH>bC5xPKdeoi zbl5EzB-dM1l?ohXs9es&h$rr@<#;jV`rZKB?^LFLNsMv`N-i3 zWlT~!7nhc9!1h~_xr4ej{SmMuF6rS}WEcoep$CC*y_nPn_W`dEZ~wx1aTY)$31Fek z#P!@phaqCq2MHxZVU-IZ`-t@iHrwJ4w+|=@Nf)&QK8CSId<**P{qyR_INbT;x0n8S zt&V)ZG+&v^^<^D!&I+SE-lm`DM6D#BBMya1E7BhNJL-@h?c8 zUAeq>XpLPJ!beW$UFGqxfv8>e8Y)Z9F2^U=Cogt&K|8$fcjbSsa34iTn15#HiBuBn z(BXK+w8mOYWfU(7UrKnp>T^-p2d0OJeV>NMj6wWj_5fg4cn$5l4xZ*hsA?oL1%jP8 zBE9P)V1(fEq~tcueWqrpy7yN}TNcyI{jzmtEfKupWCSE2rRmg#e@M* zwJ176hByKV7uKpXv`aN*mLXvXu)y4=2BAx?X^he% zd(E;5zqHN?3zg+(jB?ohhP0E8tG^RtU~FGEkb{0#!f)l%`UsyNsn<^*CNE6tdZ&5v z{2@AjAJ4c9knl>*w?e`;0$)@uWPE={Oj!IzTzW5wr(~ty%?<#8?IIg2T++BB^Ytz# z*1%HuCATVmKU>C`2!gm; z*Tdk|5T*Z&T`zun-*KB3ya3<;pDs=rB(E;8yVMg8cU-TUUJ!_PDmaazDD`Kj>as`; z-?d>dNrTti>h!T4=p$mB83Gfl5)51XenN{jhwBxGhVJ-tWV$PhAvzudAwbp&GHDQ>-o?4Ftby|tBQLxQb)g5w$!w}C zS)NEH(EEMmr_hEhH&&piQt2zd+yzx~?P58`DmYA0QshiS=_v_=ZbpgYvAgmr%Rhq6h~)3hYbn!t+-NXT4fhhhVQ( z$8gElF6RA~9J#od!;DCi2roPRVhOVT?9|8XY+<^15RIK01MJr9*eHe3`+M;I(&%AuZHVv&eK z41@WR!mGt6m(Kqxq~5U6-mILgMtY4Vmp)SC{LWTp6%g`%hv(H%_{ zw$aeI(r>}*-6CKWOLFI! zBYy|>8@laW1CH-)&-Hg7iJrW7@0gqa@_G|bLPKWuyoB_N3N!B814$;3WiHX#fwN`L zPTZOQz{#-DZ{@4<;8~%h(|Uiu<-L;`=L>zj_6&x+|86Bb6vQb#Kp5#E2@o_d7KEV? zgy%uI6QLQ3lVXFfx%SiO`L~8h7CXS7{c{0>CKgFm9=TjQaTOn0$^5*=oKLC~wB+|$ zqw}XVTmn?|Yu@72^iq>h*Tj8k!7k7ipN;n2eZX)=+OXci+_btOYddw7u|(ZALft^* z7ZrS-k~f;5-dOU!$4~aVroN^TwG2XyHHj#-zu1mfVMxMj>9>xaE6XUN6;@+q1Hr@& ztO(z9!s&aZ<7wPgfU>fOwvxSk!63PWy|-M&9+3#eXM_2s6D{NO?Q=SSLWmjGx=Abx zQ;6|>?3Qtjq}`op!kObZ{TaK)H}yn4R^>z>l;_^yaC}%ie-wuBcpDEZAagN1HY;|Q zmvPVmH&(Q*(7fSWa}yo!vLn7jLC$gdu_ukrhi;qopm27vx>NeFm$l}A%i*(FN|%JR z-No~^NoNH&50a+K z%6Yn05m71kexV~fc9;^Z$9f)iXXb3>z7<8O@&*^*YL8G9de*;;oG(P5u3cOWi;ics zmt)z>l3M)~Bk~+{_IOzY#ucLXIuC#B+xyBXPf)G+ab7+~oLyl@U-;k)=ij-*Y$If) zkhknC=dZ&8X|tU(D?xS-SSZceHN$7mt_b{`;5HlkVUDDChE*wW_oSbbmcM-DY2a`_ z7Gt)VOyih`@Mz?ALg-mmS7k++^eYXYG$1pbx(aP@2@M7bCDC`a-tRbsBlc54DYQid z3%UvxZTDP15ufI{U1NJ^;=#k$hO#EOGN}!Gx1^jsoYMs?bl>7#?%MjN=1uMaJ!9&* zuQiG4Z9iEUtKda>*YG-6P5Cd^XBe;B!KNBNYjN=D_Z*I;E9X+6S{OsXcWDKE#oCnXpmcN=mJdP zMTu0h)?Rz{Q`^s^!pUclKU+6N+T~hqTRvqj-br42G#_De)N3f5oY3TdS48CJ5FEdC znWjDW+d16_S}xq#+^hVOa81ePW65mC^|eLk^y^cG+zp9(TNmt+cM3lyo+~L{z0jOK zOB?dy?qt2a)l~X=zI`$>g_U*mWFUgso!eqiY6gc}D-zbIMxGy2)%?8jXv zhP+RIDog$_i0D~yD^RfJ^S~>k0&hDAe~*<@g+U(_!}(Jv4CM7e#hXE^WQXukaZKdb zK$jw!x@BGB)EFmd1{S2}pWs~K+(kc11*cvJ?^lo%j=L3;3{~d4wC4@BP=+d;-Rj70 z^xKch;SBYKeF8Um6?Cl_DVD%Z12hF-*6b9E!$$Q(LL!o|H#-BuyV3M;C!9|dF9XfN z#iz?p^hfEqP$2`J50h^_#?~eLC(BED48qRRtxWaSLw2(bepHN`JGWz2-*1RC(g(-} zrM>!$2c4$PaT4LN_8w)*xp)qvZkxjVAeSUTWdlL0q+v})#UyjF`}nBw(t$rY^52CS zS`3dCKQRf5{;4-qfkH$lMAktfzdLc41$$Jt;x4W9*yw@`s6D}*Lt;G?B&RX+0pS(? zjU-D?KznJO^7-GM9`AcaewNyOo%Pbo6#fe;I{_{i74zLP79A)==_q#wRK)T>!y?th zpFr`cI|;mM@C-FWlTag#dx^uCUTLJh-MYBupcr-4r~5mrNb$0wAMq;}geEHGBP#6s zRd&hAY*D`gERnv*mKrOy(_=~+u4@M5R{%w*zHobIKunsw=77HX0Iyi56lq0J?2{qe z$d6qd6I4%#9%jcg z`T?M3K;2jR1z?t^!pcrnr5du}nCVTwrFM*SA2G?qEqxKkGRz+?Z%%(PDJTKe+AS`o zFL~gnx^w=_ZIA`lFw_;M|2H{%1#%w`m*(}CBpYNMkmg+7Z(;ouoyNAtKOX9Nf9(`^ zrgWJXQkVn@R@MA9^>}ML({nQkLV+eIh19e^$r+iZ_9Q^e%!r06j6-!*igoJWqKfj) zGN>Hf8`~XaJxK!FNX|CH)f`EUKE?rZ!gjwn+}nHQPpw^!75bT5!nCxsGxAY`N)^m2 zjI13M5*?4(6lJ(b46=EoaMSz7Va7OvWXx%yTNV=FsBU{X+;#3x8sXA+9Bg<-%3kCy z+&*}Vw;4MtyuR~Qnz%QLVnm70+T-QMV^3!IaYAnQd8wew6T;S|FB;r3%XmK$ko3z= zgDT7+Cbq#>l5B&v>5NOM6`uHNFnCwyXLGm456HRA^(T6x&*w!I`{MJ=-epS3MjJCX z-j7WL%e=(aD4B2ESLYrx$|^xjOd8kZ1TQe?GrJy3FTLoBdpKB~XH%qIx$(ehDEfls z6w7=_YnF{78)Ki*daM4W56E_(b(=JswYup1-zcKlk>qR8PwZ>@~|EjAWJu3IVs6Db%yr)|e@SyFU^ihTTFNH7f zh~PDYV@(wQg>-R4+Y0;CK?H|rurzQLz{-xndL0|8B z!vg?7&+D-M+xx$w@p=Hu0rhvW_ovg)z=NJQA8vk`g;PNFW8K5}1HS%`!1ZHs%tfen zV2J=t!OVxq{Vb>F%)F>a&l%Mp|Ed0V*tC7hAWz{Q9B!@)s6< z_rvY~EU@P0$8$ac|Fyu;#>F20Eb#M?PjCN-MSWW6PN9j|cC=p``}DZPd$i-#@1vdf zRZaY_JO2FsvHvAU>-Adq9{?sp#Y17d0-^MoAvG4MzK)ByD_Z@Y$mtZg7|)wv;+7zI zb$u}r^D8P3ON$R&x+_yvU3Qk^$@)@?a-Yn3%gvF%KiNBb|$i`_Z^*rRv)x&ux$6sI0U;Xm6iJBn4-MsQJWV>a( z>hX5#*2^#3Z99GP-`Wq}g?xMUZy3n0Ygmm3b7gA$uryFgm zu-n7w9J<@f8&J2~Cm6l8`v#M!u-7k<6S_Ab^QdlbP@!pSZ%Fx#!v3(@dn;Mn4P2xl zN=a=$bp(4^7o}D_r5wwo2c=#fvpTPfQc!NpgsWORr>W?G8pjf+J)Ds!Ns0Trm*4vt z7P4?k9N0mSa)RULv*FUi>Ejw|FF_UDM2 zc;fGmm7Gg|f381z`uEpX)3?9Bcix*E^&KJxS&A?<>tfH}iIcG^WpeKC;%?oE zS57TO2AFm8&+Q~=4wte-_je2b+DXJB%h0q$vmP=2-6S)sGWMMQ9x3hJyLPE%oR7?U z<=l3YU53lJoBDf|uJ5MckmbB@%=%8|@1_P?mGi&v?^AExO$$#g7hE%Yqdm8q9y44n zeANHu^sn6vJhB1jrrTi{Qbgpt4G@J z2i{(7-7nfneWbf)K6-s_znI<=a{6e1HhTNlehGl8#=48&atx}*%+zAyUj9LajCGBL^Wen8)`Lprv>K}bi^=@C zgDTCD8k^|B$>LuJkFcm(yF`nrGXC$?X4bV1IfGMG+TUyJ(rO(aSxncueXn&HsdZ`^ zoPKis`(qsHvCA8achB;_*9BTXc6&eg?q%!u`tY>J9%~lwTj#z%i5Yq9bu{?CXzpF>;R|wFeQ1E?hdJ8Z;Y*b=Q{YOc#she@I>HG z!0vf{|CJ&{KNdTC-1MZXggNWNPl;LWv`!gkQynR*JD2>Oug z)qcSU5F{@%IE%HNdFia-W}hr3-^_K>^5m;fj32)yU+c}^iT9Tuun^3aMBp-eK&yT# zA@JJJ-cr1*oc#(krrdWp@TId*{vwPoBnEO2?p63T9_*)E&i-JA-W#ow?2uZ~h!~o1XkH?6QjwWGVg& zyPt=%PRP&A4?L%17aY+6{TFum<5`UV!EU*Q&*n{w7q#c^+xx&?{e#^^$;-*8-!JN1 zpKw*CbKRh0w<=_rrRdkoC!Tq(t8ZV?v1{?_`X44?LH3<9*UvM`dkLy2_P!wdOZZ#0 zJTrKeCGq^laLZ*thyt$Q2NQd;+z!M#H3?npzC{PRL&Eb)G73D8=VWJhPq3$l9e8yY zTYKzixSWg?w@ovQ8CV))5SNx6HVziZE)RJvPi#$H&c6(!Wj4GzcHdO$U+lA6Vciv3qo3q`V;fKhqmh5OemH3xlJ#>4bnH@zXcL*mxPN2Uzk2aM*iDdG zN|Jc^Z|p85E4=(Sc9&Dt-u)Z9%jvp@|G&X54>(}vf5+~~#H2N_VE5nH4asd-`VV%) zp1rOAzrt=o7@nb?qwqByyUI;~h@U0!SMVR~LO#DW_=(`9V^^R(U2z3!H=+V6yzJUX z$L=MdIf)nvFbdGSIO*91`@>%n+O2@qACr5r3MmE29BOdSNR!^iAw|gy#(K3LzFC7O zUVm<~Q8b2NFAk<*0gY+XcykUsQ;>qCd9+K;!c&v(VPBWiQ9I;*WLG6Sz$`zbVk>qT zj(c-?ijg*DFyRkpxnwdOdYM@r-;hld+JjQ(&a98&d!$mY>}AN7JMNhPFOF3YeZFq_ zScoa=D*f2lRGQkux;J&TcpcdJLiFsJcSm)jC`FSYY3}#l`Op|B3FFl~-|EjWMNFb` z#|T??1rX4k!EUsoW(#OcVTsEP(WV*mbI;N7yDNz)$0P8I^_fcDz-ex6m0V0Fquk8+ zF^3joxdC5CjKg(24QGnZq%b-`aW_{jFl-Fyd7?8U4#PiMlAKdotZmqf&}MvY4Z_JrWeitwZw_g0 zK3HhgPSU8GkP*OvpI9|Y>b8t$_q7A7H$yH-qm#aEu_f^REA$n_9EMBe5!+3@uI~lN z9cJcOqte19c=w(WPA61%`bG?wcdRd(BwktKemGf<*{?Jq9EJ^f~?JkUgq>9?Hbd?|YlUQt}G6Jdg0Uu21j=Ta+X!v|b>X95EK zx0~?-2J|s}5#)b&vwuV4f4UhYbj~sOd`;^Ay_@9}2-dKz_l;K-{GZ$`zJSUbx4&OA z{lB=`Q*8rH^}5;Tfs?iWb~EN^;VY1T+$?5)b73f2M$M02IKkAO&U-TY7Ki+Q+zg`Z z-gMDji$GgvRB*l~^t_*Bm?CW5+8A^%TRyt{%`b2KojljcFR5FN|8ldBBkzO)X33tc zaf+Fz)}lsY{v+Q~vQHE^R*iiWl#YHh+eUJ1BOzZvT8+Fy8`rG zll^@4yW3Buo*{nRvjFO(XmO$n~| zBw#JNN=-AWD6YZ4_~*_G#*Y;nZECygIII+Aj~}6*Hvi*hDq4~7)*dtC_om274tx=$ zWjU$nmyH1S{(!9fm`wZ5N!0UHzd`1NoJRI(VV=ClV`n~fuv;oTF18pe-MadZo3Rm< z9`*2Y2N{m#maoX@8BC_!ANH^LCd~G5(MJ0^QU|4)uFQ^V*w8%rNasXI;>4K`{d*S5 zb%d>hNxKxsNc|oIXqt|>@KD{u>zdCJU%j%Z*~3mj$?(=no9^ThIkr}{z6Kk?@AFqo zNBkQ*IH#T)_Sn!SzsY@xSr`%?$S;SV06$Pcy)8tn*i<6EcOjI^(^sylb3JWEzln<8 zZmGWq#qv}@jn8oz!2>p)38r81{n0U2@o4KEGt5rSAf!$8oW$|#zKxWgKeyv)* z*N63*{LJ@d_I|8gIxip4HU9h_sK3DQrjO!2ikW{34;k@LuMaXZvzU}AX>?jl50>2s zId5*eEoYE=af(%SkMXU+OE#c<<6E4}!B|C=ifl;Gir>vI9|n>bYC9ro`J^X{kRj_7 z{z#?-4$xV1TCRONC*93{<&{>{a^G9p4UV=|STd?=V3Cas5qe-qce7j(`P-|eVxpq; z&2%?=sA3>efv6s)yO~&4vc!S?t$DheEuAn(8Zg7X3#7YQL>Nn|xXH`TH)rT>CRAW@ z2c~6eM0c~7u?aD9k-~xobT<<`TTvm-Hi&^F<O%E=Gw z+y=2%iUa%_9?;z^t_nI736(P3t{#pEM_72$-Hd^r)&V`4OUbRv?Wen0+e~V(N$vHq zLb{vH3QPHC|9|dg7g$bh;$qoyaSUc-ENTuaP+l?yejzszJ5~zTLH7vjha0GY$I?$U zlT5)`aAXWt+J%CHN;h}1Y5Pb<S=3-EhRBpoL1@Nxw)AAiNvN?w1p=EQAZ#X)r$DYm5|QCT zW}NsFfmwhO%b*Z=33yxJWqPv|nvTM1cc3ABp_a9Zxy1tMxUSfW^;f&b{fJ_VM*+s* zCqq#XM{CQ){Khi4_&V6oAKex}Kgg4WGeyNlXo7;Ttnld~1yWqQV%=uX?l17Ud<0!b=VC%c}O%lsVvcvsH zf^jdHjCd8tpv+t7QqeWde(AF+26d?yfU))ekeugQvcqmS1yaEOIEtX z5fsLJN@ndM54Yrcccz%6?>vmepvKX!7T5`uC@gIrrE)10*RC+O;V_e#rpPjU097fF z|FnO1=3pz4NGgY3!!x9_mMsP85-03O6mr>AGToTt7=PA6Sjtj}rDaWL+k*+ua(DT- z4=jXR!$40Nz(Is&eEWqMSU`z#nlTB(dhhUgvnM!E|3f2l6B3UE=Q2s^MzP)J>c4#v z4Fsj;IUiQ*qwL6VOQtw}Zvd>eV(%5qN-vXN?QG8y!ka`d^KIZ_%bmB}Iht{|;P&cj zR#{0uPM&8w=~CDe01c9pjD>!st=83ESU7*oZ^Z9hUt>M3FrR5UKYpk#CkdvcutHuU zM*>ZGt*FF+wBPG;o#d8~>);l_xDDTt#7 zSZ~1=zpAyxUB9E(J{44I4c&9jFlHuSSMx8X9_d@lOc7<3R8(14I`auL1tv{wR zW8vt_QA}3NDVaZd9NoXMAd@b6T;ENyTt#p`T4;mrm}(`~eMd;jz-<|zHXZkND*W4g#u`bE$qO`QaO zv=hDK9zqXEt8&J3i5jZMp;hsIVkYOnAk=-l?|L{|9|Il3_^M}$f3*uLC$r6{OZXkR zMp4dJtGnFef<5O3E=Yzm=rd9-+#;VcG43_pwu38;u@%@cdgHIE&al{#!^W_V7jX3F zUV&G~94-CPa^dJ0e~7RsqYiHzbT_CF%q&d-+f(GxZ=itzcrP=KF@RYL>z*DClOjbM z8h~-(jB^YsRsb|BmPLi^v{V_Gb&vHLx9GsCn?BWJ>>OMI>-laqajy?*>V$sl4C}!$ zMPWTEXwxAhli~WA@qs3cy0GBttD$$PU?abfM!WN)7^F%j1V{D=CP_LwqYEoFvtm&X zewzVufQ(_C0qx7OYwTfJKMb*V(X$6esM2i5Wo!=wugmw~MdW=4*9T759?&}8`Tcm# z?VmY#zT8-?+&I_VgloA;dAZ3gxv8_c=|6Mv=Bx|&oU;3J`%r;w^Sm5CmIj>dxAMF@ zQ?gc<^GJ8i8z?!IdHGEZY9%%KPk!<~$}@SYRdA~_-!_X${WEJW?h5NT3vLJ1$XAGB zE3nmP{PK>~2Y|APq7?0zOY&HX#`3$&^ImGPJy${AXi@71OwgUI7Rsq}{jhpk3j(qW z6JKPFQieR!DvC5OWYcHu4Oih7FX7=}?p4-h>4a3;F-G9y85=HxGFg3;+0N#m>7{7T zU5i*tbBg!?)FuhS#@rchf$W6Z0s`x@kWZw7QxxzS9Akb2=?|`0F4xf$?j%QrQU)Af zo&y^yqXjohF>+{X0Ju1wRJl>i(tzUWM)Me!G7=oWA}&$UjD$B#uNjaB013V_q(g1i5-Bdb2*vqprG%oY52qm9#X8yE&d6X06T~r8bW+u2vLcu zkwe#6u)>pTDr>5k*h(NO$QiyzkO*dw9er;@X)gtBwhHx|N3-qyO%ZI(JzAX3BMAdRA52$6$Nh|`Pm5OhPli{m4L__X4~>Pqd2!4jN;%0-k)9M(@*hV2T)cK_Cc`+`_!w%n?X|L~$Z^WU-ViiJ zpp@r1Dd|ilOW!Skzm_HR} zbCO8|2{-Iw*4agd?{uqqbmBH%!$1}INa2S+Ng%3zwGJvg91a>|3d18usIY^`@C6Do zOqpqcN&!)%R_r}XDun9Ms0UP-iY{s)98Q(jKEU*h?;s;ndwj2Tx}>N@x|Lvb(ZUC4 zjrp#m%kUgP;1lh`8yW?Xy2Bc-%mkt!axgOf6y}^?$UM=G`zdburLaf8Sv#m#u5UfC z2t}KnZ-#<;Ig(q?tibB2{4$}hgEQSy@m-NBOyOA8F#G^mC9ij%HP}v|lGGhcg*~g| z3#-fWw;RL_yn$!&^$$pXa+kMYi)r??Uy^-V5)-U%d-2dUr|heURe& ztJd$Lir)LBy}!9IedEu2&#&)e)20cYGcMLMIafxKF3Kl}%b zexS;6KX3Scjd}jsZ1djC9kmgH6;M_iv)h6;aKt}^c9*Cv;0xrkd7QgkCPXz^Zfdx3va>C+R%^yu z>P?!!pSpiiev}f?hnW!3i07z9)?zgDr5t(-3%N>C_rWGorkVA3$VTzdLcD4b6+>a} zwbEhDv}CoUUs*y_ld^?!eMU_Z-T>l{v{TxTU9dVS^r7i#_C6qN==PF;%J`>83><1%FtT= z`e8gHJu`861V#_ZlffXyD~tc)kqZ=*S|+S6vXrc{zHPTIKi9cWB17~c2ETn1l+Nqe z1(<3p?}ao zn%goBgB)jRiTMFFc+gC?-@1Ea70L#cqJUMX9{E0Cl^rKD0D0$_xE_z43s{D)(2fwk}E+V}yfyTC;CvSh% zM1w>i5bqtxBDFpYZf&xe@s4o>S4E?^A|In`tzLA~CfgNQ7Jhh%Dl75R#@5K_kLVOK z)&}j+3qD0znF@$97}{cfOD=#Ff-S!C>JlheMs)zWa9S7sn^oT%eb)l*XTv&HweF^5 z@CO|p(7FT)Wg*5Mp1iYfQueJ(4k|?~7Em26aDile`sx;5n=lWg0|6hOI*Y&!t`+kncGZ0Pd-36|Z zS_lQ^CXg4|9z*y8pEbxt2UD)n=fWiab}?3PXT;G{_&GZa5P#`vNgs9hQM?9-2h02NgpOSdgy%6+g52*4QL5j&gM04ln~MMS&p6h_|1G17t+ue# zKn+*#U66)@l*RUjr zPx$YPcI!r|titEXeD)i-3~oj5ky85)rrE;!Nf3UAkLLN3Hq}D>RxU<`ie7O6&4)hL zsf-~^mDjJ-yr8Hl9toYcFUqHnZujHLojyA>+EkrS5pdpdy2t4Q)pd9N;?gFOg1jk{ zEi%&j8c!tUXAv(zSEs*1t(s zX48|Mj+nJXrLju4I4xZvSonEe6qdwXuf_93^IV zyl)WFSeRB8XIF~$%Iyc}uxHWhifwM?Sk ztkN|j8e`0=Ss!Y^=EiBpGoZwbl1k{#GihbN7|>~wClEXPFx<}&-c%VRfiV@n_OVlp z{f1BoFJ2-hjWa*Oo&#O;5B$JF+Kqf@k#dv{ru;%}NW*m{NG7RylZ;yx=}5Uxma;E# zj{TyqPRvU5g?2t=@mbrg!>t+xg7|Jla8j!E{#vNn89u+QYBw*$^nsopVtw5>Rjj5~ znvXoT8uZjqdGzFo2C;A~#T_&;m#ssA(?ld~U5Q!8g?8UWW8HhMP!o~OH4urBlkozS zZO8g7V6pcs7NKWPB8%>KU7`qBANW4^yi6H%O!;?x)H>}+ddf@d$D75EjVij#o&!EU z6I%dmtxF16<0WM>W&V{p!&DeDMxeIK_I_!A8Cfy*p!jEURj693Svk+qfh61Y%@87| z50a>{s=jSFHWem6*X@y|85jOa8a)YV(REDG-cDo;~!$*ti{6@6NbPv|>u2B1fGl#aENSxIL@ zdZ|2I4)eU$B*lsBGQK#;S(1~T*jA#ElKp3OP;gE0oiBh5(TEo^HJR6$^Sf#-Yr1u! zN4bxC%x=WYNJP2~<8@X^t5wZ+l&krg?g0AOsE+uZ#dSS?vg{xm^_!vwpa^PIer2ww zW<4V12=^yWpZ8P$wb2e(Otp^dMDGxYsnPoV*YWr5W7lK36G(s9j9B~-0IPt& zh${|;Y<{rwuG4ZbxhRq*XpP;v($ZbBT#zl*8O-yCO6OGQHDq!4mJ>%N+UoFm+HDi- zS6Ly(w$FW70$q950;i=P&ANqK78+Mrn_s#qW`l^H7=F&Tm&&z~?iI|%Av8UceqnX; zU%XjC=^`0@U6{Wvv>DGK`(jos zN@5~CLgh|4AQYRM$HlUzWp3m?l9>Ww3=wjqx=u&~0*A!Wk`@y>w)x_m`3Na!h=py; zQEpYA1h+V&rGrstegiI0&epW=8Ln^|zRke?Yqsm=hJO@~S+9j^0udBMXSRY#Y6Sd@866d485i$pCBH zXAt;plMwNNRIy+8`xt_q&a-?K#C1H^`S z?k2!X9ZnId_a9Puxi6e=C}h_%%KE zYM*n(hC29F)N3Gu)?H{-l#P!Nve`mqD@xVm-VOC9;*T|)U@Y8Y4`L3U5CQ&D>lq~d z;10>S!plC@XME%{t;DbTt2-PO{+P^;y@Tmx~fbx3&A zA3hV6%YF@t<&)?Ue5N|V@{N1M3j7R7|54tVd)pq@7CYJ+Q+%)P;%l_q?8B@UB`rlu zKoL^X3c^aEJ(9*uA?bJ-!x!#duFYA}kqwl2)u$`_Glga~$ke_X^bAKbXC(@zKp}mC9l1W!uaDx=mWv$qsG1(z7R~E06rvysqDEPSm4e|e zU=&7caV@&km9I7SoF{Rrn6Mo98KWRTFvxmCn>kd>`6zbo3J1>mEw?$dOU*0Pi8|oK zl^puF6V9Wz9_%|aD3=TDNRk020iy=%LI>abyvRX+6FeXWW>y@H1xH9vi%U@(NZzcF zdKJ{5d7W>;#jCaW4go9IcKV8ALQ^|AeD4|4@M$iucih-T9vjlN`iIw%DqV&e9Ga2l zBd_9nrEJLX^3J}X_4ytT+lR9L7?(_?B>SqcF}0|!Ka;mwJH;ZnLaU)RABsGkauQEI zoCCPk8EPh+yRmira&pkz%xLaR0m-WHYJT?09RBJ~5No|Z<0bjz<@qbKC(X|NI|Y>T z*X^m_%p8O-yT2(fihFdm?W$M4+qml3Q&^mn=U>ZHlq1h^r%y-Gbdh%*vK(06NZjcS z3uP93^L;f)O7`QY&}%EQ)388o{VdK{(&3wTraw$my(Z(AgQY@kul3o0BG~pGwg&;5 z@*#4ONhJYqUs!Bg7(vzP4!!3jyvl=V;^8 z4ik>bRb{Ee-;AUT`3YL(eq$s1-o%e-?)Za=Q~qLxmra#P)R|rtR({#tFFuY) z?=Hwf4v@2Zd!z3W<(v#XLyG;`Zg$`_cwB&1ZW=QhNdA!o7RF$RS38;40I?!NEMomv zdh1N^RY;38H7Zcz%sH2DZ%AKINKdW03+!KpCLZ+$tg0}1Vnys%^)eh zgTDVPLs-1p7DM!(WCSZG>kV3-X zi*)dgC3o7{pb&<~YDOtxrjuc(lZ47jOs2?H7^8%S*4I>3#q&g&)P;`Jou=xA<^~FW zJ`gIutHgZo;>VgQ!sFD%QTx7roe{R{avYM)>3McKH(S)wFq%FRCQN;e=YKbzOGwf5 z8)Jfu8R>NSZr1@$>+o=im6kEQyos$ty#;fssm{4H@OTtU_hkhM+Yjn-VPk14k`4uC zItpDk=Eq3_<2O&njbg@caha>^j1$!-RG7@IYR8iy-K3B8epZbE{u4=7=DG>z=pkrC zNX10b6?5EVx2ia6#L7f$%Y+v4v6I4MT|tY;FB68Ln)P*eivqGEurMFK6o(!QAM9Dj zp*UYc+^(Y}MC^I1gES{CZtk_SE5f~$;9g3+_I=1%7!~Rr4-0U$Jkw8Hb5osA&l|#EZ9-C) zKC=6AQ7{OK35~dKaYlqDm~;>$P%!Jodwq$?$zm|andGZO7(oh*{;&op%?IbP<0jgP z#8HW=i_@CViU6VT#yYN7^-~MMDbryoXB-D@1T19xg?%nacG3D_AJ?6cEZd)jcF{79 zhD9HQ;F3*-=@xLIiOO*nVTli_59HF>n*m1|8lGQ;Jk z0~AEx&OEChDs@^92;@7<-ljOM!LH%g&$QdATu$h8EG#bw^V8!8bE*~Qj^WF zXPprO_XLss6u^5rbAV&;MZjDZ9SpVY-^~cDe-Rh~R0tp{p7X;6gJD7V6jVG2+X?hW z=+8A-cmvW*4%*xD+q7r@lR51#0|>~=Qe^cyk)P$XsKo2#+!PRh+G$%lyuBcbtOfug zqKJBnP>%36q?lQx zjTdkWI}$yJRa$4LD6Qhu(u#cjC2zj7;b_;UG6w5KZs^dk4q-Z}$0^T&zjhNv51^E5 zxZz9F%&Ej;eVxUhq`U&2$`oR83N6LX_Pl=?QXEz?n-=9;OM< z3Cla)Pij;NZOU*Wg#qL%~CU*;(|%OBm?*9 z0c$LhnK_ePP;d>t6nevo7jYY=Ig^~S2vH-v1q4G?N>c$cP)!V2i(=^)1y;d;Hv(a= z39pvoD<`6jhwfFPWZPs6hE84DTY?1)j`by4qocl&#{|nHxTL`2=17@b0d?9KG@Uk$)GAh~1 zocHbKTjGJ&+NM_|0jz~72u373py2uh@TNwxJqo531t#Wtd9PV&*(HD6%)GS8x-KTd zXH=~$1b@v17Yc?2$S9!++Ns!bp@S0dV4^KnAY5{`=FlFMA``ArA%OirM;eyr6oI|h zpw==#8;$;LvT=H@aXue#?$ud>foK^lgT#Y<_~7n%kTeArdJ6jE_?gWVl=6(}`XcvK z6&2T`)SyAdPzT%kF-rm499qEV;w8F0hKs%+`4CkuR~1Q&_TEn}tDCF&OuY5`D|M(= zjd?3JhgD*(GLhDNA!?*GH??h`W9G9*590A&Vt~GF}TCC`Gckzl2 zt+QjFdgi^A?g8o7d3d&o0(iqDWSFP@6eNZxR%2>KVz-x#BuH5jr}oJ{`jx7xa{%9+)zfm>TL$IYV+UT@Bj2n*6aLZSB$4 z`>WtW9{78kuP)TI@beiCnB-Q(*PjF6Sk>g!*U1~3rfT?*rRj`I2yii5)?QK8zZy9J z5(P5KR7Svi^ng5gkNNs-tD-M&9G+GmRrFQdBU8!!{m)%vxrsF09ndnlv?BF%MD_~Y zULqZ{4UmutnUhb+m$LPnnKI#9R;CfX)>6c@nS>rKTJfc5rHUqCQtX?^nmrq`5=BuQ z{Ph;=D4FEvTm=eYJ3fcuZ`+n4%{N%91$oV5=mLncsAn|W;z*pW~Q3T>~u{krGIu^#34p`{;JF4&Zk?8FV@ zIrX)s5XYtV#aVaG01H zFHNySoskg$;8P--9YGxB^6D>;tplKmwTz0Rgp|G|VlQdhflb$*ohoI`^#qyneFDV8 z)F`Q^?}9Y1gTHoo9 z&OemC969Ru_+{PCzo}gf2}S%Tmwc`|TNHoMDX#S9VHW>gM?3cT5u*zzdpJ2X{fuc> zmnk=7%K1ORdRd}U53rr_&&_ncvA-v&pa=cv-^^;it$z^jisnPXXQ2gs5x{pM&Ax5@ z%Nzc6PJ#?~RfXr>ow_xKB4-Es`)VU4$en?2AKzFcSZ&`_dHH4XW`+ZH%MG6D4^HNG z1!7Qy&(Z+FV4y4j%pwe6c4hEBeS5wA8W96F56&61-7Eg;AH4&CApO4mq)+ z+7)qe>PJjZfiTwFbm9?(plAS|-JvOCJ{$irHN;ha zm@?uKI#rW?Lg{2w2}IV78K0ESS^}a_5{)8fJwInaQ4a@AEhrYuAsz+kkgNdC)Qua3 zmLBFBP3ak1O9mA_5U%leTQWl$sENPxx)3^(#^B}Zjnh13t0x~J-HQe z#aqJ{@gILS2pfF+CSe_F%jhSs+%jXf6cc75&(0c%7-sI*-{dq4aaSaXFiTO_Ty%U6 z5Fe63oVzd4iXYc+KU!R#eHC9gczN8*JZi`iF=|0}@2YEnwfCJI8L>ycjkt@Wh$k>` zD9|U%#=-pq2S?L-SJx1W_=5*xSNf0Nj;6Yt%otmQ=p5@E`4ro&V05aXD4S54(#3HW z&ZSpXxRX_GsBT$!kP(Yk<|6Mk`SuD8wO4qPs3i``r(qAP;5HYQFJF6NFZ&YZ_(I_! zm-r|HfqsVBHToCL)p(-EknS?#5j}BO%C@-xaEDnu9?W;Kiz{tW`n-yl~v>vX65ww)45?t@3eKr z4sccykyUHKs?=d&{l*r@X#;TlCdna*nY(ny;40bqyw>|h$s8u(ht5_P6s@+xar7H` z_i;Y9uVdaubR}n>y0P9m__F9_B#~!cZ;IZhlws^<7!n9D9b_|V5JVG0&3|=2Ubm&& z*)caYr|M!?76Z{BjSQ$E5z$+owoq4fy>E~t&Xi~HF6!aSwMmD+cq7W>SovZ9jK|#R z8yp43g0Kxp68_^e25G=`7a}N7bq|g@EW6Ldl&c;9HZ-N#iCkAkAEsiBfg$D(Z%8nNbj-8333|q1ENt17o#Du_AxBdxrWy#X7AIQ#)w2kicMQ z@2=gTS63}}3w1m`c81AC#uw%;UkB?QRgKiSD<>WUl+iy{RYc*WUo;L1_58Hz{3}g$?2t~~BC~ep z5Qkk)v4PMa<%{gPyRp@_ksuRqWrC6JU0)zliKsG~NMUs48nckMPuJ7?Quj3X^{@=l zF?DX7vEmfR$}`Wd<>|*+N&zyoXuM8syi&DSZ#M1Tk>=Dz?`S&Lgd;l*XRC%8AtQ4U z`Y!HoFzOhz&=KO9tz0^$w4JgWVz1^7@$ILV;(5Z-Iv(>0k_phO0fBg0Bd7vpM-3QwLKl z#-8)0S+!p}gq2{4X`>OK+S@idGj_>*fURWuC*KaZMw2LC{3~UZnaqrPsYb79q(QkO z+pO@`(%4Edn2OyX$y%MA<#N1sHGGKCH43a73+5EBKHHJ1S>!BSgP%C6u=>sTbqf82 z$ej+7Np)pHi{qMsCk!69!jiv^)WD=sS1ybhfq6>SjlDi+{s7TFfX_l(KcgNLfBA#w z!^PJoWDn<5&76Nv9)yUd!Zi!m0!2@@CJg==j>81df?`Xs9kh5Lq7fuonH#hDoLb1f z>uW_RpN~bFGJbSXn}!i-l{=>L)rW8(d4U^G0IMI!q~rsA0>|KWK)xVrNa(YVMz?h4>y)m(GpXZlvmgd1W|$VkxL z{WP4bDlj#|+2FM2%fOF;_p@(}hfWN7|Na;=IDaIosT0;Zglq$hw|&L~w+fQ%#jTRw zYUcdid-?P<57a-~&js*9`G2*#yZXMj{ol8i>ZK_=E@{6=ph@e{!&C{_@Aof$|2Y#VOaDIin$LUQv?eP} zPK6s~9R8U*2e;7-8&-NtVtU7V3Z=cUA2~QHPO%%(!d{ol|;a_4kXB+f9=hm#rc%HZ#t%)H<(+k!!$+icO zyM1^TO9(hlOt8B~a7RSRkjyc#j)*W;z_QM(5LDm-;6BI2r2b*f6@YUk?I z&Wo#?)~VZyYq->Dc#EG4syi1XeoL`d#vP(rXeg32&dX&cF@nvvFQ2gkW#KN66NVi0EB za4IIQ7ocD%@lqzkM5#*YmzQ#*gnMIydz++3cZ0`(q~}n>d;2q_XRSKLv}X}0+m=>; zAKPq5Mw&yctjEmO09LzDp1c{;2$P&gCKmvFEPC#~jl7I!2ADeW;h)i0^<&btyhCer z+R)!&`FJ1Aks+&lBf?#C<|wdw@I|Wusj#svi;e*4cPSnd#w3#wy#ph4yphjf0FInu zkb673&qR)8tUUmFZx_n0ZW6kh!ihxU!xzMI3t4961J4B{YCxC4NaSY{!N;M+}BMqa|r5F{n0rFEJpwY18o!wJ zZZ1Fwjydtb@dX?z25_Bo!2Kz%$d+r4sJ$KIH{C(Flgf+f0y|pbvYA5FP+$)Zpt1Le z_eZP<##p%vE4@3K{o=W<$*0(zV6{_Hf%9j@Jt(d#S!jk?YNT90(N#irRW=)k=|JDr z0<{ACnv>*v2E-}{wgFeC45Ew?r>+2NIBx9{pwd5|wY1TeK4LI6RzO@bh#D<08t;WI zrrm@{)~B_kyfL7H>sf(1LuKl;QpS(W{d39UI#o?mUmChBWm9~5f?CH!)|&^mtr0uQ zJL_iT))GGmCgHPm%xXe<7-x1XUxY-$-;K#RdC>^m^sg1sjGeK4g_q~ndcN;w*Oyq8 zw&J&ZjTT01&iGl>W($HGu3l!w^#1%m?{V9`S2r!2GA*0ouik16y(NYUxZhvQ-dp>t zumlq37rAhxaDiz|NMs=IMfLsF7x(9#72m#FxwC^*{)xmyD;|_KJ$`q8jZJBwOmV%q zB;o4T)W7WV)V=ldN*@R2Tuj?$_5??hc5ow#n-NN%BIp_Gar?ib<(rHYw~O08zu5X% z-NxV^w%y$JWgx;lQEB(rJC(rw-FHg+rsTIH%Q?RgChlx%p{gY+p#UMkoi3mxN(TN zmrWLj5j{|5E4S1`tU(WjHMrR$%qlu+t7HtwS_xqL1O3}&%-2C z%dS8S`K^s(oJ;R^a^hNJON6d@cR@HCt695-k*o4mBSWVR$f+Z(8L| zuC~PWbPV!n8j`A*WRt)l2$sbT8r#yKwFF)6QjEE#nW;|u6_cnK^UNmZxkwHAu~_ku z)K&wAMo~1zmA{2?ddQkA8Utc8k@EzWfSrUyKFQM%KrlntEXGkFtm=g^P9hc#04w{% ziWE1S*B^HOijVJIc+-fSuM~ls z`5gD)%g^nTdBoMm@EuI-j^X)(?E!U{hc(zpbl;yw+z7nN635(X1T!_Bj(69!V*^JI zsT~-h9_C*AlN>9}J;u{zz+l5!pRUdp%%&^GI4Els_25u$W83R$dh zcP*+(?p!$gy36yf88-2ozcv!?hhgr8Y1SJ>996uJh~}&fi?|jP#jbf-7h|e*Eld<2 ze&GjP_50%i2-ibML3aurxn~-tsaMfzvo>-?d2b)3yy);aL3b_udHAH|a;t}QcR$Iajck>%4O61r?fK-}lW}LnHz(@$`00LnPo~+G7Ykb1 zm+r8CidVPN%%R86D{AFlh9o?sALfc)ALxu1h)Iwrv;a; zUzhy}xuJa8xW@E-egEtUR?@3z@)Pn@ne+Si#A{l>3hm0L$Pz&eU1^_Q{^Ppwf%>@i z9cK1&!2?i^Klr^E{C3pe@7lGOCMzrUl@!!1db;I!k|k^pv!tIiT${Y}_+!7!mxiz> z&pM-Y(uB;XFfQNk|M&B>;jT`j!V{Ta$x7;6rl-Tw^_Ll^KA6CV->8SFpMbP;a9Tq$ z`4J7KrwrfH&F|Y`X0h}{gE5Mk6({3D?;f%)0R9x<^oC*m zsQ?MR5VqK11BpZvv#S8TE6qRDByIIx=0I8mYr_{smu2{G`GVEfUx7>OGdL>Jk%Anuqyk?UfD z(ux$v^Zb4oAfhwZ3UX#JRx|ops8Rhc#U!9FVgZQUjUJA^XU_GzvG24-eqgbrUo4fG z_2d`s;A$Xd=0V30uS^TGyuN!yCrs4JalBFIQ~^_ z!Z$~QUCpHDJLj+v#AyzG@D`XIMm3A_9Rff_rPQ?!Mf+13Sm(YJ%cV&YMGgP0@LU!& zFF=_U>-FXbJ}=4^g5po23#}?NqqpXJ3oc!_V?cy)>l6mt*O}!>IX@{3alDK5e-?9E z80y@ja~D}8Q%FZy#j3b3)_gkhQE}S}iztwc*mcgWSRE_UEs5}YdhKxQ_0u2wE%@-C zFdjX=7Q+hq$f(@HgT)p<=62QmlTI$*=OrrtZx?dN{$T=-{u8DC@W&+9l`UoL9PZ+U!p(|PI5K$Wij z%-ccEOYxn4#=Y4R&g^#Di8p?HT^|=!@Jzb-qKjj$O4|(;@tY20v7-Sm{>r9-fdLXy zUl3t4gA8I*Sp#s{kGA^6#TL6^R_)ymS+G8fie_m*t;GnZy|w))K+1D=bb2kpevCh) zkbOkJhc0Am@NZ#wc3hPB=l&>4UxA_fVx^$elb$fOX9nn+Ob5$>h)U$ptLl|8^qbIi3J;I)x$T+@wp?kR6JgN8PEZM4fcx9sNQEXVK z0q0L^CwskDp7Z^M2WrjK!g~&O)@CZx>XZ5!!lC6xDMx7~kujEc3o0UfneRTRZkexV z(amyp;TD|cvq)RQ_pBhF457O1*Ke0dvbDU*CvX2x)$u>!{Z& zE8dm!i%}fLX{lAqbICBYra0~d&?tSQ5VKAe%(c^O_Dwy!v{JNPQ}U>1taai2Esj?1 zm*yAgzc$iC=CqS9R(xE4U3N1J9Rc(EXDx0pke+z_hJH@$0ZFDUU9}bwT*0?8J?xK{C&( z9U8`~!!MuK`Z*n!x_)F)pNh;yy%xPd(i((?L2AwTu3Y`|i95z^M~Wx=b|_LJ#A<)# z>mH5@bvo7N)v|vkH^o=+fNrJufLHd9d=4X?5lI@N(K=-=dilm>+_RBQcWu4TK- zrZW$}f!G=BBlX5~mh{_r6X%&akvx?gA}g28TH{HM$L0r?Fo5+?22XN<9+B^MuovLS zP<@YxxB*hR2$%}Y5z5^c-*xo`)N>GgZCd#U%tsiZemURtM=rL(hdHflNj}w?ZF!$os#lfh|TzG3I**jJ@eIUPwnsf3zJw-jh8>Q87eV%wDFP{#y8p0r4 z32mJ9p;h~nn5@UFE#kMk2IZDJ^WXdxPX(1Ns>g9KTg|8v#-Exajg%R21i3guSerszAPT8B|fp{`Nh3>IOWORL@NEdh2HpyL3}t z;MsEV*$~owk$+6D*OYki?SV>{LT`Aiz-Sx+_hsK#SG)}YUVK_wrWCy%la@Ni*zs25 zYggqB@1gl~rlb1%7KJU|KR42!9&cJ0rZ|*+9g}%HZ@-7!6Qr-{HC&`AsW@PAM_eV> zOQiR%b_5D0G=nZIT7PT6SlV#{6hrDI1`m)9-z%1WupJLcW+RCBJY*sc> zH-gsX$G9G-QnB;^R+3sE^3mfg!}~B~>w``8eVFT+dpB{5X_D|#z%xrg z^e_#cb56m6BT72%MypjSb11sKtglb<`+tJu9huD!X@LP!8(iZ(+N2IP#YM2W(D$4X z#kOcspvdoc-Dz=TAUv7#Iob}-xNWkYaA(&9A#{;JL>wwOXJ(prei@l`M=S#{Whn(^|bS1S4u!Zb3D1;<*d*TTZ|)ag$YxwzQ!yvKPxZXtwHa?cWPi zjnmhhP^)IGa2ADBGO8Cs(!(NuFEOBUlsa&o>X#5zk{jQZQ0Mid*cCb-N@Rj+zFGe2 z;lyrfK8aM0`R_wT!k+*5zF``aH7YRQtnmVSB`Q^lbO4Dvqe&NgjGP*hp6gHwuDm=* zdGf1DFtbQ#T)lCGe7dv$B5rDxwQ$1!#k2=HX*2*L~+As&2 zG*bR#`t?nlHRE)ymeN5ks3E?C)HFx3%Rs+x&3RwD$+9=2`Q+t5ho!Act%`_0Ty&;8 zK5I;3myI<~eJ*O?o!tKyF~JCrTKEsv-uQilO0?RM9ebX!cImKoxe4-`4NG$-%SCxk z`&m_F^Z@p9J?@LlN5uX%g3+;)htZ!CYTb=HltSfBN_5zUsqlgQxxeNjO~R)uaePJg z%2BfjlXkBAnLH)Y0`yNLzC8?2RuqrgZGm1DrSe0z8?!UHpSYfO3Y85BK&OB=hxlGk zAq&QZ)}v(yq6gR%1|5uNGB@~dDhfPS5&6p@J2TtYfgA3wA3o2*({h0)ujM^?PgMAU z^ww;j)0B<7Ld%go;z~B+{h^rloHR)FY1}S1bhBwb8u?98@GS?QZj9u0H2Pu7GuMr0 z12~zJyeHNiG*?yW=nZjAaZ%e*sXyz2mPgOCYXvP;F{{4ZAiO{r2+m%|$AgjS=tO3< z_Qf8_c@0Twe+XQ?B)P93|7RkEhY=nJa_X{$7LWpYY-J__^C)Wy9vd$^YTb<3#l@q9 zesjowi`lYmJWr2f^a{N%8Dp^%u_2mZs1O6<8QYf_vynXlvL!jlC{4?Qu?b|cBy8ZT znA%t0P2?`*%N0Z`n>eDn^3?*UdLqmTlld)SXr!t#i^BFpr*P?jaLFw(z@+U<pkfwZQ72{}asLxIGZqMWF(YDG~H4PuQAJdX|wCfrz^c14VHKG|CV zp|~ZoAR~ba_%(=8o1*80c+d1ofHDgi1<;H5FqK}`aU}>V!`98nPzejIPYrE_T^=I| zrmltBnuJiBwZYWowrP**ERFh4qCAF~TQ}NkV5JRSejzg+bKeXb>ahThcjtqlWeE;#}aMNNaXRIZQs>_tqcD(h97)zk$92a#&$6w6*dv< z`DBx^TAel5pMyZPJv6?#IDT{Olc1D^%o|74*yh~S=4bVG)$!VMHhz0CwR`6kjq{?# z-xf;hJE_Gnri9ichWdJJ`x-?aOE^((x^5)!v>zfiRG~w#eG^yy_QU17uqdn{2nXkDcf*E`V?igu`CLCbPbM)gSCO`AU|f|l zfX&PSM0Njy6+19voH>#purnEgE^?`z&4vns7Eo=U;Fc$uN#=^I{*p7>Ul)R3*jTipitD#*MGJ{Q13RVC+k%HmEh4VJZ~y&`2vGtjbP{}{yuz-{lFy|6Ok9fA>`)3M(T^OhC?*#MJ`7#Mm6py>}{>-2A4xE5;H<8>gcc%Yy+v zPt&|88lREtm%;!-^$Tw)QZR43Klm*cJW8QQlBNDWl)r@Tz9lN+jEX=(8EZvn@NW}e zZXPg5c+1qmxnKvcBwU&lQ7K!c4dS^&^s(4ovbIBWwf}aA)Z?u|C zrim8im3%Da$P<6@C?btlf+}SQrO3~zH(}AjscSx7pa{PhXSiv6-(3vU=!<%X=jV9Mq#Gd63<$Xbz9(PG zJX`*#Y$$Wd>rH&c;Prvh>lrKRmD4}_>iK71XuH&`BUcF!2>{rY#-QZ8*ugL15MOl> z$;Lyy^LOcvB;XO({R(1M`L~~<9yNDt(9rX>cO+@TT;Y}W=hE2)YItc9%WK@;#2P_& zkOWYm)~#f;v+=06c3q{$4b8Jmli%DJ{ZhH)Pi=q$fL+TS<$!`Jl{XjZ0C=#h*k!p@BDYS>z3U(WG7zd&!~e^)wzIwI z;!$nE6KySbkPa0bCeZu^cpr=fCA^lJ12Uaj$jlKL>6L(jr{@HW7?|!|mBjFKVBDHa zevvO{wu(NecJmuv@V)PfL-F8wqd+{a1n8e@UWKUh2DPwiY)fPTaMby?@1TW21jKqPFSxqwz+P}6@uyPoGk1l()Dc-46<>o)O0gFrtR z0BgJ(tOuE*c0 zNnFGhUSwd%zV^2ko7VTem1f5!tHS$6j&U=Elf02iS$w z&y5!2yHNw?lmLXv8kos05g9E3EG|HSxph{jHTZx|d)aS@DGfp}XQWU&ifFKNzMOJI z?ot$h_H<08Qq~^8VWgdUAUI;+m{IV2@!l>Mq>#r}0wO6f)Q-8m>P{jiw$$9{u6;(dF@qJU^jnBx_ zO5vLVHNZxQ-=h^2JlGOM7t+xuVV4-OsC=5_sv+)&C(W0qzG6M56q{L16o^~leTRi* zUMz%?jGmr*mO+x_;kjDFi#J2Mo$9A z%vW)iKI8@gT1jiCOI9gIzc1nFx`NItnUlPyJ6qx);)i0w`%Vm}^C#56r1luz-4uz( zR95>6EE?XiB(N?-ZW(|+0QI?-Mg33`i9&d*#h{;cX-E{8C;( z1NdL9BtFlpb~oH_;BLh$jrS5TpX*0exq=3Kch|dLXG<;^ehQ!3lI}RU`E#vIpv}fl zu=f1(7uB`CqMUbqg)RT|uM0MN3?9^#d4(U9S|$8oKRki{l5ha9G!jpJmjjyf#ft6- zUbrRPUBm231v?NR^LRl*%~ytm$#1Sd+Hd`QTJwXMew??vTVU{)HSf=G*dot+>ZSqb z4Z$_jJBL6tfkuQd3+PcWHA5*Zf;x$CmD-VXE_wGpib~yBmXL13AGm7$L>~KHD8W>< zVX{cU!*-SK6zT{lQwM92K2ZLTqPy^F>iy#Yeiw|51*02Bhs2OB+303qAs{Ub1e^$b z3Bl1L9ibv6(t@N)S))NfCm@1^bV!JZh{=!NKXA`I_nz~d=lMLJ&+Bd9>7W$#>kmS$ zn)$#uO;A7eN!ehzb%D-{0abtCq1JaTd9BY6C+b|D9YfX{k0zVEhfdd}Osp0arc0u8 z{WR)l+Cw%!zDUz-aII9Sa0vB2mu(xX_e1b>x>nO-Z@Q4~DqOX0xInw<^viVZ$EzcS zx>5hu|EpQAxq#^Uo38Wp^ZQ56&%)#*DxF$_5U*Zl=(g_6KEM8h=db4OJa92e$RJa% zeQ)KG#8ACOL>pslxYqeqrv9_AqzCbLMIFPR9e!OND)=EBwdZ6S$jWQLZt(I?ACrG& zzL8*+9+zIWZvq?IdCFla!`z7B6r zVuPy45~V~=Pt203(K5^^nU`>{biqrD?zE3zBKBC}*qNtXGqq zR2h*WG8l1OTX;dLZr=Z+8PowW>`7k+Jy-zzowE-iHK0I3vOHxXR6{3m8v_CGvvHg< zkEp8Iyss^MW1$9Ubzc$M6i#9vpZ!VcsO>9`1`egKzX8A7>+4`Dc-Bi6qM&F61zaaw z5Hvhwi`5=jfOF|V8MbI0muwoF5prBr^-d`RVR`EC^9la+9%9$LnP=oms(@)m#AVD?lAz2=>5TS5L8#fPGxLqL9r>w0X2!= zbD%;>2SOqp3yA;Z<~HV*mYV)8bjo`0(PN{J46JXeWZ&S1d6Dj%ffn^KISxN9IFpJ2{QwJh@H*feyw z{TzZ9kG>i&tT43B#+iCmF94(l2lOS~>6k}lLVQa-Y9(11**{8gcf@J>xmdZ_8=VMs z;Q^tT3#@$UrQF8LCU4F!)cY|?BQ875B#<~?Rp23Er=0{tD5sPZLn&!4A#7?LQgqr1 znS1}xtoz^BTP#Pk&vuu>FBrxpp8SQDjDdbl{O(VC=38Rp2!eN8$i{-_r0j9u#o=$? zB&j5nFCjH3URknL4|PJXW#?L3K83w6zfkV(JpCF3cfcYqk{xg>r1OTOt8m%p%3?cM zQ`9k=X#@3OJtiX2F$`MHLe16xa-hP8)v9qgj8Ip&vr^(9+^o>!wq*`i$dk1vmX_D^ z_3y!Ip>eDfBHR!}<|LNHzKm;=he;gspnHwnh!`$GQKLEZs9MHZ=mHru4^k_6mnO!* z!IT`GfnOJ~#v$F_>ii&NaAUGx_#zCiMZ_?*E08tjTJsMoO_cMq{i;Evm>T;M%lB2> zC8Q(tCVqvOeQ2nO;k+Pg%j6V z#=k3<50zj#nzcQbrVFjCKg2mLkn9ArT%H=Sr&lNZ-lAqdaDAU;WiNLZ=chfU5Ly>d zM0?i8M-9-~z->~>&D%HPFMM}BAfI{H!WPzuyw}$TJI8(zo|-Eyg3$2q7`3n* zcjHF$dnl(}j0Hl4SnMU%VCt!N>wD^NKUplyteF_DIo~)&?o*usC3EPLaY3{#{|BQD zDb40;t=n}DIc=E#0wmyNO9V^^(dpCqK2?6R=4B6+U3BVVfrQXF>erl-8^} zzo0;07k11s+p0b|sN)@4t<)ap^eyI9q@h%UlTJ?{`|Z6u@{ zT_;Nu;JsMRLy;dN{Z8o%gW@Dhex4ATRD-XJ(a$sIVLCs} zjbXbc)Gc-SR`BQmcvww-vusV>}_@C|1i~G7hHF_CvLr zBbh(1gyjy|Cm^52(eLVwEib>`ewI={Qw715y72va_Ka9f_T`)WL$|k_aLFOsc%WC* z{8J_=g@Mixv7``B{h%0$V!t~6s3{mOX)^GU1b>NmCfc-2LYTIN1;6c^rySZGFt1m@ zpH)%`+00ZUcIeJC$}@s6bu&SsIg2GLX?{iu#iLb(BVx^WtF1DWx9VKXp=-}sFFu+- zQcALIG{e4Tr2EN=aAd)$IK>$8=nszjGi#|3FeUzy43(qxv&Kiwd8#Dd@Q>ysqgujH z;q=5d0({R8usnPPl)iy#Fz=%zFLQFqSc7&^jZ|c&`;!0rsQ zoE+!er_Qny(L&+e{PQs-rtCN>x%MtJ{eZpXJOo1peMktpd?pscVb02NtNj|bHv`m%XLRj!iULgNYOoeou z3G;$w4>;o#w!I&ASv(00x~j_ud;`ostSMbF$CB{zAd*7QH<%6}_ip}%9@C-jJ!;=e z&veu$lTHl&k&w8~C72kqD1+#L`9GTo+)}|lYjK7V(N_sz^Lcn?2`qw+@u2>9?2>6P zP|ag@n%`YvQcGyp%E{ACQzFb>y{(9ZT`w`LgpPz_x%zB*S8)@Y2NhR4o?dJNO(1}f-T zTq1^^aWo%B4^#id2k=lJvjq3{r87I6Xz8Zprvqn{SF$vE9pWXkm6tKh`7_XUbCDDW zsh`=VT?q+!bagq-w(}rFBj|WM;>l%7$~xdRkM%z1_%!bvkMH?iPQ}|D zUC~)uc{oe5>BT`G76A*y>g_;*+tEIz9&%Xn z!`pu=L_N%egC5?sjJ*uypn$(#7D#ck{uvSFmnWEIJRyxG`8Yd8q+fuN10wQrEI6WQ z(WsynP973 zXz5clyHWHBQM@2mykt>4{uA3Wl`wtLXJxQ>d!zUZ;{I2^BKD>Ni^}3nx%)pK-v2## z|F3q@8lvz!5BW#R{eON(C9J6>h|k6Qes_tB9`zO_MwRzhDVTnDWD5ng=8j=|{XpdN z17<{VQI&H%|AQWZ`!nvyHadEUf!@JkKqT~g9ESM)fx3U0hUNWR5l&eV1yWAlyX_b` zC(L3aYKmU^9t$keG4I`xLXl;5Y!xuJ61nC(&o|1f4vJT)$hAgHKMB3oh+4sygWQqt z8R$iKiSY?(6T2?2r6^mI` z5GTs6fAre%LoQP=?{Vx-cgo+-V{QnRFH$SFHxTCT)%R;kw10(~!ycr|du_|1ef*I* z9hcuXqFQiAn177RM7FZU#;hd^xssY%htJr17CDuXH7ir-+WZIe(%87js#lFCU8FIu z8Zn*jmj(okLi-ETD4fqy>wf*L{L2zH^0b=a@3l3B0pZXscy`d;Di8%Tg{@6%$Gj(D zIvdgNk_&^rCBFG-Cv&gv_s3#dzVo{~#q*I~TkXf6xUpFto8^QFldrbDOxeex7V*e# zD!SGG-tB70@5fnNFblXWaPYQ+)J3`b{^5ZPE*$})Jgm+kKSIVmRV$+V~M zwT~)}1Ped0JwAE9_?tWO;jdD}Q2qOPcA^ue1&@LYH9W_n%)JYJA8UOJ3!Nxyu#iNz zX`!Vym6TU**wLTH3l&XAid}Q6aTD_N5CUXVlQar2uSggjWbGTapbjQ%nhE}f*ohPPhL6P)Tq52iX^D9>{1)n5xYB0LRi%8TQ=qT-QCH^_>C|E) z#%CPA--zkhMIVt++KuQFq=p}H4trRL{XAy79DJFMA(J4u97xeFD&d0MT(}xLU(Yp# zyx-Z?vlT^V)*cq=l^V{NTsDerW&1Q9)m!qsSI+fKdlo^VKTYqYX>S=Qtsgg0{B8H; zO?k|k59O#4-AaY_?t+dQ2OcQgceZ-=>~c@muzOAb<`u5#75)ebUl{Cd#GD|oCo8;e zwJtXLV;vsRU;W&D2Z6q#iwgVvTJkx%7mp02ATNFey-OW>t5^IO8JNm8__DB{@%;EH z?Y&r_l?m&lLwYHQB0TB|YGh4#M6tH)^Y?+OfDy3t{f=EWu}8>fRMd5IP`Cfn;K@ zvQ6Yiz3`^s!LcHfSSkR*A74WQ5?E(J>4H=n?{dv4PzfN0gPg)cxUiGJJYZDfvHtKw z{tFLH>}0kLAj$y5=%75jWPF85h8$x@ByIg zqUkR(9K#1(J0%)IDyQ~96RV0|6#NmezdeP*W1%I0C?0Y{6SAQ{4*^&eWTQwVBG}{V zVI{|#DoCX9~A0}U9_vkv67KlpK| zH|lIBVfU~=_I0@n`Xw?-p2CEA$lT%y#!RsY@I)WNEjMNU63buPWOqS9haa%)M>v{6 z@C5Kz)g>C7G0e5X*HH z+?jN{ep$(S;h?}}`AWZbeJmSu@v{iPj{Uf2GtWK0#G>-VBrOe3gKHW?t*$A9c3IlW z5$d?qnu&9H*rNnqFU}rw;=i+xbCNZG9TE&D*(A=uW|^94XHvB0F$SMaWDjf2{bax$AGEXXh z-+ajlwIdj4;l~xG&cWg(LoM zom^etkKW#xT?SvMM0m5eg+6i%o;LFI{>OO)nkK|p94j|e|6-{ z22%I}q9~{B{|s)^h%v^}=EL93t7K zy9*}nf(UZ@J+h6BI;#&p-eUL6;+=t!u$C|9EJUGoSFs9PN=m}aL+3@n=tFZ;)$ zOTb1mG2E_*+gMnc0oD$CzW#~QPGj1qFKMUxp-RWZG1WMY0?vS9_xz7?8&%O$rJU}< ziGJ_o2ms%RsBK6q+~j_WfO^pKAcn+|IFz&5#@Uj_mRSl-uO9QFE z+tJ<>55wMD=QhB*12nQzjTqjxnlc*dnE}L{Od?nE+R!;}RL$0Llr5-+D3n4=4oqNA z7~`Dp@L#=dxn(j(iA`em8-C;BRxFOQ`QOi*MGE`56n#7hoM{jJvN}8cTcTO8R7he0S=7@&4t-0z0YM*0guiZa9+KT+S^KIa&!PS?Wzc$|v z8|b|))+aCgyxOw-WzW{yRPr8~`!Ac?E&eyvz6A6!C3 zs@zy_s|sRrmiIQcpRqZe(e?G|`;bCK3Gi>VeUdTPxe)u!N&)fMs>~}6AP6j6DOPl~ z6IG=z@O`{o`NAGM0Tlgb|LoZ_=R)7lR9-7K)kFIl? z*OZjAfbZmttpSu&`lYeE^alus}^Mg-u z)WOfSP{qCpsl1k_9OUwx^N|}SMwk$9!+Uok{cG3@Tm0{ceC2aLQ+x31N~8UdW@{bP?($yK20a5;dBmH$yinUZN6_&0bw5ZsWiF8+ ztCaTww90iuG_pcS=KKEU^ndqT^<8~&5DjT?Df>s6f)_Uy-WV7}=I(#?tK9tf8x_td zf3z410>JtSyL1#Yo^=|@U{T0QN_NB>{QK?pE9^g&Pb{cd3CRbf!p0W@)=NSH;pTq} z@ma9R1)&^20=Hg2*ME(rT=$o_S?{;1Z4fh}r6fI%I08peup1?yq7pOJWizOT9x+Vd zVd9~nJ7VmHx0;e;qSoi33s>B+9U_NiHJ<5&~_^Ts1W}4HkEBr*i9j&9H4GNpL@MSN!)i zBN^KncH%*wPzZ@tKyOKG+awj+MNU@#=x^KY~Gt; z@^>Xq+GGzYh-rX#L0wv6kTVJHFg5Mdx@Re2Pk|J@M#*&E1Sw!D%Sej9iOq1Ygu8+a zQ|G^iuo(wfnbpJWbAu^#R8F2A(sMrMsNb5(nZB8Ja0!Ft@@222ORMl}B$|4ED zpl>MLQzf0uCuIkqU!8Kz-Ts11=^9Dpmy zOG!ga$@}f3vU*o~JEXYK_%tq!o!f}boF2zY|8R*#7FW=}S#7Vq$8!yqq+m}qL34UR ze5~U|EGLmd{sQq_jX;miS24$1C(qk+IQB)J@lg3lonSZq06Bq^{gJsgn*hT>mcGJ6 zcfaZVc%mgEcBybg-0I2T>yW-PKGUjnjs%T?MX@WlyHH`BNmshQi=ih?czq?arCS^B z5Axt|&pvALYdb0v29b?~f=TJNYGoY*;b06E4`F+wGIINzoArl7A7i_~mb-AO!be=9 zC(~#&`=6=6sS;{3;31MSHy{rBVE*Kq)40R4HyX^;dFLktHiHsx;Z~V-`~J;%qi)A> z&*;tai|zcK%z%)Hna=_to+2eZ-}70LuXz6mP#FRGG?K236)cvZ9>2h_n_ME-zmT4q zUY%;rTF4Atju)L#87WcR^y5F7Gregm@GCWHQ|3dY?^flHw+iV2UcS)a@2joDl^)xU zUcWDY-ubW7#wTdC^!KIA^rlXcf4eEYLiau^T#;=!@bqI`odt>*X?+ zHmrOj$BO>#S03GvC_0{B{Xlipw2W%(GjfW8^t^GR>#ak2Azq0`|5zkRmn z+Kf=byV9&ZC7fnos9mfTy*&I z{?XqD;V*c`*pCJaG_s=it6r^^y1g_|$HW{$dN-H(1`D(rV*acdthC;IxRih5Ca{%K zZw1ccr$WsE*1Q}^hW1T!sK8;;&FTs+9IfCR>-BZ$-yufPnU{QCFWZp>>~KnbnL#Fj zDl!?1Y^0`C)9-R63F1LqZLt#Wr8gN+$-|@@j*7WJQo$0dR5%!oGVCqT>C=_&pMHV0 zHq4suu!JMHo>OW0r-8=PcXiVUlTaHtn6H{;>Txp898&a+h~qXqm8*TQN%*0vE9GKD zpYIR?5K#UaBt0jY7JK)bqB~v9j{wp=WVNJ&A{&zd{#Y&o$cmB1?+(IwC#&LPd1T|W zkA{**v|<&Pla;ANHU%Ra7wvz#5_zvRzIYkgFT?At2)77SfktUD67cIf^==g0&N5kN z($D~EtgP^VG%m6vftOroOJ&J;XVV7*y@0^(Gl>Lw7ay&*Ye2>`rDS+CvTMqk5x*QFUl$KkS@^T)5-O&;!N zN*L~3*PCxQrS_E7eWS6^(Dox(Uk?VbGqNyU99vu{lP` zNw{(z3ixVu|A}$4Xgwo;@5dz6fW3lAW;N9r9+&r=1;I4E5TgBQul(lGdh%?INhq}Y zx^;g#3dmGz2i70m%wx49fsLM+-B25}+3HFAYEbKCQM=s1HA!QuhDpJb+z;b4n0)|0 zcxuVG^ptjtdmPNL|Ne9Hx+t?82{}>YA;U!raq4SwVPD}_M*m|=s<&ZA3Dv(b(Hb}G zUjp2cJzZ!VtA|fa%rkd(>A&7HaC5io!~lfRVRgO@YCF&+!yMopfLJgH&n3IJzxC{V z8>*erxjF!6Bf&1?le}mneApp?-{S7|<3UoSuB>+IzG2C}(E~MY(-33xKdiH7L6Y;t zD0n|>anUPn^I^X~{tBp6Z8#I>X_H}D*i=#5LyFzw*YIYmOryrK9 z*KApBd3V=e8PSe-G5-y|Z~Q#VJ8945?b4XlW01OBn%&Q>j+@*FH~dRofXZ>0Oy0Fy z(zU_r7=n9E8=l2yS10_@dk!43V&E@-$CH?4Zvg!kwoBcggb;3%%x>tFJ=7ksY^m?$MZHWm5%anCS3}Lz&jD?* zXDiOtf731zdSO>v;HW>L77MGS#_L66Tv@0})o1VS_59EydA~R>ku@$9e*Skt{3=NN zRjsXc;<*6D3*t>LhnCJy)IxLO9*O5Zx}bjcdeH?Iv7}Ik3lmpZJZYkyMWU}uFHq|^ zhGkC+GClyiER|Wh!Dz>%OY}uuAV`E7L}uZ$8k7ojOb?`=rWuh*~MxDDd z;+QkSZbpV_QYOr1fk;O*b@N`c^JGq-!!E#%J8Flxh9_B8Crk(?+RG-|lat5<$D_}$ zfSeEcmo5Cm6U<5=IuxfE+7vW4Ubi~Iis596moa6=>yj^+uzdh7w`E^)dX)e(p*mGZ z!=F|_q8P9m6D*lj;Y*utiJ)LsWWhC*{lMy%s z3+PM&qi6s`1uz3}9a2I>V@fIw?nX~gN-e(^Ju|`5d1Kc(gVfnl0YTz>Q%WE=0fZw# zrS%Y$o(HpLf_&(#f{l=8B4UY0BQr}G2IFX|6qY*rp2kOV%a~_vT%noc6)cXm()rSY z????o+d}&cFC6|Ayr6CmV9X)&!1r^XAiUI> zS=zT*+8Z{&DID?#4G7@n1ZCqL$ca8!7_S!C^bk4-2h!*ei+N~YaIAb#gRgtCAT!p= z9LiID!h;0Tc2CfPmp{FCac{(9?K&$ghPHnxZZ*_(J!4LP;UvFg?AOn!NDTFFQIbP; zY!|%Y)L}dA$Pqavjs!f%L_9MnAlt*%cO)OFpbvl1fFYaW@hK~rkxtA^;N6WciT2D1 zj{Q$5u_zk;tgS$3LX&K3xG@h$tuO3HgW&ute4rHGb@3X{xD@{7lxmbv-YtN?ff7SQ zC@pW|m2Ww*-^i!!o-2>xDXp3D1_{RieE56cA|y)c{Z>i(zA)w?;|ucm6}0=dDhJF zo1>1G>sOAReUx-tcNilzR;UD|(Wp^l>0!2C>pGCeKfjvNn0A9KbRUqq|2uiXa1{ke?Ox>Ox zty?lPPww5w^>b&@Z%atjT9-QBFTU8ylJ=eb?)(u$?J0sgQ9s{r{ABzmHV*gTYhoZU z&gCSVGPC-ryEv)CB&e%>lg_(y`~w2kVSyT#qNwW+CaX3dZdoS=jX9@ZJz9soJb;_e zv$plN-fDTHbr964+8oiCJaQfyHNdL>E=YdK`ykWc74YSnkmmQsFPhhb4u`M4h`IHj zmBvoxMX(#?neA1LO>fw(UB9B&K$H*S)ov=!h2dTb#@GLS1rx)5ttKLnL-}i2#RTAd zmCNl~4hIG9dyT&;58ArR-|#gf)=TK^*WAvlB457V;-CorxF&WY4|?INSXFD#I*fcw ziiamj`f7IR?`2r++2bLC>s0t86;lFI!O_oPv}S(y+(iV-*|h`_l0<1#FpYY=#S+4U z)NfHmaY@R2>LAU%Q@5TT+uv$WZ`@9R8Sj;%&>7hks8vLurN#8f+Jlci2^iXzWbpuLWytyWPO6lKhGlB zUL#6?d4_jmqG>nH_tPn?*6=v)p;$e4KLeeHO!LGO6u5^u{G6-*jenHv#4t&j(5s46 z^gN4c$^IG*(05Fa7Do72LpVzobqVp;lv2)-{`*}5<-_v}w!H$w;A6@3zgK1Bj<*KsN9G~9Cw?@3fQN;xh5EC+YFi75p(VSKRdaqT z=#Zb{{sQ60(PGiVvax!ORWd$7R5*@;>x_f3i(6My#JZC(Ck(mU z1w_UaB^N9?m@RErYW*-+YUqxVTUWZ=b?K3Om1pm0c(yly=38-qqFN_Z2xS`D9+z%M z-o=iZH{O#D&1diyqFUB-nKcA~VJMr?J72-4fL^7&VUwkM9x#@mSY-KLdbT6fDo~z> zp!^A-2ya}IOnFCm5ymcI`zTyxsW0<{QKo`GX&y8^pJ$S>>5u>_1{vc58mA5YubSho zEt}ii3vqLr@mMCmB^ZNdj{D!Gm0_Hw!5uL?G;zjbKq(*LUDd-PVMB!O{OcSzgO08D z7^>8dc((MMS5fEJ$;f%xoz=Hxb`3H5zYliKw0s`@d_Ny79j?F05pT-wePnE#gOOIx zqZqOWJ-rT#J=v(jJa?G|$k0u}k|C%1#4ZS(b4Bc1%Uk9hw?h4XJ-7*0C&8f}(`R1moiNbb@?EmRrz6py^y=yq4Cvoof z*+Hq>&P?}v6?d;+n_6+Xc!wsX0g73G2D`$hR*9c3so9$fg`UkT_;2xERdLkbPgl)- z`2Jfpe_=cWB*O25)h|C%DX(xwT5OQY5hL$2Mbpo6njyl}-pdq+zpMV>7K=75uBs}{ zuc@hRW|J!eacgG?aZu=9#98Y|QB7hG#Sn8@>!bgzx!wr-aG?d+N6ka0rXM|?L7R}W zY`{8B^=ls2SI@rv%>UU{9d@2nI+N{O-@aBlO?tw@<5w5eSve$HIPce!Wd^>@*YNW9 z-t?D;MlT}inr*+Fca2J7rjKJjGJI=0*^PvodT!Z#_kFsr(6QYkbSB|KQ*f$`Qolrw zOY@*~bj0@H-^i0S87wQ$nuk@#Tpo}7eRCz?MW|)SPbZvDjGt=LZ70@h1R*KZ?l?o2R^IN<`^#5wuNM~)$d*65_4@!w~SsoeB`eS^k2T~RouRsxN@#_ zs9fWD`+8Z<8_oIoL~HtJ#pAh-P1}?IP7xzsxjx(0Ih7MdX+5s@79Pa(JlpNQ-=8i@ z47uZtjdU^B^9nf`uryn%>-OTi2FI8Q2m&yofmsNN$kpR=b8@4vGAQEb=HpMCzH`?N z2*B6>+&ZPUKX6C2c`SW;btJC6sJaWG5|;U1fvDRzmA+nD$*yFu8ZvQr>25z(%?NBU zS7Sa2G5(6nmc0=;vWZ-JP{e9AaN0A#JzgN!4YXz!3kJJGiXw06>e<8M!@-EFx+ezc zy#`$1tY{nBrys6)#8?s)EN4yxu~L#yFOmh_CS!@mkuO;T@i6PJ_x^FNGjMTDkpuX7 z7S>(EpK=`Ra`RwOXmDpJbPKn`fTlXq zb4{D`_nE!iG37%MIFX|;Y3VF5&ptAU^Z)&D^YHXGh+t`O@sb*pC75n3rSuMwF^h`) zq~o-X^Bele@*ob1A_&RJ(YZ+sYVn{u_AHE-GgnN|qtr6azXU9~TPiS4gP`4yi>oCI zup|6Hr)c>-nt$^=YmqEQ?ey{V{S)98=(zmq$p8!Cz4Gz+PLZr{i998=f{5Hsd;jn^ zk-X(BwRwHFCm-|zC>miyAydyEi1*ci2>w5=#+DR!3R)#XrV&x|gDAT{s_dk*yzDC3 zFO}y`Ra9&aOG4;i$V(S4GlEzZ=r9f~ z8GH7r9z^M3N<7=-e5THmdG_o!;}l1^6uhiDV1|aUX~hcj8A||NBoM#+H5fNJ5vSDl zy1XQdcZwcbxWVj}#&?1_>&{`)Y26NB#uyLO6m!+1)_LLMBc(3uifo=E5!C@Lx+Mmh z1Mzs^JCTL`FzN0vT3vH?!arI6cuZ%wIv&*s%4o!apE9X1UH{#Lkms@Yy*>O-2Rm1A zwwuV0l4Ao8WqG2;sqnUiDnAP8xHKxp#v)dZdvAz6c^V>g(L42az7CbHPS98!nWcpVgQ?)J(QpN5ej!fJ-AO#heb#vQSOsxS!8)R?hdmgd4(5_~N8S}1=OO1#!73)N%N?hg z#abp>LA2SDp~2JTL2*1D@IW95(rW<2y}hyXSjGOFL+6OnVE$=gwYYixKatXT7RF^D zRXBBxWGUw?Jm55iXMcMBb`{ITG{+w#UL~Uo@XCcMk9!}bLdZ=FDMK^l6IJ@h(;&_M ztWs1xRC&ooDl52xQ!y6{tT!(R)b9F;v-muidX2r2=)w9KJ7_2DEyaddt#Oz$eSz0ysBVf z>jR$soCay&wVHS=SdUEtR=RH^%DU*5XuZ^1=MjgLw@)!SlXwzw@L$036@y0}Avc?D zG-&ZlrATFOU{kbXhR!j3X(t=^AjpFw+I|B-R}&=iP2*-F5-z|>6rlpLO@3TSJmWan zi~@%mkoiWBE`?FS6A^_#Mo3RAA#4k({xTx&O6IqZmTik>oBZQ(!|Gy*7zS6~*9TmM z$6c&gTR_z2J8ydlm-_y*$FzV}4o>}Vlz5oPP9s(mDhCscF@~X z8K+n@t<5a%xx_gCmS_1-{$t$A9r5o{i$@85KYUY}r_LNz%wG67Dq8T-Lb>$k+TB3G zO>;5WLLKq#RmS6tY==Ge(h7a)Hk_Bk-6!#z(Ydu({$Jt6g^Ho#NV6oIrHBX59IE~H{XZEc?1Lpdkqdlx ztWhm|{MjA?ro&jaB7vF(!M-7AD(Q%?*+clX*y8|?_gbHX7d=GlQ~6smM1L)u_>2&A zLgP3+#s1NR;%r$27sXGNK38y?r@Kp7E=uOJp3vcx^7NGcn24KuARW0VGYSXIg9`z{a1@*L=AwZ5lE!l0j>ILEr?3xp!CLNNN+`X4) zk5>fAWAD`PB>`~s0-Jjq4%>^&zJusMZ}r~ZedPJ)g}1|<9xS$u5a4a2VTeaFkZNE) zeyj>FRY$%@CUI2uw!?3+9(nl6remo?eua4mmADR3a=d&dGU>QF1y@MG<8n?(d5DwG zAeKQ_r22^>#fv=sxFQD6SB%Ww;zjXQ;;^_n8TrrL9_1!TsJP$Xv-+SYSS-dGPbLR6h(8D17iI<=-{){t3 z@KXA0Jy`ed^TnqAkNVI&31?Nn(vGacE{0O$AT~#^P9wm%RcPNV@vF^CCYbEmx2*j+ zmX^7GJjSG7UQG~Yh^UEGz~Qm{<_-2)1VT48ohq^o(oHnS(1{=i8e$Sq_O5Q{5ja{B z_Vc&xM#x@5Jpt#yEKgR;9yRoE13S~HN3MZ))mZTOnFQ5Z`J^-#Z!%+jpQWxn9&x)ldgUc& zQB`y8%U`vMVr#npHW@YJ2Ao@+HQc4*TV1EKt3;){4Y!V<0VhlYdQX#%8l3|Ae%U?t z59n{PYl#dPxbwE>pmrcXU}Jc5u;$VB{O0RBp4uoQ{J0T*rVAes&$3rL6yvEqZlpgD z_x2#*?Q27oo=V=zla+F%Z!8ye4g-Kcj}+hu3WPx5a7*LtRENN$QQJigV0&yLek?|j zIM6k6`hJ(cqQt4X54MW$Pj3?rA07Pk;x@GtXWN<& ze|f0T0_XM=i9n#r^N${Fu)8A0H>Y5$?g|V4#OCCKrj-&DnB5IK{3!S++GW-;394Sw zxhQ5VPdl?pd9?CxVZ<rDKh37U>3FyYt;`cBRBS7%t^H{G=aI8x%;f5iW0U)(H#S<780`7Vw zf-rhPfc06EOXC;d@#plqe>4}Ri(>}Gznqu@zn?Ht88;e%LG+EU0w^QkLpT0U17QXn zDxQFpoD4GOJGoq4iJKI!&X%MY{uSqcqfJz+cDYEhcSoQ1P|t6D2d$k=Ke0Zw zV>fzmRk$Pmt*zp?VZ%bbkqXp^_=ht7PtouRgP_-)_Wj*vg)lXA>Y8~Xah56G!$3Vh zkK70OBl;a_{CB6moPF*{-TrcFuG1~V2E8VB?uO!cOyJQ0i~u@e3~9J;2z3p{#}dlH zCQSqyjF69p`fq3P!_P{VjB=l2$=q-(OZkFr%Mq2xT=|Ou?mQBRS~yCoBX~o`d5yqU z4aCDOh;q;1J#&;XkyoTsRx3H3*Qvi9IJpccLW!c!J1@@UC-FnYy*W4oy5ux%Eo>I5 zESm*=l*jMHE1=@{??Cop1Z}-9I_ZIUR=m;hSDDV+tjT=*Wt<^QKyZMnJRZx=1gZ$t zgxpnoM;7&I$bx$22Pi>FFrnpNGf#FP;2jLmy5DNljc0Ce2?o8F{sbGd-|grQ!V#qk z_sl~b=|qS^A;H$3MGTn`xNLNXg-W;pK04`Gu=aXnx$!a4cu!Yo!3A2k9T&LG7APzO zRlqu=3U{Vnb|0^x$)Q~x0$!_)-ETkIbST086Se zwThD%Md*oy$63=~Iyu!`EOt^X637-0M(32eYfKd$+Pl>g&IXTbY%l!V(vTQi15YuOOSN^KwELl_~ zcHDIXU8grIzcTrw-gz#G)ypbhGze5-#=9D2w+6&ztb?ZATZA!*2E*!*n}H)v+urQ6 z&uNzK*d&qS13{lqP)xi5N{0N2Gj=v)I8arb*QsQjzmI(LsUPx3FuRQ#^gnym|3XeW zP|mc5IRAJ3LeDj~A7QLEYtYty%75V>P+eO0b<=)C7^MHVYhn_T5Losj${;;V#@Z~e zC8GOBv@R0fnG(G8-_0+`@VB(6-#@6wDCjwGtVFobYsFq^Ep%xqq7ogVdAJLO0%AX* zENsWO3JUyyAz%wCf&lO%0Me5xwRyiY9?m82GSK*-I~glsS!mu=YLfbl`uBS4=Tfs< ztv&B@92F+J0ogM(E`!Y#Lw8SlyxJY2g9t?h&|g-o zKF#9OQg@ICbv`}rE05$82Ajt#ZCpEp7A-Ya7;#o(HJ1uhPZZi>Dthsq;%4{AyvGUT zh7Coc8Eh+^c~?qzxl(3c%Z4B|7aKkPNsmSe-}AVb>)P%A()zr#O-qSUT8rFpue@h| z?qs;7{nd9j_n(OG|3G~d7Z}NJ;}b>{4oZDwN!RM z@oG>nDxxP{>9V6PhC$4!)vF7;l=*TS6u=a7E^h{LDtyQ;g zfS`sm0><2!xu>mgwmZY$Ou2t8Fi;8>?LKF3n1AoIJ_k4Pj3?^sUEsFYQya57YqMd7 zJM{@akfnP~>nS#gH&$EtqHvJoYs-fD1u@H^to79b^n)=&#RHUwnE}z9ObR^|;VvWQ z4$$8U*zaHOLx)Y~7|Co&uDBL|c_(3qxzlrDA^PX`54)Rx|Hdz-*>K#RHAjgiWFns` z7%v!u{)~;B6yQAa6;76a`15r>c!XQL@Oi3iF5VWQ&Q4VrXI2q!zHt=6?6qML*s7R# z3`)9?aH81gh3RtPU8RkOm4z=4-iqs*|FF~XWm2K|$$vSPSe0=s*d6gx+TODBsgCpo zx7!=HEn63}eDa%ICVU#;&mMXUnFyA?YX?9+}W%1>+*UakBY{W<;Io2fST?c|-e zN3JKhh|_BgNB=ZS_9(X7L%ue><;}!~r7!C1C_wIvVbrCpxz9bM( z;tfaZ3%7!YcNG+d*?fceUkM)!X&tdWVE-RaXBidM8}QpHdVrasQ<|X%7&;s}qyHU) z9-DsV(o@Pq(e)~uQ$R4`VI0c?e@>(|Uka$0k5|~hpjlj!SK?@u#R{QIMh>8Vu02Zf zii(NojlhyUCi)^r$29z-T=P_U=&GhRy{s-HV zck!diLYM@y-p<)_UsZnxwEoRz( zi%QdN53}q%e8JF2qS!B{5TyQTlP4%}1F-X@TS(?^_Zbg8zxiGQbOw3rjGxJHi<90X zoo)*qDN~qtZ5J1v&AVE)GWI!($Vc^K6wAsu+x4pyTHI}@9P8B+NE(3fG)m6m3G%Nz z^Tq+@9HkY5EHkK6%G~jLQ5UU)(0?m0 zUJ88Ru_^d*liP*r<3x-azXlA-=afp%5jkYz8Bv<*NQJn=LRT;~7pa?GND~ zXQLWE{2X;h%U5~^N68G=*4FaQvm16#B0er+yB}pPsNCF&wA#Y+!G*PrM$gN26%59A zbff{r;z*V9!rG|5mN7n$nPzLp^L0iJ@ z5~)HOAhV)6>XZ*}L%~)$&kuin+DfBC&ymO?-kWkklTxj!Q^a3gNSC^I&8_0n#WZi9 zU?v8vjT9;kT@_#97o{WFk;bOIx(&4X{ZMPj3e~?uNvTwAdZ)7pmSvlvrkT%F0TXX* z#bZIEJESI+$Mt3o|MbFEFyOSm{5Rsx{4>}s9{fos`)^%fmf`vDX|G2dPts3*yD^qi z`B;fSAy?fpv6J$WS#a}juYL6MblR5;pMK=&coyIAsDNN*6#o7YSC=WCa9uH zStJfaSr?3<&7*nmUF?HHJV{M2PnAXXQ?H9T_&9{X>nSKNXO_e>MZxCvr$f^Ye%x{6 zwzNUSMLV~VHg5I+H`+}ZMOcp`raB+T9@zb`pZYDw&*+oEMGzl%*xYP4SkP@RQxWev zS>SV9kW8a{9Qfl#!`gQ_sZxoRn$_ensm5pJw3Br`w;I_pd>(Tbt>5a=YMoJwXJtAU zW!t$r&ikRe`8-{aEbF8!YIpYCgR8#^M>ha5vLp~UL*5)%>}2xh-}@WKYd6WBgWTXO z^Duaz2CQH4?=St^dV#Mk4f_Jz!HRh9G)=KN%{06zy46%zLrt?nXzBz}6=2wRato-B zM|M8d3D90aL+|#Brnx@5bPfIZ+aP}7nRQl#G9~{`5P|AcS{D-ptjf42UjRHrl$< z3%tP>9TOYz?Sxw5EaDVFaLqN*b~Du5!e$_GCv%#o0$Tk<)4a}DESWoSDyqaX@`I(B z5Hha#>=|6ne7QxKEEkuy8M*BgbH##pF^_7i3KVfRjr(B1H;qrQ5t5I(^&iHp3=Q+v zw|hhR{M#w)qm6!W7N-0#lFv=-wtiG}NYYYJINTgD!5WDrqwvHbf^monB4P*!otT26 z7y;A76#d-TH)qyIjCbx{H=oyI=GQ1&%MgSZ#91!l>mjY8TQVcwrlG^8YSSi_iQ|RU z-GsEeqK$VEO=&NgY$g3+<$<=<<)GRux`I-WvYD(rm@U0Y_6w_%!w@C%s-N=j@ip&3 z`xS@yiZv2OrZF8o9&NLGXXUuWbbmI=XDImo$F}>3BfDim`*mmgt#bRl@AfAoI(-sN z9TB40sLCV^KeI|bUQ2S*kI9?MxVug}Xp@X!a|p>6W^{2F43-n$bmUqH>kfhmxH#_M z9ost`!v-E3z#qzHGs=^z^TQu0!ua0H!&wkwHI#u^MnF9=4``MGJ_7!Onk!5-8R^cKc1ombofAm{9I3}OG5)xJc36J)C&J4i|aZn7y95vx@x|n*A zp|~o9>i`XqL^F|!P^Ze&G)Ua2a^L88*N*Y2?4T^s^{9yU5V|6I)NOb){`PIw^<;?l zbY8O!j+Xn6*`;3>GHC5Job5%=LXp$*km*P?n=O($lP#|+FMd56h@mIO1!{M}mW6zZ z^?Wwoa#vUT>}~iQ3Ozb;d2~kpxU3EN&syR{+LvtDlj8I-%?Do!pdWXs(%vY{nGNoG zH)Er$F!Vd3U^@>u1*Ni$rRjzVxcWy#_?Aio&70M}p*lxt^oFkmK_>NGeT7DBia@`8xmzgnS>y z_Cng3NXed~)(tDF6eb^_Exi-Q#Us-|ME-w3mK-3gO0b6nu(B0MHB#D-F(u5@Jup$5+yV4hcpK{Nx$N~_iEu| zo3Kt(;)llCTtpTrTXdy0Qm568#xyG=j-xh?Dl+w&BbJV8DYEUr$i@GTBv!=6BX{vh z3@JcNOy6OXlUOT*Sf@~6f|?Bjv$IB=XR7l+2at#AVHU->bBj`UDcqooHZXCeG;uYf zIcW}|eApNd>9kAe9&D|L;$JUs>S{(uqf!y2fX00GE((u!i05mi6?(QN#(y{gmTR!E zIC*5Ofg^`4nMjeu0g$_B@-K+-_*1QR1uRfD7iBpr6R{0v;!S1Xt#-`M&Wi6v9yuX3 zh%K+%d}4dfkC^F>gb^BjH2rjPfV%gAuxIA748d@RF#=W3>$%SjMdOI~qq--`U1uEd zOAh$ITG(Ey@H+tQBZTG?ZD&wzzAq*m;3&NBC|XGnXP9+Yfr>nT z3{#tiR2XtkH}_Y)1u2?|qJNk1CBiB#XetstYtm!?RlgZuCrS19qU>Iq{SoV5b^n2o z`pGAKGQjhU;A3|iEHlLg*Q63{u@AyQZ3f0N9Y??Uvkr<&kw{fjdnH0pxb&yKZ(~Mj zJ*_~V9$S?LX}b=ZSyU)OR(5ujR&#QsP&fospvFt#(D)J=0V#U>NUneEv4w49`;B8G zS#98y30ua^PE!0xvRxg*z7UbBGU{a4VbFp;x>Ueth>q~dy*q?XUSg`hA&`M~rh?Dz$Z~#O3gas>Zl+GiQ%yYt;Qx>0)u#Ty zW)jA))*I=25Dq=>-FC*A#Mzz<5Kc3xy?=gKlFj#kk=s@!||tM4cQA@L>K z`SbgywCw+cif^TTq^*B-mC^9V@PldPxX+(Aa(@OSZ!x5{eyaQWsp0RZMy}7Je&(!Y z8VBIW9-5Cpp8v?vZzGYPNnBqB)V>VSex^vkqskih9~xMA_P#D`)411j>|u6p^=0Ad z*TolKhn_ZxD;l$3Jz?|woYehpVB4+*c&}T)3ihdYY47XO!BaK6Or3dJ13DV!`(B@J z{OR~TZc_7i>EGXP0Pf|czatNd=SCx^LB&g;zGdjrGTnQBz%7g9W>=GE zF+-*!O5ZaZzSoRf{QB^{UTrg(hw8-vuVc|>mE@;bMzQjP%{YPWlz*E^g*!Zw&+Ihz znx3w-a4VS>ZFHztcO@HC{9Ak4w>xFBKg~To;PaJ@SJT#td5q=rgZGpQMVxK#_exDR zEc@C&G)T=J?DnXy@AhrbHYjm|Kc5`zi19K4G29gSoFJt;e*Zo_KF}KHR*P5UtSUNK zQ`lylJ_zbt{zs(wLB{a!km}F^w0n#gFq7zKr|l(?=M?{lYx*IZ%4u|mx%rpoFgJiQ zZ*RSCE7)hh@+x^hMR+aEexLHkL5}uMdhTzMEA(kopon2=2b`#+IeQ_B_8pA&Yzc8u zggAxYlgs2}r8tN?*luswY-HRUJvtmd`DJFmo(anXVjf;2Wk8-cK}3Xfi^evw0GLWm zr3unAQ~8pH_zORM{NVJ&%;EK8`->$;AdCv-3qk^@;0S=#7X;%fue#7@@T)E3t36`T z{vH=Msqu$=>S+`-4QcMuhC97z|4}hbXV?gKm*Nh2z*v?tQ+We$Z^2n3UHFeAn2-X# z*>#o}uh!{LXn6X|=EhEl)A`H^3)pr=bjgMa=o} zkN%il5fJNe?xGZ-j7h|_(C32W3|y4hckZ7(OEE()ziI;^@iSDR|GoQ}j)1po=-d8z znc@lHogfo-de?Vet0e#sX9I#LFW>s;)3QtK9nv_uI%pXxZYj0q4AKd5(;4SO+xOQE zv*|j>{w!K-OymXs&x{UkoZsa1cWwLdn^}oY5{Stby)^^S%6lJhVYy>jBU|2u)u-o$ zKhao(U^YRyqYV$Zj&C13m1;T2^igr{`tE*;k*SLEJFee-lj!O2F;?jnT{>@&V*{uB z-r>y9zZ&%@2GaEVVW54#T^M7POQoLJt3+F+)`{tt#;JICXdH9cC^?Eb)a>du5OC75kc_q8l&H1j=E3pY3-Om_&CCPYK zg<@G9NBR+ADEEHE(uES3JOMPaNz3C>vdQe%T~M8i-K*Uenw8W#Qv0RIFN!>|EitX( zyu0pK&3ZFod$@AzJgvybqB^z0+tFO4O0Zz>sBGkIX6l7yC#5vw7?KLDu6V~68Cmp)T4|$6UD^06Gc=2_ zrK#SH`?m{i;th_n99LT>h185*AB$ifwH}#CDG;R8ZV#iaZdt6{LL2Enh?-XkYwr@w zb!hjiEAWn{5-PvLzbmU^dQ(TvQg)KFZ3>+b*kT3)+p_-~EEgmEDr0on^nLL@_D5mlxN|=iX`L(!G~w>KSt7Ui<1G z&H7#kuf-kvNB9|YAMjy&O+k~c%KmVC==Y()h|J6xz1{ik&sp#rDXW zsxhhOyYsx)f@|~k%W?KNsAaQ5fyi`o($?*%enI7Y& z_0dN4g@-gAlKG8)-W~(mTY76m+@mjjW(gY=(ER_O6f$~HTh65h2_NGYG*Vn4hk!$# zihOkS@ESSzu-A3??bpF(f^dbwX5x-Zx?Wvq_5be(HO3e}S$)M9NXIa-EtJQ zC)(5>%O!8A%C!DQVCW&;Z*7?Z)elP#>$lY6fO#CDD@kYY1=k zCv#S%a;K#N)tYlswC9#F`9EWXBYZ&|Sbp9U-{fPPV`@MUKQrN{$u>1>bFaRc7VQy* zOm{ymd)W&90rtJB+ru~jtdwp4Y#_EUgCcmDg7wucy$6U`xO^u+90`NSEujd2#x*6N zz8>qrX*Ua98P{*{EvjP@)ZUAXi!W^y&bH7w65xru<@8Z?=`fOI6(c!5jrXDxp zBE^w$2KI{J$VeG+*Sc$1IXCUuNmQU+B&CXgC4r&!Akb@Q-O9Qt){Gp$tKNyfYpXd_ z9k9j}R&wuMZi%+OMVzu;sZM<#u>=mZ!$g*b{wB=d}6XKtAprS_#49?~h0T z9uk?#`zQ660lcS!OFP)oF-W@w|8Ci^$lWc?CnG-s>suWG4WLBjJsu%SX-z&0WX90? z!4nVv!ZT`HnSR_1Z%=U~6gO?=9nBlj2piW{IZD!>f^!KX2hkD86l$us^$84UGqIvv?kikh|L{#~Gl3e&w#b!R#1*GT)T3Xtv{!48M`wuZ z#1)<St`cr@JU>Br29$;PIs;-#-YZ2$ z<-Nc2MT4S$hUUxMVAeYSw7?(H7H2TzoWA8@v=daC^w{$g z*$JnGQeL=;T^sGzM1bw1tKecETiK7?DVCw>x7#Cwh|hlB@4v=670VDOLW->s!whlP z8TXrw+P`hneVHY`2uyg6p1K&N6!d-I6#xvD$ePHs@&J`{eBWJIYx@7m{ldFHt046E z?uKO^gc!DiqgXdv&D)LRQkbIDjBF%LS2VO)C&m>Rks^?=S3CEBd|v+ga4x{Za*k;` z9Wc)#YaJ%!@+gI-YMQ0f6ZKq6yoB#A^Gd}B;Tvam<)C7uj%Wp=cA#57ck_Yz14LhZ zw07C!-&4sbfP>VD4TM^pcN7`9RntqU$!_=&>Qv>Q=seeDa3YyCueVLNk=Wx1JGxGqK59?y0>&eQ!4o70-1Qt?mY&gA+24`DOrU$MkHlo>Bg zq44AWxV`DCL~VnrP;yU4*-q9vzOf_yf1c_ZFE63+UnZ%aY%MLh{y=1=#xYUt(v~%a%Ssl}NQ)7k8*yVO7Dw!)vuD8M|*n8)Z##KE`_nY{Els zOc^1ZDky(gq5~5({uJNQjPF?Pb&aL-wunGJZCm!{vLqVHm;SrBIq*vUca_e)T;vrSaFuoBSsZ|?#!$?`Ng0;a^q~VGhpoVQ0 zs%bYCK=MlPk^z9f{OZkbGXC#wdEy`<1J}oZZe5mXIr~5-6 zql=9UXkjx$56g|Vs{YA9#_36lOSZ=rxPwdBAp>1hFVyGm(&u~FqWW6jS_@q~#o}bEqc_l>dNS-dE2~K(BLHqH z2v13dRvmRpczEjPI_c#@+0v}`c09|R{aJUbNbCgBWCwf zoH=wl^D=$I6GQ9tBFb(hIb?O)4qM+gn4#?8%ggd-;KKK4bk)5bOk@jiid#`mv9ueE zfsVfzi+R67_1tEtdPISFJ9<2THN!W?iW29(O!b@}S+Y1J^+(@hPGTvfJni$#<<+<= z2m%Uoif3JcIaqW~)m~q_p?r$(^N3tGAJWTR8d?i*42tvE#ENZFb?d3db&Lt+nU&k4 zpcPZ^vr(PMQ3^#K<5`$eHiptLB9?W#NMW#yZ9VPYp27H+chSDX8TyI_)RWm}3n6le%4T?WH{zq%n+#L58AM)o zK>0R9a#Jb3nFeRc3S#BKuZQxoDgxgnP^J1$H~&#t_(U_-B2F@#CI_V{V#PTri{YZh z4Dooz3rZi>2;qHB`h1x7jB>-zZg|pUiVY-(y`tygsC)Qm#H#V)VUHCt>S{_%&;=Vc z#bB0AqBl8=vcj52_C%T0-|T)FZ~JbrGyc=)?J*8`4RxdlQ z{LXkmE=X#W#w?V@v2*Ib+%t{bH@2e%*dr4CGzj1R$u8&YQKXolmcv=Cwn`oChE8! z?~*Dw%|I9Wi$cgNdIEUo({iNia+Ecn_6jS%C}+a6EX^$#Ougo_s5V!bGXFdexj}2B zPwuSiV$`F6KzBE^KN2yN)xy3}a#VyNSvnbd* zfhy|)=vb9oIGa9w+aM5~NEDF&sVz+$1parh-*IW(8N7OAwRpk#Kr8-pok`BR>DL`P zm)vM~3_=o=&Wo2QNGdKTik|ms-#9k~|D^=CC1*U*$O%!fOzhYn)Vb>|1n;(3N9P8x zf+(`7)MeP{*vzt1b+Dy6qfTWs&UbeCJ_b{*$nmm2$SgRgeETv{vJql1VI=9SD&-RJ z_BX;PCUKrV$e~VzA!z6o_E7gZBt^1-q!#wW($wTj<)?#$Z%|Ftb8{<6O%-Pip{PyeJ zkoD@hTl$|@mARQgHLGK8(f1T9jJU^p!=1F)oUGH8kdxd;+vfglZ*>DNkaE;z@%ieIW*zjoI@O-=BwXos6vEk$6 z5_z=Y%e?6)wCOLu`B>LAfP6E+W%HUT436FmQQr*A-VAHseDZcPoZB^QVe{GV&FAD> zVH=w`p{)q{EkeTvUUw_XWh?rhYgEuyZ1&bwob@K5cq`%UR^o|?oU*^Tg?yXbz`N>tX=C@^ zzTFP!gN$VF*_C-El(n}(nVa+0@!`sci-=F0zBNZ`MIjVY$4I7e=XNWVZmD|YrHBM-#_1(0M0AwJj0J*p~FR)24L18>0^<(g{eLON=(-M>13 zcoCzeY`h(?*R~gkQ7nGm@d*IQh3`{8ME z%*TZtMgX}R_t5FJP%oD}2pfR_5Ug;~0U=-}MbH~=??38zI0`SE%FT!JzP7X&FGd<- z$%K~iF3U&mXGc7G*Fb=ad&n=f|F+bJU6_%c$Vt3FK>fZ*{t z*-jZMAa|>}fk1+{oAB1iD31qG(~|qA^N-9P1^rz*Vx~L_D~Mw(Uug?)>ReIdwHpcmx<|Ftb)is0f)F_Zz;TaNU8em3tyA?OR#MGf#g zIx>s}Gy8((=+0xg&pobdYU<~jo8A@48(04hPqSiJV?j13ouuzF zHaWK4mC%^k`ZxgpAis0FZCC!4{jcA4K>zBMnMvjc=d*#X-(T6j&PJtFs+f93Ozx2h zeF|uwuRlc(veO4Ih^$t+4c@0cPd#=^6xL6QV_^T-8Vj8$>sSaAJs|38_TcM_;b9HmbBcl1mub{PE<0tC~TG!A}4A7CvAR0?z0Rdih;P^-k5 za_$zZa3;?-8d-ycCA>i0j-h3zz2=t$k$wZzxe*30a^b(`fI*2}PN0=anObz7#JgFSmG#f=v5a1WIyNW3`KMZe|Lw}8UVLx6 zY`a~85df_uk21&f6I3200*)!=oyOLMvb@qMyEdY2(bZ%o{NhC@Q^Cj9s=r>_BENNZ z0cbr9mu)F_1Hg?mg^BkC-%?hNzf?1XC>kq2P(0cmmslgU(v)T%D@x^6R0;C^%!MX( zYY&eYy>O9@`mLp^kwI|v{Vh+eru3={mUa|ayuLom`Bh=M^kTIcFF>wX7tf-lMMcfT z5U^P>8(`QTpZA|l=X}viTIE~v8X0TJ2x<{0I-V^Tf#B~GZo*B;7~a;gwx65HC~40A zSRsu)RFbOPC26FX^`Th3z?E;+qFCj+BY$G^*6E0qj|%jACzbYNyyVD8997A>3x^f+6(_ZL0Rkz4L z&dciZ^SE8sKGv}tgQp?81ND6s{B4uIKAWLaLzN&jS_YXWV{_2t%fh83I_|_1-N`zB zvUZmxXso3O zg|Afm83zyFX?aPxQh5`s48y%GWQvOI$0~quKq9jPuMa@fjI9aA-V*>;N}!5c zQV>n z1fmyf%xK0b6%x^9i9BM)?#ncfY_wU)6Lft)`9-r=0jPT1-ZH=JryccQi>j`HiVeW7 zuM~LZ2ds-lk<)w*nK;u~7z25wR2WQ73e9v?|pLQ^N#73Lmri)X+L&&3Uucr+UO z@WCM~wdDz7jJ-=|?hN#{W*ze6+EYo6kmBoeeh@LF$gBAMhw&fkg+3uX9NXJZj)AR9 zp;LIoKQXozHNJ646!o~xkxS)8YGtH{w1YLIge^Ip1FrYA$FMw-NuzKOsu+Sr7I}>) z!k|MRt4^+cE=fB$fhjaVw;m5h9ZC&>XlCEiERCLAMlCes{gMfstCS_L+3>8-F1v2a zPW4B~s5UUx>pLsc&4X%O@+)QW#zWAznOz9wSX0R~8h9(r3L#VWFy%QA4RE z5mpEst%aEKkMkZ}(I)-JdJfJb*!-(!DepH7T-iW2F;jiN1Kia#IjN_Y{uE76cK23Zd(#cC;A5+tBJWY)`n^CFd&2bAgtr9+lwGPTHy2ojcH zg;;__rWRFt)L9onVvZ^R zB87Q05Mhv012FBU0R|auIdOtSEa4_fn@Jb@@C^j-wIcO`s2>1kC0{<^a-jdGsf<~X zs?$QTX6RT_ML0n4P8l2~TEE)}68Z#*2`#P5WqKC^o8&iTOD!eHpge1AR3m_>xW`WY3Fn)~6AOT-;Pp?fHmQ!9ad>^0F~<~c&v%)wAG-b>E3$( zq|0cyd*MAtcn7at_8KjCK;O*MLxFgaGzq*8MM-5Wdo1Q)6EIam!97E-#hLN+iU! z8UIB{3^~hpu8K2iDODMuw)!PdR|PQvA2C?1ZYly*T?+pmP~1sSs1fiIkW%^vfUu0P z*>Og=+@RNWkcVKv&3pxQqsm(Y3J{F+I!Qhp7v)8zF`@<4#;YWzQfF!?qiKli7-3{0 zf|2rbF|}~|ca!5XTFHk>`a?UZ>+(=7X;nc@ScwK@toKHl345vWgw{P~hXbV{`7a;@ z;~+&Rf4J?N;GiMZ+(D_HRjCP0?L$}Tx8TD%Lazw%OPX}3@3LyhH2J7B?rl+kmP8psTF2dT>zkX0`-D+-PBV9I z^!CqD;XH22oT6&Er0V>3o&5G{c%Dch^>D#^_q4I<7umWmR%|o6osl$5x2{ zAUR9@w7wV+lmrj}P<+AhV?;!LReN=c6xc#I69J_%q7WWZ2?zsB1aLC$D6hC1Pb>le zBmfdaIdVOICP7f507VikwI(xv$LI^Gf(`%-0e~Yh1AkjJ1f)i_gwrY78 zyliH^imYJXtq}$E71ctWNKiWf#HqG0O508%N`;ZesLty4mkjY~WJ5cI)2rtCG4OIN z2!;cNkf1iZ(8XN0h+x{>90Shr^LM+LEsrJ;0Bp23 zgdhP>fHqstb`YlCUc1?=#CTDQmNgMqR)?V;dKR#$Px4Z(*JjZkno|aGj&M6HLMWVe z-!JV^ATdK@fX4T@PG&I*J8cQXr_eUaTk$NyBo126qC`gUfG)n(`3+X)%KcF|udI^XpLQ_#tGerZ5d2$jL>59xWe5Yb{ zZ%C#Fq>8J(@NBmriPJ&kJ=QdfK4e4a;H)3B?LJ<*Y0}!+S=c5;l}n~={8PqV2z5&`WYtP|Int& zIhV}ckKtN3VZ|CQSS2%WPL5r{H?Lli4k*dsl9K?`;0-Z^FXhmWjupGL}GwnL==9$%!`#mgMJ8bTI6R~*2FFq$=})JeK@r6 zNsN24={*R;z3cZb2%~dY{s1JDtpSc@GJ${lF?;Dd9%q9NkEg|^la=bN6k;fS5Bs9b zXX%Ef12p2l#I2wSWG^SO!l$Z85&a|?!;xdE6z+wFt48B3kCqD=u2(;#!~h@2fip~z zu}LoO?UeHRQmp!*53I*^N{?@}f*Dc%g4%~}7$@g4)67or#3(14Gl@n&g1c&juMjss zWNOs+<;2A9O$gtj4Vt>KRN?6>6;tm`a-76}(x$3=4G4hA8oD%F43sMNCrG?QM0oct zhAr{F@=#9nCCTu@w@BPcv}cOSmqmkB;-PH-B@YKX1pkRjTXYTs1vEC6Ph`oJEzjVn zv>Pb$CPOUaK08aY{}%wT^9GD88HrJb;4o@GJxtc5O?PEUGmWtV+Ld(&n8+y%An0M9 z`K8d1EUX=aZfAPqCH^0p`hyI=euV002&d&0rhPR4co^7q5zu6TdmT2U?36=O64=A|E8?Iqq+#62Wcv#&lv3hVk*cQ1 zKl$nLU!dnbZ;8PmQ>X^#=f`dwxH=&W{e%`}@Gizc!FL$V@!?kzB#L7>u;$tCi-?-4 z_~2#}dAo5rKZ-ZOJT)aXJ3!SPu8~uH_-Tu%=XGt{CF&T;yr5*RLuFeunABu%YdyjpF&5ru^*}m0IOoa zdl>dP&5*|-@dT;ZKSQE04KNx2=Ik!H5ym)|Qy{EO%n1RsO6N>Qav{S?q%KMxVAxSh zrKV+h|Ft|-P%>`pFLv!NamBEgX)xO1N`fycS+vU-Ia+{m86wVk{F5|?1f z1rtowet(ge5q1kpU<@Z&sQ}+qhLJn{E$(iN2uguTAMrMC0)~|33PT_CAd68$TynBGzdj5CkQ=_5%t@8WqXrgKl!iP2zAj8=l zYoeYZ77hM7KmoxsTK*m94+8@<*cx98hJ;JhHQXowSPdl1IP>2^q;h6za<&3c`d|9` zH=CZn-Yw{f(kGj4OLon#i3mXeD=A^!-1w!ROa12yN6+2t4p+jz3;XK!wc0W~yy~$j2x;m%O z_@##;-nuf1Q}2mQzR`LFOPiFc{Ki~YhO6nzn|N|2<;<-~fKJ*Dv@z3tw&FffH;es$ z(mJdL{@qzuyVGTw1#+H0H5OtVEmHN5NW>K_GU`1Wr>X6`rZz~JcP0xzbsamuY@F)fAIHDLzCupw_74-Q`P!`PH(k%)`3tn5->rAUad;~;j7-| zfo@2*d>Jyjh)VdfA`!~yOBowTxi>YK*mD6>1=B-GqGVm{j|%P)J?qIto~1_M=`R_k z-*Wg}4JB%yR7HT57z@~6bvZ_`)$%MjU1Fn67R-^u!o`Wrm0Ssb#L+wifmdwUb~fF3 z`o5v!#76X(@tExNP}}qA!}&{k{{PI%uPNzCET%M zbJkT)4qX8)JpQ;*;P?LSW`TtL&K>Vy?uLhX&XUA*Vw#EVcKPFjE|-do-RP}q8XWmf zZPR;O&q^D0laKXzqK+QX6#6{)r!0~nvtVYw;yl+*HW2wxV?a!nNgesUAHBERJ1zS1 z(!;y}utkR$8gbv;+!?IH1cd3?)KS%D-Kx@w14hFSF9axAZ2&K_P$tM?s0)ZFl+0@6)}J0gWOab-#ZBJ4DylZj ze$?t5jbn4-0c)C(hB;Ba%)ZIYk2<-~%)DTiMgqABN=KxqiSnaC1OwNo9p4xWz~&lS zOtg9PBg8D;0XCq#l zV6~p`+K>s_PEl2a&;>}gI&O6{1BmWPZopJcx{BEdz=&5Ea&ZVL0FOa5YCQ2NCBkEn z{eaF=I+>9Iv>`TTAQJ%X{B!@XfmOfv+#5afi37l4jlTzRNb5+w!c8Fb*?0W-08_*p zbgjbu3f$A)kHZPjT)sr+Jzl_e)qR=^j1Dt(dX|mY3#-eH4u18i)DJO2q>=l2t%*MDh}E!9n>8Zq;3nQ=Y&ZiaB}@Z@6YHUn(4)4~4xJ};LVpd7jm=ptpl$)dL>%@X z2`HJ8RPV#;bo~c+A(nS5j6)P+*&Vcn11!>Wc^Yc&?f1O>ZCPLRrpZF~z+K-WH^AM9 z*B`$>$b7m0m1RH{G&bEmBCRYf$$jxn$ZSHRGqZxCl_9Y7;BKnM%f93G7w+5T+&^pS zqz#S8*G$KwB^Y?>^etaUEP|DzV7wP2;?D-0^a~b5+zM!OK9>bISKbja zgY?*JX2c5?w|IG;7A?K~^r_(3V-49eEgD-KhU9i5wfB0jl1>!(@yI%;+Y3$sn^3W{!h2eOkU1ZBdP znY?;y9!)=YqLK~ue+&-yQx2`^*_~SMF}!-*lNT%LZFK1977iUhNyrT~f(V9nA><@y z=P{h}GW>bge?KKT^-AktJ!G&I$^V{Cd}01>b=#NKO|r_9UR`X&eClJ4);1Mu^6pvjrXleo zH19{3j@RyTW~8^iDULp*#6^JWwiT$BCw|y$m(r}|O4k+62)xZYsaN;Zn~P#i>bS7_ z)$CDAq-bXYv#S;q-P6BcG+#uCs%nXyj0(J(G-v~uL{JG#HfVUv)8Tr(QcpDkG5WJL zV`=fGHvqyrLEd!Spq_D23b&j&LsrVf`A#S7?ZRdZ^te;!oZ7>Tu)c@46Da3f7*Q=-*U^tY6mlKKuE8GuM6ERxryh&7HoSPU z^DZuJ!CxB#pwLnG4%i@T@#nm6xySaQ8<9&ZWh&^CK6i~Rx_BpbN?jqfGsR7oBLz(g z3ry_&2Qp7I$Wg0k)1YT;><>>r$<#TGo%jT8tpCjG0&+OM*%<^zfjl=Lyam3jITiFR zp79bk{zTf3qoI9s8;1&+X%b!N1EH6m6v6&UT6iO7;S31xS6p=TIXmIug7$d>N6UU+ zw+r52v9ohP)syo#b)p<0TryuH)ug|L*KHR@3qj~>Sp+_s*VL94`w%l)g$gX2Q(1e| z`mOxDNEqvN`U$J0mqbMa0|MokDB3)R?}bRJOVJukSl>igoq2 zY*3;W*|h{ILq{}Ah=0_z@itw{%2=VWLHhiy%KI(e!ovz%`-my&Vwz!n%=cf zB_rBc%$wx!CVgFYn0jrX{#K%k$IOk0n%s!ke`gceeEw+{y%;~akR!%nd`c6Q-25P! z;bMc9otwPK*G^%gjnR$4p%&USNeh|aaF&=SNo6FoIWaNa=`X4bb;dxS;wYPE+NEwa z=43Jq;Tgv810C+HjhT$ql2i4WU0DGgUvKf|!CwyVbEXGy){wD$I5ih|2^FQm&zf=c zRIF62ZWHRxHD6ZY-TrV

    kT?M0~?O_ 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

    8-V*=?1m@EXghhmGkn7H=yM74n~X*-?Um%9-`+ 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%0DMbU==`G(xLePN@zg(@KqGQpJ(ZccZa3_ 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�C4HFa2eclnQ(Ah0Jy)1EQfn; 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κe-^O%k$ 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

    6>3hHcb^TQ*Z)GA3>EVPN*x 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 Date: Mon, 19 Feb 2024 23:06:38 +0100 Subject: [PATCH 44/64] Dump private keys in non-prod (#2501) * init * fix compile * add command * refinement * fix clippy * remove comma --- bitacross-worker/Cargo.lock | 2 + .../core-primitives/enclave-api/Cargo.toml | 1 + .../enclave-api/ffi/src/lib.rs | 14 +++++ .../enclave-api/src/enclave_base.rs | 49 ++++++++++++++- .../core-primitives/sgx/crypto/src/ecdsa.rs | 10 +++ .../core-primitives/sgx/crypto/src/schnorr.rs | 11 ++++ bitacross-worker/enclave-runtime/Enclave.edl | 6 ++ bitacross-worker/enclave-runtime/src/lib.rs | 63 ++++++++++++++++++- bitacross-worker/service/Cargo.toml | 1 + bitacross-worker/service/src/cli.yml | 2 + bitacross-worker/service/src/main_impl.rs | 10 +++ .../src/tests/mocks/enclave_api_mock.rs | 9 +++ 12 files changed, 176 insertions(+), 2 deletions(-) diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 4c7f382cc3..be6734a409 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -773,6 +773,7 @@ dependencies = [ "itp-enclave-metrics", "itp-node-api", "itp-settings", + "itp-sgx-crypto", "itp-storage", "itp-types", "itp-utils", @@ -5083,6 +5084,7 @@ dependencies = [ "itc-parentchain", "itp-enclave-api-ffi", "itp-settings", + "itp-sgx-crypto", "itp-storage", "itp-types", "log 0.4.20", diff --git a/bitacross-worker/core-primitives/enclave-api/Cargo.toml b/bitacross-worker/core-primitives/enclave-api/Cargo.toml index b0541b1136..afc82899b8 100644 --- a/bitacross-worker/core-primitives/enclave-api/Cargo.toml +++ b/bitacross-worker/core-primitives/enclave-api/Cargo.toml @@ -22,6 +22,7 @@ sp-runtime = { git = "https://github.com/paritytech/substrate.git", branch = "po itc-parentchain = { path = "../../core/parentchain/parentchain-crate" } itp-enclave-api-ffi = { path = "ffi" } itp-settings = { path = "../settings" } +itp-sgx-crypto = { path = "../sgx/crypto" } itp-storage = { path = "../storage" } itp-types = { path = "../types" } diff --git a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs index dc4469f198..3a3ac922d8 100644 --- a/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs +++ b/bitacross-worker/core-primitives/enclave-api/ffi/src/lib.rs @@ -121,6 +121,20 @@ extern "C" { pubkey_size: u32, ) -> sgx_status_t; + pub fn get_bitcoin_wallet_pair( + eid: sgx_enclave_id_t, + retval: *mut sgx_status_t, + pair: *mut u8, + pair_size: u32, + ) -> sgx_status_t; + + pub fn get_ethereum_wallet_pair( + eid: sgx_enclave_id_t, + retval: *mut sgx_status_t, + pair: *mut u8, + pair_size: u32, + ) -> sgx_status_t; + pub fn get_mrenclave( eid: sgx_enclave_id_t, retval: *mut sgx_status_t, diff --git a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs index 8cc9d6c42a..870e61f01a 100644 --- a/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs +++ b/bitacross-worker/core-primitives/enclave-api/src/enclave_base.rs @@ -20,6 +20,7 @@ use crate::EnclaveResult; use codec::Decode; use core::fmt::Debug; use itc_parentchain::primitives::{ParentchainId, ParentchainInitParams}; +use itp_sgx_crypto::{ecdsa, schnorr}; use itp_types::ShardIdentifier; use pallet_teebag::EnclaveFingerprint; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; @@ -72,6 +73,12 @@ pub trait EnclaveBase: Send + Sync + 'static { /// retrieve vault account from shard state fn get_ecc_vault_pubkey(&self, shard: &ShardIdentifier) -> EnclaveResult; + /// retrieve the btc wallet key pair, only works in non-prod + fn get_bitcoin_wallet_pair(&self) -> EnclaveResult; + + /// retrieve the eth wallet key pair, only works in non-prod + fn get_ethereum_wallet_pair(&self) -> EnclaveResult; + fn get_fingerprint(&self) -> EnclaveResult; // litentry @@ -84,7 +91,7 @@ pub trait EnclaveBase: Send + Sync + 'static { /// EnclaveApi implementation for Enclave struct #[cfg(feature = "implement-ffi")] mod impl_ffi { - use super::EnclaveBase; + use super::{ecdsa, schnorr, EnclaveBase}; use crate::{error::Error, Enclave, EnclaveResult}; use codec::{Decode, Encode}; use core::fmt::Debug; @@ -326,6 +333,46 @@ mod impl_ffi { Ok(ed25519::Public::from_raw(pubkey)) } + fn get_bitcoin_wallet_pair(&self) -> EnclaveResult { + let mut retval = sgx_status_t::SGX_SUCCESS; + let mut private_key = [0u8; 32]; + + let result = unsafe { + ffi::get_bitcoin_wallet_pair( + self.eid, + &mut retval, + private_key.as_mut_ptr(), + private_key.len() as u32, + ) + }; + + ensure!(result == sgx_status_t::SGX_SUCCESS, Error::Sgx(result)); + ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); + + schnorr::Pair::from_bytes(&private_key) + .map_err(|e| Error::Other(format!("{:?}", e).into())) + } + + fn get_ethereum_wallet_pair(&self) -> EnclaveResult { + let mut retval = sgx_status_t::SGX_SUCCESS; + let mut private_key = [0u8; 32]; + + let result = unsafe { + ffi::get_ethereum_wallet_pair( + self.eid, + &mut retval, + private_key.as_mut_ptr(), + private_key.len() as u32, + ) + }; + + ensure!(result == sgx_status_t::SGX_SUCCESS, Error::Sgx(result)); + ensure!(retval == sgx_status_t::SGX_SUCCESS, Error::Sgx(retval)); + + ecdsa::Pair::from_bytes(&private_key) + .map_err(|e| Error::Other(format!("{:?}", e).into())) + } + fn get_fingerprint(&self) -> EnclaveResult { let mut retval = sgx_status_t::SGX_SUCCESS; let mut mr_enclave = [0u8; MR_ENCLAVE_SIZE]; diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index 709c1ad97a..74f8039d39 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -39,10 +39,20 @@ impl Pair { Self { private, public } } + pub fn from_bytes(bytes: &[u8]) -> Result { + let private_key = SigningKey::from_bytes(bytes.into()) + .map_err(|e| Error::Other(format!("{:?}", e).into()))?; + Ok(Self::new(private_key)) + } + pub fn public_bytes(&self) -> [u8; 33] { self.public.as_affine().to_bytes().as_slice().try_into().unwrap() } + pub fn private_bytes(&self) -> [u8; 32] { + self.private.to_bytes().as_slice().try_into().unwrap() + } + pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { let signature: Signature = self.private.try_sign(payload).map_err(|e| Error::Other(e.to_string().into()))?; diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs index e9caa01651..ca32644ad7 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/schnorr.rs @@ -39,11 +39,22 @@ impl Pair { Self { private, public } } + pub fn from_bytes(bytes: &[u8]) -> Result { + let private_key = + SigningKey::from_bytes(bytes).map_err(|e| Error::Other(format!("{:?}", e).into()))?; + Ok(Self::new(private_key)) + } + pub fn public_bytes(&self) -> [u8; 33] { // safe to unwrap here self.public.as_affine().to_bytes().as_slice().try_into().unwrap() } + pub fn private_bytes(&self) -> [u8; 32] { + // safe to unwrap here + self.private.to_bytes().as_slice().try_into().unwrap() + } + pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { let signature: Signature = self.private.try_sign(payload).map_err(|e| Error::Other(e.to_string().into()))?; diff --git a/bitacross-worker/enclave-runtime/Enclave.edl b/bitacross-worker/enclave-runtime/Enclave.edl index ecc350dd13..7ebae232ac 100644 --- a/bitacross-worker/enclave-runtime/Enclave.edl +++ b/bitacross-worker/enclave-runtime/Enclave.edl @@ -89,6 +89,12 @@ enclave { public sgx_status_t get_ecc_signing_pubkey( [out, size=pubkey_size] uint8_t* pubkey, uint32_t pubkey_size); + public sgx_status_t get_bitcoin_wallet_pair( + [out, size=pair_size] uint8_t* pair, uint32_t pair_size); + + public sgx_status_t get_ethereum_wallet_pair( + [out, size=pair_size] uint8_t* pair, uint32_t pair_size); + public sgx_status_t get_ecc_vault_pubkey( [in, size=shard_size] uint8_t* shard, uint32_t shard_size, [out, size=pubkey_size] uint8_t* pubkey, uint32_t pubkey_size); diff --git a/bitacross-worker/enclave-runtime/src/lib.rs b/bitacross-worker/enclave-runtime/src/lib.rs index 06bde8acfd..0ef69b9a96 100644 --- a/bitacross-worker/enclave-runtime/src/lib.rs +++ b/bitacross-worker/enclave-runtime/src/lib.rs @@ -60,6 +60,9 @@ use crate::{ }; use codec::Decode; use core::ffi::c_int; +use initialization::global_components::{ + GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT, GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT, +}; use itc_parentchain::{ block_import_dispatcher::DispatchBlockImport, light_client::{concurrent_access::ValidatorAccess, Validator}, @@ -68,7 +71,7 @@ use itc_parentchain::{ use itp_component_container::ComponentGetter; use itp_node_api::metadata::NodeMetadata; use itp_nonce_cache::{MutateNonce, Nonce}; -use itp_sgx_crypto::key_repository::AccessPubkey; +use itp_sgx_crypto::key_repository::{AccessKey, AccessPubkey}; use itp_storage::{StorageProof, StorageProofChecker}; use itp_types::{ShardIdentifier, SignedBlock}; use itp_utils::write_slice_and_whitespace_pad; @@ -239,6 +242,64 @@ pub unsafe extern "C" fn get_ecc_signing_pubkey(pubkey: *mut u8, pubkey_size: u3 sgx_status_t::SGX_SUCCESS } +#[no_mangle] +pub unsafe extern "C" fn get_bitcoin_wallet_pair(pair: *mut u8, pair_size: u32) -> sgx_status_t { + if_production_or!( + { + error!("Bitcoin wallet can only be retrieved in non-prod"); + sgx_status_t::SGX_ERROR_UNEXPECTED + }, + { + let bitcoin_key_repository = match GLOBAL_BITCOIN_KEY_REPOSITORY_COMPONENT.get() { + Ok(s) => s, + Err(e) => { + error!("{:?}", e); + return sgx_status_t::SGX_ERROR_UNEXPECTED + }, + }; + + let keypair = match bitcoin_key_repository.retrieve_key() { + Ok(p) => p, + Err(e) => return e.into(), + }; + + let privkey_slice = slice::from_raw_parts_mut(pair, pair_size as usize); + privkey_slice.clone_from_slice(&keypair.private_bytes()); + + sgx_status_t::SGX_SUCCESS + } + ) +} + +#[no_mangle] +pub unsafe extern "C" fn get_ethereum_wallet_pair(pair: *mut u8, pair_size: u32) -> sgx_status_t { + if_production_or!( + { + error!("Ethereum wallet can only be retrieved in non-prod"); + sgx_status_t::SGX_ERROR_UNEXPECTED + }, + { + let ethereum_key_repository = match GLOBAL_ETHEREUM_KEY_REPOSITORY_COMPONENT.get() { + Ok(s) => s, + Err(e) => { + error!("{:?}", e); + return sgx_status_t::SGX_ERROR_UNEXPECTED + }, + }; + + let keypair = match ethereum_key_repository.retrieve_key() { + Ok(p) => p, + Err(e) => return e.into(), + }; + + let privkey_slice = slice::from_raw_parts_mut(pair, pair_size as usize); + privkey_slice.clone_from_slice(&keypair.private_bytes()); + + sgx_status_t::SGX_SUCCESS + } + ) +} + #[no_mangle] pub unsafe extern "C" fn set_nonce( nonce: *const u32, diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index 77a79434c2..b3a05aefb0 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -95,3 +95,4 @@ anyhow = "1.0.40" mockall = "0.11" # local itc-parentchain-test = { path = "../core/parentchain/test" } +itp-sgx-crypto = { path = "../core-primitives/sgx/crypto" } diff --git a/bitacross-worker/service/src/cli.yml b/bitacross-worker/service/src/cli.yml index 7e411b3181..6954013454 100644 --- a/bitacross-worker/service/src/cli.yml +++ b/bitacross-worker/service/src/cli.yml @@ -150,6 +150,8 @@ subcommands: about: Get the public ed25519 key the TEE uses to sign messages and extrinsics - dump-ra: about: Perform RA and dump cert to disk + - wallet: + about: Print the bitcoin and ethereum custodian wallet key information, only works in non-prod - mrenclave: about: Dump mrenclave to stdout. base58 encoded. - init-shard: diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index dcc7404187..9c50baff94 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -268,6 +268,16 @@ pub(crate) fn main() { } else { setup::migrate_shard(enclave.as_ref(), &old_shard, &new_shard); } + } else if let Some(sub_matches) = matches.subcommand_matches("wallet") { + println!("Bitcoin wallet:"); + let bitcoin_keypair = enclave.get_bitcoin_wallet_pair().unwrap(); + println!("public : 0x{}", hex::encode(bitcoin_keypair.public_bytes())); + println!("private: 0x{}", hex::encode(bitcoin_keypair.private_bytes())); + + println!("Ethereum wallet:"); + let ethereum_keypair = enclave.get_ethereum_wallet_pair().unwrap(); + println!("public : 0x{}", hex::encode(ethereum_keypair.public_bytes())); + println!("private: 0x{}", hex::encode(ethereum_keypair.private_bytes())); } else { println!("For options: use --help"); } diff --git a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs index a1de0ba1c8..3048230d83 100644 --- a/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs +++ b/bitacross-worker/service/src/tests/mocks/enclave_api_mock.rs @@ -23,6 +23,7 @@ use itc_parentchain::primitives::{ }; use itp_enclave_api::{enclave_base::EnclaveBase, sidechain::Sidechain, EnclaveResult}; use itp_settings::worker::MR_ENCLAVE_SIZE; +use itp_sgx_crypto::{ecdsa, schnorr}; use itp_storage::StorageProof; use itp_types::{EnclaveFingerprint, ShardIdentifier}; use sgx_crypto_helper::rsa3072::Rsa3072PubKey; @@ -88,6 +89,14 @@ impl EnclaveBase for EnclaveMock { unreachable!() } + fn get_bitcoin_wallet_pair(&self) -> EnclaveResult { + unreachable!() + } + + fn get_ethereum_wallet_pair(&self) -> EnclaveResult { + unreachable!() + } + fn get_fingerprint(&self) -> EnclaveResult { Ok([1u8; MR_ENCLAVE_SIZE].into()) } From 3d351a4994639ab0127cc2a1f20ddc5c54d5b3e4 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Mon, 19 Feb 2024 23:52:59 +0100 Subject: [PATCH 45/64] purge relayer registry file (#2498) * purge relayer registry file * remove root use --- bitacross-worker/service/src/setup.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/bitacross-worker/service/src/setup.rs b/bitacross-worker/service/src/setup.rs index c636a18284..0f44676707 100644 --- a/bitacross-worker/service/src/setup.rs +++ b/bitacross-worker/service/src/setup.rs @@ -18,8 +18,9 @@ use crate::error::{Error, ServiceResult}; use itp_settings::files::{ - LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, SCHEDULED_ENCLAVE_FILE, SHARDS_PATH, - TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, + LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, RELAYER_REGISTRY_FILE, SCHEDULED_ENCLAVE_FILE, + SHARDS_PATH, TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, + TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH, }; use std::{fs, path::Path}; @@ -144,6 +145,7 @@ fn purge_files(root_directory: &Path) -> ServiceResult<()> { remove_dir_if_it_exists(root_directory, TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)?; remove_file_if_it_exists(root_directory, SCHEDULED_ENCLAVE_FILE)?; + remove_file_if_it_exists(root_directory, RELAYER_REGISTRY_FILE)?; Ok(()) } @@ -180,6 +182,9 @@ mod tests { fs::File::create(&shards_path.join("state_1.bin")).unwrap(); fs::File::create(&shards_path.join("state_2.bin")).unwrap(); + fs::File::create(&root_directory.join(SCHEDULED_ENCLAVE_FILE)).unwrap(); + fs::File::create(&root_directory.join(RELAYER_REGISTRY_FILE)).unwrap(); + fs::create_dir_all(&root_directory.join(LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)) .unwrap(); fs::create_dir_all(&root_directory.join(TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH)) @@ -193,6 +198,8 @@ mod tests { assert!(!root_directory.join(LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); assert!(!root_directory.join(TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); assert!(!root_directory.join(TARGET_B_PARENTCHAIN_LIGHT_CLIENT_DB_PATH).exists()); + assert!(!root_directory.join(SCHEDULED_ENCLAVE_FILE).exists()); + assert!(!root_directory.join(RELAYER_REGISTRY_FILE).exists()); } #[test] From cb19d092e11692e47484c59852b1c3315d72991e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 20 Feb 2024 00:53:39 +0000 Subject: [PATCH 46/64] Bump syn from 2.0.48 to 2.0.49 (#2504) Bumps [syn](https://github.com/dtolnay/syn) from 2.0.48 to 2.0.49. - [Release notes](https://github.com/dtolnay/syn/releases) - [Commits](https://github.com/dtolnay/syn/compare/2.0.48...2.0.49) --- updated-dependencies: - dependency-name: syn dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- Cargo.lock | 88 +++++++++++++++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 44 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9da2d4f2de..c546ea9364 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -445,7 +445,7 @@ checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -456,7 +456,7 @@ checksum = "c980ee35e870bd1a4d2c8294d4c04d0499e67bca1e4b5cefcc693c2fa00caea9" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -1068,7 +1068,7 @@ dependencies = [ "heck", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -1740,7 +1740,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2039,7 +2039,7 @@ checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2066,7 +2066,7 @@ dependencies = [ "proc-macro2", "quote", "scratch", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2083,7 +2083,7 @@ checksum = "b8fcfa71f66c8563c4fa9dd2bb68368d50267856f831ac5d85367e0805f9606c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2361,7 +2361,7 @@ checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2554,7 +2554,7 @@ checksum = "5e9a1f9f7d83e59740248a6e14ecf93929ade55027844dfcea78beafccc15745" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2565,7 +2565,7 @@ checksum = "b893c4eb2dc092c811165f84dc7447fae16fb66521717968c34c509b39b1a5c5" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -2865,7 +2865,7 @@ dependencies = [ "fs-err", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -3382,7 +3382,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -3497,7 +3497,7 @@ dependencies = [ "proc-macro-warning", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -3509,7 +3509,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -3519,7 +3519,7 @@ source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -3686,7 +3686,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -5531,7 +5531,7 @@ dependencies = [ "cargo_toml", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -6517,7 +6517,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -6528,7 +6528,7 @@ checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -7901,7 +7901,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -8425,7 +8425,7 @@ dependencies = [ "pest_meta", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -8466,7 +8466,7 @@ checksum = "ec2e072ecce94ec471b13398d5402c188e76ac03cf74dd1a975161b23a3f6d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -9790,7 +9790,7 @@ dependencies = [ "proc-macro2", "quote", "sha3", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -9905,7 +9905,7 @@ checksum = "0e99670bafb56b9a106419397343bdbc8b8742c3cc449fec6345f86173f47cd4" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -10301,7 +10301,7 @@ checksum = "2dfaf0c85b766276c797f3791f5bc6d5bd116b41d53049af2789666b0c0bc9fa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -11096,7 +11096,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -12072,7 +12072,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -12358,7 +12358,7 @@ checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -12699,7 +12699,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -12941,7 +12941,7 @@ dependencies = [ "proc-macro2", "quote", "sp-core-hashing", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -12960,7 +12960,7 @@ source = "git+https://github.com/paritytech/substrate?branch=polkadot-v0.9.42#ff dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -13171,7 +13171,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -13339,7 +13339,7 @@ dependencies = [ "parity-scale-codec", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -13538,7 +13538,7 @@ dependencies = [ "proc-macro2", "quote", "rustversion", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -13712,9 +13712,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.48" +version = "2.0.49" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f3531638e407dfc0814761abb7c00a5b54992b849452a0646b7f65c9f770f3f" +checksum = "915aea9e586f80826ee59f8453c1101f9d1c4b3964cd2460185ee8e299ada496" dependencies = [ "proc-macro2", "quote", @@ -13830,7 +13830,7 @@ checksum = "090198534930841fab3a5d1bb637cde49e339654e606195f8d9c76eeb081dc96" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -13999,7 +13999,7 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -14170,7 +14170,7 @@ checksum = "5f4f31f56159e98206da9efd823404b79b6ef3143b4a7ab76e67b1751b25a4ab" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -14213,7 +14213,7 @@ dependencies = [ "proc-macro-crate", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -14651,7 +14651,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", "wasm-bindgen-shared", ] @@ -14685,7 +14685,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -15816,7 +15816,7 @@ dependencies = [ "Inflector", "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] @@ -15924,7 +15924,7 @@ checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" dependencies = [ "proc-macro2", "quote", - "syn 2.0.48", + "syn 2.0.49", ] [[package]] From 4de61c82e6621807cdc48c5ec30b2bf6ffc082c8 Mon Sep 17 00:00:00 2001 From: "will.li" <120463031+higherordertech@users.noreply.github.com> Date: Tue, 20 Feb 2024 13:41:34 +1100 Subject: [PATCH 47/64] feat: P-212 Club3 NFT VC (#2492) * feat: P-212 added new data provider morails, added arbitrum and polygon network, added general nft holder VC using this new data provider, added Club3 VC, migrated existing WeirdoGhostGang VC under it * add fail fast config for retryable api, add RetryableError * fix build error --------- Co-authored-by: higherordertech --- primitives/core/src/assertion.rs | 6 +- primitives/core/src/lib.rs | 3 + primitives/core/src/network.rs | 14 +- primitives/core/src/web3_nft.rs | 38 ++ .../commands/litentry/request_vc.rs | 15 +- .../commands/litentry/request_vc_direct.rs | 7 +- .../interfaces/identity/definitions.ts | 2 + .../interfaces/vc/definitions.ts | 5 + .../core/assertion-build-v2/src/lib.rs | 1 + .../assertion-build-v2/src/nft_holder/mod.rs | 296 +++++++++++++++ .../src/token_holding_amount/mod.rs | 4 +- .../litentry/core/assertion-build/src/lib.rs | 4 +- .../amount_holding/evm_amount_holding.rs | 4 +- .../nft_holder/weirdo_ghost_gang_holder.rs | 2 +- tee-worker/litentry/core/common/src/lib.rs | 3 + .../litentry/core/common/src/web3_nft/mod.rs | 60 +++ .../litentry/core/credentials-v2/src/lib.rs | 1 + .../core/credentials-v2/src/nft_holder/mod.rs | 90 +++++ .../src/token_holding_amount/mod.rs | 4 +- .../core/credentials/src/credential_schema.rs | 2 + .../src/nodereal/crypto_summary/mod.rs | 10 +- .../core/data-providers/src/achainable.rs | 2 + .../core/data-providers/src/karat_dao.rs | 102 ++---- .../litentry/core/data-providers/src/lib.rs | 241 +++++++++++- .../core/data-providers/src/moralis.rs | 237 ++++++++++++ .../data-providers/src/nodereal_jsonrpc.rs | 343 ++++++++++++------ .../litentry/core/mock-server/src/lib.rs | 2 + .../litentry/core/mock-server/src/moralis.rs | 55 +++ .../core/mock-server/src/nodereal_jsonrpc.rs | 27 +- tee-worker/litentry/core/service/src/lib.rs | 7 +- .../src/platform_user/karat_dao_user.rs | 2 +- .../litentry/core/service/src/web3_nft/mod.rs | 23 ++ .../service/src/web3_nft/nft_holder/common.rs | 124 +++++++ .../service/src/web3_nft/nft_holder/mod.rs | 39 ++ .../web3_token/token_balance/bnb_balance.rs | 4 +- .../src/web3_token/token_balance/common.rs | 2 +- .../web3_token/token_balance/eth_balance.rs | 4 +- .../web3_token/token_balance/lit_balance.rs | 2 +- .../receiver/src/handler/assertion.rs | 3 + tee-worker/litentry/primitives/src/lib.rs | 3 +- tee-worker/local-setup/.env.dev | 4 +- tee-worker/service/src/prometheus_metrics.rs | 1 + 42 files changed, 1584 insertions(+), 214 deletions(-) create mode 100644 primitives/core/src/web3_nft.rs create mode 100644 tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs create mode 100644 tee-worker/litentry/core/common/src/web3_nft/mod.rs create mode 100644 tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs create mode 100644 tee-worker/litentry/core/data-providers/src/moralis.rs create mode 100644 tee-worker/litentry/core/mock-server/src/moralis.rs create mode 100644 tee-worker/litentry/core/service/src/web3_nft/mod.rs create mode 100644 tee-worker/litentry/core/service/src/web3_nft/nft_holder/common.rs create mode 100644 tee-worker/litentry/core/service/src/web3_nft/nft_holder/mod.rs diff --git a/primitives/core/src/assertion.rs b/primitives/core/src/assertion.rs index 8de472e606..38e0cd2c7b 100644 --- a/primitives/core/src/assertion.rs +++ b/primitives/core/src/assertion.rs @@ -20,7 +20,7 @@ use crate::{ all_web3networks, AccountId, BnbDigitDomainType, BoundedWeb3Network, EVMTokenType, GenericDiscordRoleType, OneBlockCourseType, PlatformUserType, VIP3MembershipCardLevel, - Web3Network, Web3TokenType, + Web3Network, Web3NftType, Web3TokenType, }; use codec::{Decode, Encode, MaxEncodedLen}; use scale_info::TypeInfo; @@ -232,6 +232,9 @@ pub enum Assertion { #[codec(index = 25)] PlatformUser(PlatformUserType), + + #[codec(index = 26)] + NftHolder(Web3NftType), } impl Assertion { @@ -281,6 +284,7 @@ impl Assertion { Self::A2(..) | Self::A3(..) | Self::A6 | Self::GenericDiscordRole(..) => vec![], Self::TokenHoldingAmount(t_type) => t_type.get_supported_networks(), Self::PlatformUser(p_type) => p_type.get_supported_networks(), + Self::NftHolder(t_type) => t_type.get_supported_networks(), } } } diff --git a/primitives/core/src/lib.rs b/primitives/core/src/lib.rs index e9f94c5f07..ad0cb102be 100644 --- a/primitives/core/src/lib.rs +++ b/primitives/core/src/lib.rs @@ -65,6 +65,9 @@ pub use web3_token::*; mod platform_user; pub use platform_user::*; +mod web3_nft; +pub use web3_nft::*; + /// Common types of parachains. mod types { use sp_runtime::{ diff --git a/primitives/core/src/network.rs b/primitives/core/src/network.rs index ddad0cef7e..8f8d92061e 100644 --- a/primitives/core/src/network.rs +++ b/primitives/core/src/network.rs @@ -90,6 +90,12 @@ pub enum Web3Network { BitcoinP2wpkh, #[codec(index = 13)] BitcoinP2wsh, + + // evm + #[codec(index = 14)] + Polygon, + #[codec(index = 15)] + Arbitrum, } // mainly used in CLI @@ -114,7 +120,7 @@ impl Web3Network { } pub fn is_evm(&self) -> bool { - matches!(self, Self::Ethereum | Self::Bsc) + matches!(self, Self::Ethereum | Self::Bsc | Self::Polygon | Self::Arbitrum) } pub fn is_bitcoin(&self) -> bool { @@ -175,6 +181,8 @@ mod tests { Web3Network::BitcoinP2sh => false, Web3Network::BitcoinP2wpkh => false, Web3Network::BitcoinP2wsh => false, + Web3Network::Polygon => true, + Web3Network::Arbitrum => true, } ) }) @@ -200,6 +208,8 @@ mod tests { Web3Network::BitcoinP2sh => false, Web3Network::BitcoinP2wpkh => false, Web3Network::BitcoinP2wsh => false, + Web3Network::Polygon => false, + Web3Network::Arbitrum => false, } ) }) @@ -225,6 +235,8 @@ mod tests { Web3Network::BitcoinP2sh => true, Web3Network::BitcoinP2wpkh => true, Web3Network::BitcoinP2wsh => true, + Web3Network::Polygon => false, + Web3Network::Arbitrum => false, } ) }) diff --git a/primitives/core/src/web3_nft.rs b/primitives/core/src/web3_nft.rs new file mode 100644 index 0000000000..4f11cb30f0 --- /dev/null +++ b/primitives/core/src/web3_nft.rs @@ -0,0 +1,38 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +use codec::{Decode, Encode, MaxEncodedLen}; +use scale_info::TypeInfo; +use sp_std::{vec, vec::Vec}; + +use crate::Web3Network; + +#[derive(Encode, Decode, Clone, Debug, PartialEq, Eq, MaxEncodedLen, TypeInfo)] +pub enum Web3NftType { + #[codec(index = 0)] + WeirdoGhostGang, + #[codec(index = 1)] + Club3Sbt, +} + +impl Web3NftType { + pub fn get_supported_networks(&self) -> Vec { + match self { + Self::WeirdoGhostGang => vec![Web3Network::Ethereum], + Self::Club3Sbt => vec![Web3Network::Bsc, Web3Network::Polygon, Web3Network::Arbitrum], + } + } +} diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs index 5a3adb496f..022b62f817 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs @@ -30,7 +30,7 @@ use litentry_primitives::{ AchainableDate, AchainableDateInterval, AchainableDatePercent, AchainableParams, AchainableToken, Assertion, BoundedWeb3Network, ContestType, EVMTokenType, GenericDiscordRoleType, Identity, OneBlockCourseType, ParameterString, PlatformUserType, - RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, Web3Network, Web3TokenType, + RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, Web3Network, Web3NftType, Web3TokenType, REQUEST_AES_KEY_LEN, }; use sp_core::Pair; @@ -111,6 +111,8 @@ pub enum Command { TokenHoldingAmount(TokenHoldingAmountCommand), #[clap(subcommand)] PlatformUser(PlatformUserCommand), + #[clap(subcommand)] + NftHolder(NftHolderCommand), } #[derive(Args, Debug)] @@ -228,6 +230,12 @@ pub enum PlatformUserCommand { KaratDaoUser, } +#[derive(Subcommand, Debug)] +pub enum NftHolderCommand { + WeirdoGhostGang, + Club3Sbt, +} + // positional args (to vec) + required arg + optional arg is a nightmare combination for clap parser, // additionally, only the last positional argument, or second to last positional argument may be set to `.num_args()` // @@ -491,6 +499,11 @@ impl RequestVcCommand { PlatformUserCommand::KaratDaoUser => Assertion::PlatformUser(PlatformUserType::KaratDaoUser), }, + Command::NftHolder(arg) => match arg { + NftHolderCommand::WeirdoGhostGang => + Assertion::NftHolder(Web3NftType::WeirdoGhostGang), + NftHolderCommand::Club3Sbt => Assertion::NftHolder(Web3NftType::Club3Sbt), + }, }; let key = Self::random_aes_key(); diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs index 9c3bd08091..983a7a57e4 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs @@ -31,7 +31,7 @@ use litentry_primitives::{ AchainableDate, AchainableDateInterval, AchainableDatePercent, AchainableParams, AchainableToken, Assertion, ContestType, EVMTokenType, GenericDiscordRoleType, Identity, OneBlockCourseType, PlatformUserType, RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, - Web3Network, Web3TokenType, REQUEST_AES_KEY_LEN, + Web3Network, Web3NftType, Web3TokenType, REQUEST_AES_KEY_LEN, }; use sp_core::Pair; @@ -257,6 +257,11 @@ impl RequestVcDirectCommand { PlatformUserCommand::KaratDaoUser => Assertion::PlatformUser(PlatformUserType::KaratDaoUser), }, + Command::NftHolder(arg) => match arg { + NftHolderCommand::WeirdoGhostGang => + Assertion::NftHolder(Web3NftType::WeirdoGhostGang), + NftHolderCommand::Club3Sbt => Assertion::NftHolder(Web3NftType::Club3Sbt), + }, }; let key: [u8; 32] = Self::random_aes_key(); diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts index 60ea972254..672d38917a 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/identity/definitions.ts @@ -49,6 +49,8 @@ export default { "BitcoinP2sh", "BitcoinP2wpkh", "BitcoinP2wsh", + "Polygon", + "Arbitrum", ], }, LitentryValidationData: { diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts index 1976a90040..a13515f856 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts @@ -33,6 +33,7 @@ export default { CyptoSummary: "Null", TokenHoldingAmount: "Web3TokenType", PlatformUser: "PlatformUserType", + NftHolder: "Web3NftType", }, }, AssertionSupportedNetwork: { @@ -178,5 +179,9 @@ export default { PlatformUserType: { _enum: ["KaratDaoUser"], }, + // Web3NftType + Web3NftType: { + _enum: ["WeirdoGhostGang", "Club3Sbt"], + }, }, }; diff --git a/tee-worker/litentry/core/assertion-build-v2/src/lib.rs b/tee-worker/litentry/core/assertion-build-v2/src/lib.rs index 69ea1134cd..ef5b56095d 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/lib.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/lib.rs @@ -41,5 +41,6 @@ use lc_assertion_build::{transpose_identity, Result}; use lc_service::DataProviderConfig; use log::*; +pub mod nft_holder; pub mod platform_user; pub mod token_holding_amount; diff --git a/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs new file mode 100644 index 0000000000..677f8f1b82 --- /dev/null +++ b/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs @@ -0,0 +1,296 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use lc_credentials_v2::{nft_holder::NFTHolderAssertionUpdate, Credential}; +use lc_service::web3_nft::nft_holder::has_nft; +use lc_stf_task_sender::AssertionBuildRequest; +use litentry_primitives::{Web3Network, Web3NftType}; +use log::debug; + +use crate::*; + +pub fn build( + req: &AssertionBuildRequest, + nft_type: Web3NftType, + data_provider_config: &DataProviderConfig, +) -> Result { + debug!("nft holder: {:?}", nft_type); + + let identities: Vec<(Web3Network, Vec)> = transpose_identity(&req.identities); + let addresses = identities + .into_iter() + .flat_map(|(network_type, addresses)| { + addresses.into_iter().map(move |address| (network_type, address)) + }) + .collect::>(); + + let result = has_nft(nft_type.clone(), addresses, data_provider_config).map_err(|e| { + Error::RequestVCFailed( + Assertion::NftHolder(nft_type.clone()), + ErrorDetail::DataProviderError(ErrorString::truncate_from( + format!("{e:?}").as_bytes().to_vec(), + )), + ) + }); + + match result { + Ok(has_nft) => match Credential::new(&req.who, &req.shard) { + Ok(mut credential_unsigned) => { + credential_unsigned.update_nft_holder_assertion(nft_type, has_nft); + Ok(credential_unsigned) + }, + Err(e) => { + error!("Generate unsigned credential failed {:?}", e); + Err(Error::RequestVCFailed(Assertion::NftHolder(nft_type), e.into_error_detail())) + }, + }, + Err(e) => Err(e), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use itp_stf_primitives::types::ShardIdentifier; + use itp_types::AccountId; + use lc_common::web3_nft::{NftAddress, NftName}; + use lc_credentials_v2::assertion_logic::{AssertionLogic, Op}; + use lc_mock_server::run; + use litentry_hex_utils::decode_hex; + use litentry_primitives::{Identity, IdentityNetworkTuple}; + + fn crate_assertion_build_request( + nft_type: Web3NftType, + identities: Vec, + ) -> AssertionBuildRequest { + AssertionBuildRequest { + shard: ShardIdentifier::default(), + signer: AccountId::from([0; 32]), + who: AccountId::from([0; 32]).into(), + assertion: Assertion::NftHolder(nft_type), + identities, + top_hash: Default::default(), + parachain_block_number: 0u32, + sidechain_block_number: 0u32, + maybe_key: None, + should_create_id_graph: false, + req_ext_hash: Default::default(), + } + } + + fn create_token_assertion_logic(nft_type: Web3NftType) -> Box { + Box::new(AssertionLogic::Item { + src: "$nft".into(), + op: Op::Equal, + dst: nft_type.get_nft_name().into(), + }) + } + + fn create_werido_ghost_gang_assertion_logic() -> Box { + Box::new(AssertionLogic::Or { + items: vec![Box::new(AssertionLogic::And { + items: vec![ + Box::new(AssertionLogic::Item { + src: "$network".into(), + op: Op::Equal, + dst: "ethereum".into(), + }), + Box::new(AssertionLogic::Item { + src: "$address".into(), + op: Op::Equal, + dst: Web3NftType::WeirdoGhostGang + .get_nft_address(Web3Network::Ethereum) + .unwrap() + .into(), + }), + ], + })], + }) + } + + fn create_club3_sbt_assertion_logic() -> Box { + Box::new(AssertionLogic::Or { + items: vec![ + Box::new(AssertionLogic::And { + items: vec![ + Box::new(AssertionLogic::Item { + src: "$network".into(), + op: Op::Equal, + dst: "bsc".into(), + }), + Box::new(AssertionLogic::Item { + src: "$address".into(), + op: Op::Equal, + dst: Web3NftType::Club3Sbt + .get_nft_address(Web3Network::Bsc) + .unwrap() + .into(), + }), + ], + }), + Box::new(AssertionLogic::And { + items: vec![ + Box::new(AssertionLogic::Item { + src: "$network".into(), + op: Op::Equal, + dst: "polygon".into(), + }), + Box::new(AssertionLogic::Item { + src: "$address".into(), + op: Op::Equal, + dst: Web3NftType::Club3Sbt + .get_nft_address(Web3Network::Polygon) + .unwrap() + .into(), + }), + ], + }), + Box::new(AssertionLogic::And { + items: vec![ + Box::new(AssertionLogic::Item { + src: "$network".into(), + op: Op::Equal, + dst: "arbitrum".into(), + }), + Box::new(AssertionLogic::Item { + src: "$address".into(), + op: Op::Equal, + dst: Web3NftType::Club3Sbt + .get_nft_address(Web3Network::Arbitrum) + .unwrap() + .into(), + }), + ], + }), + ], + }) + } + + fn init() -> DataProviderConfig { + let _ = env_logger::builder().is_test(true).try_init(); + let url = run(0).unwrap(); + + let mut data_provider_config = DataProviderConfig::default(); + + data_provider_config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".into()); + data_provider_config.set_nodereal_api_chain_network_url(url.clone() + "/nodereal_jsonrpc/"); + data_provider_config.set_moralis_api_key("d416f55179dbd0e45b1a8ed030e3".into()); + data_provider_config.set_moralis_api_url(url.clone() + "/moralis/"); + data_provider_config + } + + #[test] + fn build_werido_ghost_gang_holder_works() { + let data_provider_config = init(); + let address = decode_hex("0x45cdb67696802b9d01ed156b883269dbdb9c6239".as_bytes().to_vec()) + .unwrap() + .as_slice() + .try_into() + .unwrap(); + let identities: Vec = + vec![(Identity::Evm(address), vec![Web3Network::Ethereum])]; + + let req = crate_assertion_build_request(Web3NftType::WeirdoGhostGang, identities); + + match build(&req, Web3NftType::WeirdoGhostGang, &data_provider_config) { + Ok(credential) => { + log::info!("build WeirdoGhostGang holder done"); + assert_eq!( + *(credential.credential_subject.assertions.first().unwrap()), + AssertionLogic::And { + items: vec![ + create_token_assertion_logic(Web3NftType::WeirdoGhostGang), + create_werido_ghost_gang_assertion_logic(), + ] + } + ); + assert_eq!(*(credential.credential_subject.values.first().unwrap()), true); + }, + Err(e) => { + panic!("build WeirdoGhostGang holder failed with error {:?}", e); + }, + } + } + + #[test] + fn build_club3_sbt_holder_works() { + let data_provider_config = init(); + let mut address = + decode_hex("0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".as_bytes().to_vec()) + .unwrap() + .as_slice() + .try_into() + .unwrap(); + let mut identities: Vec = + vec![(Identity::Evm(address), vec![Web3Network::Bsc, Web3Network::Polygon])]; + + let mut req = crate_assertion_build_request(Web3NftType::Club3Sbt, identities); + match build(&req, Web3NftType::Club3Sbt, &data_provider_config) { + Ok(credential) => { + log::info!("build Club3Sbt holder done"); + assert_eq!( + *(credential.credential_subject.assertions.first().unwrap()), + AssertionLogic::And { + items: vec![ + create_token_assertion_logic(Web3NftType::Club3Sbt), + create_club3_sbt_assertion_logic(), + ] + } + ); + assert_eq!(*(credential.credential_subject.values.first().unwrap()), true); + }, + Err(e) => { + panic!("build Club3Sbt holder failed with error {:?}", e); + }, + } + + address = decode_hex("0x45cdb67696802b9d01ed156b883269dbdb9c6239".as_bytes().to_vec()) + .unwrap() + .as_slice() + .try_into() + .unwrap(); + identities = vec![( + Identity::Evm(address), + vec![Web3Network::Bsc, Web3Network::Polygon, Web3Network::Arbitrum], + )]; + + req = crate_assertion_build_request(Web3NftType::Club3Sbt, identities); + match build(&req, Web3NftType::Club3Sbt, &data_provider_config) { + Ok(credential) => { + log::info!("build Club3Sbt holder done"); + assert_eq!( + *(credential.credential_subject.assertions.first().unwrap()), + AssertionLogic::And { + items: vec![ + create_token_assertion_logic(Web3NftType::Club3Sbt), + create_club3_sbt_assertion_logic(), + ] + } + ); + assert_eq!(*(credential.credential_subject.values.first().unwrap()), false); + }, + Err(e) => { + panic!("build Club3Sbt holder failed with error {:?}", e); + }, + } + } +} diff --git a/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs index 4973ce5ecf..a39660141b 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs @@ -38,8 +38,8 @@ pub fn build( let identities: Vec<(Web3Network, Vec)> = transpose_identity(&req.identities); let addresses = identities .into_iter() - .flat_map(|(newtwork_type, addresses)| { - addresses.into_iter().map(move |address| (newtwork_type, address)) + .flat_map(|(network_type, addresses)| { + addresses.into_iter().map(move |address| (network_type, address)) }) .collect::>(); diff --git a/tee-worker/litentry/core/assertion-build/src/lib.rs b/tee-worker/litentry/core/assertion-build/src/lib.rs index 6f2f1a124b..bf093be0a1 100644 --- a/tee-worker/litentry/core/assertion-build/src/lib.rs +++ b/tee-worker/litentry/core/assertion-build/src/lib.rs @@ -179,7 +179,9 @@ fn pubkey_to_address(network: &Web3Network, pubkey: &str) -> String { | Web3Network::Khala | Web3Network::SubstrateTestnet | Web3Network::Ethereum - | Web3Network::Bsc => "".to_string(), + | Web3Network::Bsc + | Web3Network::Polygon + | Web3Network::Arbitrum => "".to_string(), } } diff --git a/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs b/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs index 70715e0770..cebb4616b7 100644 --- a/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs +++ b/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs @@ -55,13 +55,13 @@ fn get_holding_balance( block_number: "latest".into(), }; match address.0 { - Web3Network::Bsc => match bsc_client.get_token_balance_20(¶m) { + Web3Network::Bsc => match bsc_client.get_token_balance_20(¶m, false) { Ok(balance) => { total_balance += balance; }, Err(err) => return Err(err), }, - Web3Network::Ethereum => match eth_client.get_token_balance_20(¶m) { + Web3Network::Ethereum => match eth_client.get_token_balance_20(¶m, false) { Ok(balance) => { total_balance += balance; }, diff --git a/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs b/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs index b06474ca12..e768d5a420 100644 --- a/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs +++ b/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs @@ -42,7 +42,7 @@ fn check_has_nft( block_number: "latest".into(), }; - match client.get_token_balance_721(¶m) { + match client.get_token_balance_721(¶m, false) { Ok(res) => { debug!("Get token balance 721 response: {:?}", res); Ok(res > 0) diff --git a/tee-worker/litentry/core/common/src/lib.rs b/tee-worker/litentry/core/common/src/lib.rs index b8654a586f..655f1de648 100644 --- a/tee-worker/litentry/core/common/src/lib.rs +++ b/tee-worker/litentry/core/common/src/lib.rs @@ -25,6 +25,7 @@ extern crate sgx_tstd as std; use litentry_primitives::Web3Network; pub mod platform_user; +pub mod web3_nft; pub mod web3_token; pub fn web3_network_to_chain(network: &Web3Network) -> &'static str { @@ -43,5 +44,7 @@ pub fn web3_network_to_chain(network: &Web3Network) -> &'static str { Web3Network::BitcoinP2sh => "bitcoin_p2sh", Web3Network::BitcoinP2wpkh => "bitcoin_p2wpkh", Web3Network::BitcoinP2wsh => "bitcoin_p2wsh", + Web3Network::Polygon => "polygon", + Web3Network::Arbitrum => "arbitrum", } } diff --git a/tee-worker/litentry/core/common/src/web3_nft/mod.rs b/tee-worker/litentry/core/common/src/web3_nft/mod.rs new file mode 100644 index 0000000000..3d48ae84e4 --- /dev/null +++ b/tee-worker/litentry/core/common/src/web3_nft/mod.rs @@ -0,0 +1,60 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use litentry_primitives::Web3NftType; + +use crate::Web3Network; + +pub trait NftName { + fn get_nft_name(&self) -> &'static str; +} + +impl NftName for Web3NftType { + fn get_nft_name(&self) -> &'static str { + match self { + Self::WeirdoGhostGang => "Weirdo Ghost Gang", + Self::Club3Sbt => "Club3 SBT", + } + } +} + +pub trait NftAddress { + fn get_nft_address(&self, network: Web3Network) -> Option<&'static str>; +} + +impl NftAddress for Web3NftType { + fn get_nft_address(&self, network: Web3Network) -> Option<&'static str> { + match (self, network) { + // WeirdoGhostGang + (Self::WeirdoGhostGang, Web3Network::Ethereum) => + Some("0x9401518f4EBBA857BAA879D9f76E1Cc8b31ed197"), + // Club3Sbt + (Self::Club3Sbt, Web3Network::Bsc) => + Some("0x9f488C0dafb1B3bFeeD3e886e0E6E5f3f4517925"), + (Self::Club3Sbt, Web3Network::Polygon) => + Some("0xAc2e4e67cffa5E82bfA1e169e5F9aa405114C982"), + (Self::Club3Sbt, Web3Network::Arbitrum) => + Some("0xcccFF19FB8a4a2A206d07842b8F8c8c0A11904C2"), + _ => None, + } + } +} diff --git a/tee-worker/litentry/core/credentials-v2/src/lib.rs b/tee-worker/litentry/core/credentials-v2/src/lib.rs index 0921662eb3..258a408f5e 100644 --- a/tee-worker/litentry/core/credentials-v2/src/lib.rs +++ b/tee-worker/litentry/core/credentials-v2/src/lib.rs @@ -36,5 +36,6 @@ compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the sam // TODO migration to v2 in the future pub use lc_credentials::{assertion_logic, Credential}; +pub mod nft_holder; pub mod platform_user; pub mod token_holding_amount; diff --git a/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs b/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs new file mode 100644 index 0000000000..b4b5e11d7b --- /dev/null +++ b/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs @@ -0,0 +1,90 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use lc_common::{ + web3_network_to_chain, + web3_nft::{NftAddress, NftName}, +}; +use litentry_primitives::{Web3Network, Web3NftType}; + +// TODO migration to v2 in the future +use lc_credentials::{ + assertion_logic::{AssertionLogic, Op}, + Credential, +}; + +const TYPE: &str = "NFT Holder"; +const DESCRIPTION: &str = "You are a holder of a certain kind of NFT"; + +struct AssertionKeys { + nft: &'static str, + network: &'static str, + address: &'static str, +} + +const ASSERTION_KEYS: AssertionKeys = + AssertionKeys { nft: "$nft", network: "$network", address: "$address" }; + +pub trait NFTHolderAssertionUpdate { + fn update_nft_holder_assertion(&mut self, nft_type: Web3NftType, has_nft: bool); +} + +impl NFTHolderAssertionUpdate for Credential { + fn update_nft_holder_assertion(&mut self, nft_type: Web3NftType, has_nft: bool) { + self.add_subject_info(DESCRIPTION, TYPE); + + let mut assertion = AssertionLogic::new_and(); + + assertion = assertion.add_item(AssertionLogic::new_item( + ASSERTION_KEYS.nft, + Op::Equal, + nft_type.get_nft_name(), + )); + + let mut network_assertion: AssertionLogic = AssertionLogic::new_or(); + for network in nft_type.get_supported_networks() { + network_assertion = network_assertion + .add_item(create_network_assertion_logic(network, nft_type.clone())); + } + + assertion = assertion.add_item(network_assertion); + self.credential_subject.assertions.push(assertion); + self.credential_subject.values.push(has_nft); + } +} + +fn create_network_assertion_logic(network: Web3Network, nft_type: Web3NftType) -> AssertionLogic { + let mut assertion = AssertionLogic::new_and(); + assertion = assertion.add_item(AssertionLogic::new_item( + ASSERTION_KEYS.network, + Op::Equal, + web3_network_to_chain(&network), + )); + if let Some(address) = nft_type.get_nft_address(network) { + assertion = assertion.add_item(AssertionLogic::new_item( + ASSERTION_KEYS.address, + Op::Equal, + address, + )); + } + assertion +} diff --git a/tee-worker/litentry/core/credentials-v2/src/token_holding_amount/mod.rs b/tee-worker/litentry/core/credentials-v2/src/token_holding_amount/mod.rs index b2d95dbe2a..2cd05e903f 100644 --- a/tee-worker/litentry/core/credentials-v2/src/token_holding_amount/mod.rs +++ b/tee-worker/litentry/core/credentials-v2/src/token_holding_amount/mod.rs @@ -75,9 +75,9 @@ fn update_assertion(token_type: Web3TokenType, balance: f64, credential: &mut Cr )); let mut network_assertion: AssertionLogic = AssertionLogic::new_or(); - for newtork in token_type.get_supported_networks() { + for network in token_type.get_supported_networks() { network_assertion = - network_assertion.add_item(create_network_assertion_logic(newtork, token_type.clone())); + network_assertion.add_item(create_network_assertion_logic(network, token_type.clone())); } assertion = assertion.add_item(network_assertion); diff --git a/tee-worker/litentry/core/credentials/src/credential_schema.rs b/tee-worker/litentry/core/credentials/src/credential_schema.rs index 753093a5fc..b1d5aafacf 100644 --- a/tee-worker/litentry/core/credentials/src/credential_schema.rs +++ b/tee-worker/litentry/core/credentials/src/credential_schema.rs @@ -103,5 +103,7 @@ pub fn get_schema_url(assertion: &Assertion) -> String { Assertion::CryptoSummary => format!("{BASE_URL}/23-crypto-summary/1-0-0.json"), Assertion::PlatformUser(_) => format!("{BASE_URL}/24-platform-user/1-0-0.json"), + + Assertion::NftHolder(_) => format!("{BASE_URL}/26-nft-holder/1-0-0.json"), } } diff --git a/tee-worker/litentry/core/credentials/src/nodereal/crypto_summary/mod.rs b/tee-worker/litentry/core/credentials/src/nodereal/crypto_summary/mod.rs index 1c587c060a..f39ca0a98b 100644 --- a/tee-worker/litentry/core/credentials/src/nodereal/crypto_summary/mod.rs +++ b/tee-worker/litentry/core/credentials/src/nodereal/crypto_summary/mod.rs @@ -243,7 +243,7 @@ impl CryptoSummaryClient { for address in addresses { let res = self .bsc_client - .get_token_holdings(address) + .get_token_holdings(address, false) .map_err(|e| e.into_error_detail())?; let result: ResponseTokenResult = @@ -272,7 +272,7 @@ impl CryptoSummaryClient { // Total txs on BSC let tx = self .bsc_client - .get_transaction_count(address) + .get_transaction_count(address, false) .map_err(|e| e.into_error_detail())?; total_txs += tx; } @@ -283,7 +283,7 @@ impl CryptoSummaryClient { // Query Token let res_token = self .eth_client - .get_token_holdings(address) + .get_token_holdings(address, false) .map_err(|e| e.into_error_detail())?; let result: ResponseTokenResult = serde_json::from_value(res_token.result) .map_err(|_| ErrorDetail::ParseError)?; @@ -319,7 +319,7 @@ impl CryptoSummaryClient { let res_nft = self .eth_client - .get_nft_holdings(¶m) + .get_nft_holdings(¶m, false) .map_err(|e| e.into_error_detail())?; let details = res_nft.details; @@ -338,7 +338,7 @@ impl CryptoSummaryClient { // Total txs on Ethereum let tx = self .eth_client - .get_transaction_count(address) + .get_transaction_count(address, false) .map_err(|e| e.into_error_detail())?; total_txs += tx; } diff --git a/tee-worker/litentry/core/data-providers/src/achainable.rs b/tee-worker/litentry/core/data-providers/src/achainable.rs index 1b432bff0f..41668ecfd7 100644 --- a/tee-worker/litentry/core/data-providers/src/achainable.rs +++ b/tee-worker/litentry/core/data-providers/src/achainable.rs @@ -184,6 +184,8 @@ pub fn web3_network_to_chain(network: &Web3Network) -> String { Web3Network::BitcoinP2sh => "bitcoin_p2sh".into(), Web3Network::BitcoinP2wpkh => "bitcoin_p2wpkh".into(), Web3Network::BitcoinP2wsh => "bitcoin_p2wsh".into(), + Web3Network::Polygon => "polygon".into(), + Web3Network::Arbitrum => "arbitrum".into(), } } diff --git a/tee-worker/litentry/core/data-providers/src/karat_dao.rs b/tee-worker/litentry/core/data-providers/src/karat_dao.rs index 2a211b22a1..718fbfbd79 100644 --- a/tee-worker/litentry/core/data-providers/src/karat_dao.rs +++ b/tee-worker/litentry/core/data-providers/src/karat_dao.rs @@ -20,26 +20,27 @@ use crate::sgx_reexport_prelude::*; #[cfg(all(not(feature = "std"), feature = "sgx"))] extern crate sgx_tstd as std; -use crate::{build_client, DataProviderConfig, Error, HttpError}; +use crate::{ + build_client, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, RetryableRestGet, +}; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ http_client::{DefaultSend, HttpClient}, rest_client::RestClient, - RestGet, RestPath, + RestPath, }; use log::debug; use serde::{Deserialize, Serialize}; use std::{ - format, str, + str, string::{String, ToString}, - thread, time, vec, + vec, vec::Vec, }; pub struct KaratDaoClient { - api_retry_delay: u64, - api_retry_times: u16, + retry_option: RetryOption, client: RestClient>, } @@ -54,68 +55,33 @@ impl KaratDaoClient { let api_retry_delay = data_provider_config.karat_dao_api_retry_delay; let api_retry_times = data_provider_config.karat_dao_api_retry_times; let api_url = data_provider_config.karat_dao_api_url.clone(); + let retry_option = + RetryOption { retry_delay: Some(api_retry_delay), retry_times: Some(api_retry_times) }; let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); let client = build_client(api_url.as_str(), headers); - KaratDaoClient { api_retry_delay, api_retry_times, client } - } - - fn retry(&mut self, action: A) -> Result - where - A: Fn(&mut KaratDaoClient) -> Result, - { - let mut retries = 0; - // retry delay 1 second - let base_delay = time::Duration::from_millis(self.api_retry_delay); - // maximum 5 retry times - let maximum_retries = self.api_retry_times; - - loop { - if retries > 0 { - debug!("Fail to call karat dao api, begin retry: {}", retries); - } - - if retries > maximum_retries { - return Err(Error::RequestError(format!( - "Fail to call call karat dao api within {} retries", - maximum_retries - ))) - } - - match action(self) { - Ok(response) => return Ok(response), - Err(err) => { - let req_err: Error = - Error::RequestError(format!("karat dao api error: {}", err)); - match err { - HttpError::HttpError(code, _) => - if code == 429 { - // Too Many Requests - // exponential back off - thread::sleep(base_delay * 2u32.pow(retries as u32)); - retries += 1; - } else { - return Err(req_err) - }, - _ => return Err(req_err), - } - }, - } - } + KaratDaoClient { retry_option, client } } - fn get(&mut self, params: KaraDaoRequest) -> Result + fn get(&mut self, params: KaraDaoRequest, fast_fail: bool) -> Result where - T: serde::de::DeserializeOwned + RestPath, + T: serde::de::DeserializeOwned + for<'a> RestPath>, { + let retry_option: Option = + if fast_fail { None } else { Some(self.retry_option.clone()) }; if let Some(query) = params.query { let transformed_query: Vec<(&str, &str)> = query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); - self.retry(|c| c.client.get_with::(params.path.clone(), &transformed_query)) + self.client.get_with_retry::( + ReqPath::new(params.path.as_str()), + &transformed_query, + retry_option, + ) } else { - self.retry(|c| c.client.get::(params.path.clone())) + self.client + .get_retry::(ReqPath::new(params.path.as_str()), retry_option) } } } @@ -126,9 +92,9 @@ pub struct UserVerificationResponse { pub result: UserVerificationResult, } -impl RestPath for UserVerificationResponse { - fn get_path(path: String) -> Result { - Ok(path) +impl<'a> RestPath> for UserVerificationResponse { + fn get_path(path: ReqPath) -> Result { + Ok(path.path.into()) } } @@ -139,25 +105,33 @@ pub struct UserVerificationResult { } pub trait KaraDaoApi { - fn user_verification(&mut self, address: String) -> Result; + fn user_verification( + &mut self, + address: String, + fail_fast: bool, + ) -> Result; } impl KaraDaoApi for KaratDaoClient { - fn user_verification(&mut self, address: String) -> Result { + fn user_verification( + &mut self, + address: String, + fail_fast: bool, + ) -> Result { let query: Vec<(String, String)> = vec![("address".to_string(), address)]; let params = KaraDaoRequest { path: "user/verification".into(), query: Some(query) }; debug!("user_verification, params: {:?}", params); - match self.get::(params) { + match self.get::(params, fail_fast) { Ok(resp) => { debug!("user_verification, response: {:?}", resp); Ok(resp) }, Err(e) => { debug!("user_verification, error: {:?}", e); - Err(Error::RequestError(format!("{:?}", e))) + Err(e) }, } } @@ -182,12 +156,12 @@ mod tests { let config = init(); let mut client = KaratDaoClient::new(&config); let mut response = client - .user_verification("0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".into()) + .user_verification("0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".into(), true) .unwrap(); assert_eq!(response.result.is_valid, true); response = client - .user_verification("0x9401518f4ebba857baa879d9f76e1cc8b31ed197".into()) + .user_verification("0x9401518f4ebba857baa879d9f76e1cc8b31ed197".into(), false) .unwrap(); assert_eq!(response.result.is_valid, false); } diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index 48fb2c9e86..e6e4050773 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -41,13 +41,14 @@ use core::time::Duration; use http_req::response::Headers; use itc_rest_client::{ error::Error as HttpError, - http_client::{DefaultSend, HttpClient}, + http_client::{DefaultSend, HttpClient, Send}, rest_client::RestClient, + Query, RestGet, RestPath, RestPost, }; use litentry_macros::if_not_production; use log::debug; use serde::{Deserialize, Serialize}; -use std::vec; +use std::{thread, vec}; use itc_rest_client::http_client::SendWithCertificateVerification; use litentry_primitives::{ @@ -70,6 +71,7 @@ pub mod discord_litentry; pub mod discord_official; pub mod geniidata; pub mod karat_dao; +pub mod moralis; pub mod nodereal; pub mod nodereal_jsonrpc; pub mod twitter_official; @@ -194,6 +196,10 @@ pub struct DataProviderConfig { pub karat_dao_api_retry_delay: u64, pub karat_dao_api_retry_times: u16, pub karat_dao_api_url: String, + pub moralis_api_url: String, + pub moralis_api_retry_delay: u64, + pub moralis_api_retry_times: u16, + pub moralis_api_key: String, } impl Default for DataProviderConfig { @@ -238,6 +244,10 @@ impl DataProviderConfig { karat_dao_api_retry_delay: 5000, karat_dao_api_retry_times: 2, karat_dao_api_url: "https://api.karatdao.com/".to_string(), + moralis_api_key: "".to_string(), + moralis_api_retry_delay: 5000, + moralis_api_retry_times: 2, + moralis_api_url: "https://deep-index.moralis.io/api/v2.2/".to_string(), }; // we allow to override following config properties for non prod dev @@ -308,6 +318,15 @@ impl DataProviderConfig { if let Ok(v) = env::var("KARAT_DAO_API_URL") { config.set_karat_dao_api_url(v); } + if let Ok(v) = env::var("MORALIS_API_URL") { + config.set_moralis_api_url(v); + } + if let Ok(v) = env::var("MORALIS_API_RETRY_DELAY") { + config.set_moralis_api_retry_delay(v.parse::().unwrap()); + } + if let Ok(v) = env::var("MORALIS_API_RETRY_TIME") { + config.set_moralis_api_retry_times(v.parse::().unwrap()); + } }); // set secrets from env variables if let Ok(v) = env::var("TWITTER_AUTH_TOKEN_V2") { @@ -328,6 +347,9 @@ impl DataProviderConfig { if let Ok(v) = env::var("GENIIDATA_API_KEY") { config.set_geniidata_api_key(v); } + if let Ok(v) = env::var("MORALIS_API_KEY") { + config.set_moralis_api_key(v); + } config } pub fn set_twitter_official_url(&mut self, v: String) { @@ -442,6 +464,22 @@ impl DataProviderConfig { debug!("set_karat_dao_api_url: {:?}", v); self.karat_dao_api_url = v; } + pub fn set_moralis_api_key(&mut self, v: String) { + debug!("set_moralis_api_key: {:?}", v); + self.moralis_api_key = v; + } + pub fn set_moralis_api_retry_delay(&mut self, v: u64) { + debug!("set_moralis_api_retry_delay: {:?}", v); + self.moralis_api_retry_delay = v; + } + pub fn set_moralis_api_retry_times(&mut self, v: u16) { + debug!("set_moralis_api_retry_times: {:?}", v); + self.moralis_api_retry_times = v; + } + pub fn set_moralis_api_url(&mut self, v: String) { + debug!("set_moralis_api_url: {:?}", v); + self.moralis_api_url = v; + } } #[derive(Debug, thiserror::Error, Clone)] @@ -460,6 +498,9 @@ pub enum Error { #[error("GeniiData error: {0}")] GeniiDataError(String), + + #[error("Retryable error: {0}")] + RetryableError(String), } impl IntoErrorDetail for Error { @@ -506,6 +547,161 @@ pub fn build_client_with_cert( RestClient::new(http_client, base_url) } +#[derive(Serialize, Debug, Clone, Copy)] +#[serde(rename_all = "camelCase")] +pub struct ReqPath<'a> { + path: &'a str, +} + +impl<'a> ReqPath<'a> { + pub fn new(path: &'a str) -> Self { + Self { path } + } +} + +#[derive(Debug, Clone)] +pub struct RetryOption { + // default delay is 0 + retry_delay: Option, + // set retry_times to 0 or None for fail fast + retry_times: Option, +} + +const DEFAULT_RETRY_OPTION: RetryOption = RetryOption { retry_delay: None, retry_times: None }; + +pub type RestHttpClient = RestClient>; + +pub trait RetryableRestClient { + fn retry(&mut self, action: A, retry_option: RetryOption) -> Result + where + A: Fn(&mut RestHttpClient) -> Result; +} + +impl RetryableRestClient for RestHttpClient +where + T: Send, +{ + fn retry(&mut self, action: A, retry_option: RetryOption) -> Result + where + A: Fn(&mut RestHttpClient) -> Result, + { + let mut retries = 0; + let base_delay = Duration::from_millis(retry_option.retry_delay.unwrap_or_default()); + let maximum_retries = retry_option.retry_times.unwrap_or_default(); + + loop { + if retries > 0 { + debug!("Fail to call rest api, begin retry: {}", retries); + } + + if retries > maximum_retries { + return Err(Error::RetryableError(format!( + "Fail to call rest api within {} retries", + maximum_retries + ))) + } + + match action(self) { + Ok(response) => return Ok(response), + Err(err) => { + let req_err: Error = + Error::RequestError(format!("call rest api error: {}", err)); + match err { + HttpError::HttpError(code, _) => + if code == 429 { + // Too Many Requests + // exponential back off + thread::sleep(base_delay * 2u32.pow(retries as u32)); + retries += 1; + } else { + return Err(req_err) + }, + _ => return Err(req_err), + } + }, + } + } + } +} + +pub trait RetryableRestGet { + // set retry_option to None for fail fast + fn get_retry(&mut self, params: U, retry_option: Option) -> Result + where + U: Copy, + R: serde::de::DeserializeOwned + RestPath; + + // set retry_option to None for fail fast + fn get_with_retry( + &mut self, + params: U, + query: &Query<'_>, + retry_option: Option, + ) -> Result + where + U: Copy, + R: serde::de::DeserializeOwned + RestPath; +} + +impl RetryableRestGet for RestHttpClient +where + T: Send, +{ + fn get_retry(&mut self, params: U, retry_option: Option) -> Result + where + U: Copy, + R: serde::de::DeserializeOwned + RestPath, + { + self.retry(|c| c.get(params), retry_option.unwrap_or(DEFAULT_RETRY_OPTION)) + } + + fn get_with_retry( + &mut self, + params: U, + query: &Query<'_>, + retry_option: Option, + ) -> Result + where + U: Copy, + R: serde::de::DeserializeOwned + RestPath, + { + self.retry(|c| c.get_with(params, query), retry_option.unwrap_or(DEFAULT_RETRY_OPTION)) + } +} + +pub trait RetryableRestPost { + // set retry_option to None for fail fast + fn post_capture_retry( + &mut self, + params: U, + data: &D, + retry_option: Option, + ) -> Result + where + U: Copy, + D: serde::Serialize + RestPath, + K: serde::de::DeserializeOwned; +} + +impl RetryableRestPost for RestHttpClient +where + T: Send, +{ + fn post_capture_retry( + &mut self, + params: U, + data: &D, + retry_option: Option, + ) -> Result + where + U: Copy, + D: serde::Serialize + RestPath, + K: serde::de::DeserializeOwned, + { + self.retry(|c| c.post_capture(params, data), retry_option.unwrap_or(DEFAULT_RETRY_OPTION)) + } +} + pub trait ConvertParameterString { fn to_string(&self, field: &ParameterString) -> Result; } @@ -518,13 +714,20 @@ impl ConvertParameterString for AchainableParams { } } -pub fn convert_balance_hex_to_u128(result: serde_json::Value) -> Result { - match result.as_str() { - Some(result) => match u128::from_str_radix(&result[2..], 16) { +pub fn convert_balance_hex_to_u128(hex: &str) -> Result { + match u128::from_str_radix(&hex[2..], 16) { + Ok(balance) => Ok(balance), + Err(_) => Err(format!("Cannot parse hex {:?} to u128", hex)), + } +} + +pub fn convert_balance_hex_json_value_to_u128(value: serde_json::Value) -> Result { + match value.as_str() { + Some(str) => match convert_balance_hex_to_u128(str) { Ok(balance) => Ok(balance), - Err(_) => Err(Error::RequestError(format!("Cannot parse result {:?} to u128", result))), + Err(err) => Err(Error::RequestError(err)), }, - None => Err(Error::RequestError(format!("Cannot tansform result {:?} to &str", result))), + None => Err(Error::RequestError(format!("Cannot transform value {:?} to &str", value))), } } @@ -535,39 +738,43 @@ mod tests { #[test] fn should_return_correct_value_when_param_is_valid() { assert_eq!( - convert_balance_hex_to_u128(serde_json::Value::String("0x0".into())).unwrap(), + convert_balance_hex_json_value_to_u128(serde_json::Value::String("0x0".into())) + .unwrap(), 0_u128 ); assert_eq!( - convert_balance_hex_to_u128(serde_json::Value::String("0x320".into())).unwrap(), + convert_balance_hex_json_value_to_u128(serde_json::Value::String("0x320".into())) + .unwrap(), 800_u128 ); assert_eq!( - convert_balance_hex_to_u128(serde_json::Value::String("0x2b5e3af16b1880000".into())) - .unwrap(), + convert_balance_hex_json_value_to_u128(serde_json::Value::String( + "0x2b5e3af16b1880000".into() + )) + .unwrap(), 50_000_000_000_000_000_000_u128 ); } #[test] - fn shoud_return_error_when_param_is_not_a_str() { - match convert_balance_hex_to_u128(serde_json::Value::Bool(true)) { + fn should_return_error_when_param_is_not_a_str() { + match convert_balance_hex_json_value_to_u128(serde_json::Value::Bool(true)) { Ok(_) => panic!("Expected an error, but got Ok"), Err(err) => assert_eq!( err.to_string(), - "Request error: Cannot tansform result Bool(true) to &str" + "Request error: Cannot transform value Bool(true) to &str" ), } } #[test] - fn shoud_return_error_when_param_is_not_a_hex_str() { - match convert_balance_hex_to_u128(serde_json::Value::String("qwexyz".into())) { + fn should_return_error_when_param_is_not_a_hex_str() { + match convert_balance_hex_json_value_to_u128(serde_json::Value::String("qwexyz".into())) { Ok(_) => panic!("Expected an error, but got Ok"), Err(err) => - assert_eq!(err.to_string(), "Request error: Cannot parse result \"qwexyz\" to u128"), + assert_eq!(err.to_string(), "Request error: Cannot parse hex \"qwexyz\" to u128"), } } } diff --git a/tee-worker/litentry/core/data-providers/src/moralis.rs b/tee-worker/litentry/core/data-providers/src/moralis.rs new file mode 100644 index 0000000000..4a606dcb44 --- /dev/null +++ b/tee-worker/litentry/core/data-providers/src/moralis.rs @@ -0,0 +1,237 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +use crate::sgx_reexport_prelude::*; + +use crate::{ + build_client, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, RetryableRestGet, +}; +use http::header::CONNECTION; +use http_req::response::Headers; +use itc_rest_client::{ + http_client::{DefaultSend, HttpClient}, + rest_client::RestClient, + RestPath, +}; +use litentry_primitives::Web3Network; +use log::debug; +use serde::{Deserialize, Serialize}; +use std::{ + format, str, + string::{String, ToString}, + vec, + vec::Vec, +}; + +#[derive(Debug)] +pub struct MoralisRequest { + path: String, + query: Option>, +} + +pub struct MoralisClient { + retry_option: RetryOption, + client: RestClient>, +} + +impl MoralisClient { + pub fn new(data_provider_config: &DataProviderConfig) -> Self { + let api_key = data_provider_config.moralis_api_key.clone(); + let api_retry_delay = data_provider_config.moralis_api_retry_delay; + let api_retry_times = data_provider_config.moralis_api_retry_times; + let api_url = data_provider_config.moralis_api_url.clone(); + let retry_option = + RetryOption { retry_delay: Some(api_retry_delay), retry_times: Some(api_retry_times) }; + + let mut headers = Headers::new(); + headers.insert(CONNECTION.as_str(), "close"); + headers.insert("X-API-Key", api_key.as_str()); + let client = build_client(api_url.as_str(), headers); + + MoralisClient { retry_option, client } + } + + fn get(&mut self, params: MoralisRequest, fast_fail: bool) -> Result + where + T: serde::de::DeserializeOwned + for<'a> RestPath>, + { + let retry_option: Option = + if fast_fail { None } else { Some(self.retry_option.clone()) }; + if let Some(query) = params.query { + let transformed_query: Vec<(&str, &str)> = + query.iter().map(|(k, v)| (k.as_str(), v.as_str())).collect(); + self.client.get_with_retry::( + ReqPath::new(params.path.as_str()), + &transformed_query, + retry_option, + ) + } else { + self.client + .get_retry::(ReqPath::new(params.path.as_str()), retry_option) + } + } +} + +trait MoralisChain { + fn get_chain(&self) -> String; +} + +impl MoralisChain for Web3Network { + fn get_chain(&self) -> String { + match self { + Self::Ethereum => "eth".into(), + Self::Bsc => "bsc".into(), + Self::Polygon => "polygon".into(), + Self::Arbitrum => "arbitrum".into(), + _ => "".into(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MoralisChainParam { + value: String, +} + +impl MoralisChainParam { + pub fn new(network: &Web3Network) -> Self { + Self { value: network.get_chain() } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct MoralisPageResponse { + pub status: String, + pub page: usize, + pub page_size: usize, + pub cursor: Option, + pub result: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetNftsByWalletParam { + pub address: String, + pub chain: MoralisChainParam, + pub token_addresses: Option>, + // max: 100, default: 100 + pub limit: Option, + pub cursor: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct GetNftsByWalletResult { + pub amount: String, + pub token_id: String, + pub token_address: String, + pub contract_type: String, + pub owner_of: String, +} + +pub type GetNftsByWalletResponse = MoralisPageResponse; + +impl<'a> RestPath> for GetNftsByWalletResponse { + fn get_path(path: ReqPath) -> Result { + Ok(path.path.into()) + } +} + +pub trait NftApiList { + fn get_nfts_by_wallet( + &mut self, + param: &GetNftsByWalletParam, + fast_fail: bool, + ) -> Result; +} + +impl NftApiList for MoralisClient { + // https://docs.moralis.io/web3-data-api/evm/reference/get-wallet-nfts + fn get_nfts_by_wallet( + &mut self, + param: &GetNftsByWalletParam, + fast_fail: bool, + ) -> Result { + let mut query: Vec<(String, String)> = + vec![("chain".to_string(), param.chain.value.clone())]; + if let Some(token_addresses) = param.token_addresses.clone() { + for (index, address) in token_addresses.iter().enumerate() { + query.push((format!("token_addresses[{}]", index), address.clone())); + } + } + + if let Some(limit) = param.limit { + query.push(("limit".to_string(), limit.to_string())); + } + + if let Some(cursor) = param.cursor.clone() { + query.push(("cursor".to_string(), cursor)); + } + + let params = MoralisRequest { path: format!("{}/nft", param.address), query: Some(query) }; + + debug!("get_nfts_by_wallet, params: {:?}", params); + + match self.get::(params, fast_fail) { + Ok(resp) => { + debug!("get_nfts_by_wallet, response: {:?}", resp); + Ok(resp) + }, + Err(e) => { + debug!("get_nfts_by_wallet, error: {:?}", e); + Err(e) + }, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use lc_mock_server::run; + + fn init() -> DataProviderConfig { + let _ = env_logger::builder().is_test(true).try_init(); + let url = run(0).unwrap() + "/moralis/"; + + let mut config = DataProviderConfig::new(); + config.set_moralis_api_key("d416f55179dbd0e45b1a8ed030e3".to_string()); + config.set_moralis_api_url(url); + config + } + + #[test] + fn does_get_nfts_by_wallet_works() { + let config = init(); + let mut client = MoralisClient::new(&config); + let param = GetNftsByWalletParam { + address: "0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee".into(), + chain: MoralisChainParam::new(&Web3Network::Ethereum), + token_addresses: Some(vec!["0x9401518f4ebba857baa879d9f76e1cc8b31ed197".into()]), + limit: None, + cursor: None, + }; + let result = client.get_nfts_by_wallet(¶m, true).unwrap(); + assert_eq!(result.cursor.unwrap(), "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"); + assert_eq!(result.page, 1); + assert_eq!(result.page_size, 100); + assert_eq!(result.result.len(), 1); + assert_eq!(result.result[0].amount, "1"); + assert_eq!(result.result[0].token_id, "5021"); + assert_eq!(result.result[0].token_address, "0xfff54e6fe44fd47c8814c4b1d62c924c54364ad3"); + assert_eq!(result.result[0].contract_type, "ERC721"); + assert_eq!(result.result[0].owner_of, "0xff3879b8a363aed92a6eaba8f61f1a96a9ec3c1e"); + } +} diff --git a/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs b/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs index ff9cf7b52f..905c473591 100644 --- a/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs +++ b/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs @@ -17,13 +17,16 @@ #[cfg(all(not(feature = "std"), feature = "sgx"))] use crate::sgx_reexport_prelude::*; -use crate::{build_client, convert_balance_hex_to_u128, DataProviderConfig, Error, HttpError}; +use crate::{ + build_client, convert_balance_hex_json_value_to_u128, DataProviderConfig, Error, HttpError, + ReqPath, RetryOption, RetryableRestPost, +}; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ http_client::{DefaultSend, HttpClient}, rest_client::RestClient, - RestPath, RestPost, + RestPath, }; use itp_rpc::{Id, RpcRequest}; use litentry_primitives::Web3Network; @@ -32,7 +35,7 @@ use serde::{Deserialize, Serialize}; use std::{ format, str, string::{String, ToString}, - thread, time, vec, + vec, vec::Vec, }; @@ -53,6 +56,8 @@ impl Web3NetworkNoderealJsonrpcClient for Web3Network { Some(NoderealJsonrpcClient::new(NoderealChain::Bsc, data_provider_config)), Web3Network::Ethereum => Some(NoderealJsonrpcClient::new(NoderealChain::Eth, data_provider_config)), + Web3Network::Polygon => + Some(NoderealJsonrpcClient::new(NoderealChain::Polygon, data_provider_config)), _ => None, } } @@ -90,21 +95,9 @@ impl NoderealChain { } } -#[derive(Serialize, Debug)] -#[serde(rename_all = "camelCase")] -pub struct ReqPath { - path: String, -} - -impl ReqPath { - pub fn new(api_key: &str) -> Self { - Self { path: "v1/".to_string() + api_key } - } -} - -impl RestPath for RpcRequest { +impl<'a> RestPath> for RpcRequest { fn get_path(req: ReqPath) -> core::result::Result { - Ok(req.path) + Ok(req.path.into()) } } @@ -116,9 +109,8 @@ pub struct RpcResponse { } pub struct NoderealJsonrpcClient { - api_key: String, - api_retry_delay: u64, - api_retry_times: u16, + path: String, + retry_option: RetryOption, client: RestClient>, } @@ -129,67 +121,22 @@ impl NoderealJsonrpcClient { let api_retry_times = data_provider_config.nodereal_api_retry_times; let api_url = data_provider_config.nodereal_api_chain_network_url.clone(); let base_url = api_url.replace("{chain}", chain.to_string()); + let retry_option = + RetryOption { retry_delay: Some(api_retry_delay), retry_times: Some(api_retry_times) }; + let path = format!("v1/{}", api_key); let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); let client = build_client(base_url.as_str(), headers); - NoderealJsonrpcClient { api_key, api_retry_delay, api_retry_times, client } + NoderealJsonrpcClient { path, retry_option, client } } - // https://docs.nodereal.io/docs/cups-rate-limit - // add retry functionality to handle situations where requests may surpass predefined constraints. - fn retry(&mut self, action: A) -> Result - where - A: Fn(&mut NoderealJsonrpcClient) -> Result, - { - let mut retries = 0; - // retry delay 1 second - let base_delay = time::Duration::from_millis(self.api_retry_delay); - // maximum 5 retry times - let maximum_retries = self.api_retry_times; - - loop { - if retries > 0 { - debug!("Fail to call nodereal enhanced api, begin retry: {}", retries); - } - - if retries > maximum_retries { - return Err(Error::RequestError(format!( - "Fail to call call nodereal enhanced api within {} retries", - maximum_retries - ))) - } - - match action(self) { - Ok(response) => return Ok(response), - Err(err) => { - let req_err: Error = - Error::RequestError(format!("Nodereal enhanced api error: {}", err)); - match err { - HttpError::HttpError(code, _) => - if code == 429 { - // Too Many Requests - // exponential back off - thread::sleep(base_delay * 2u32.pow(retries as u32)); - retries += 1; - } else { - return Err(req_err) - }, - _ => return Err(req_err), - } - }, - } - } - } - - fn post(&mut self, body: &RpcRequest) -> Result { - self.retry(|c| { - c.client.post_capture::( - ReqPath::new(c.api_key.as_str()), - body, - ) - }) + fn post(&mut self, body: &RpcRequest, fast_fail: bool) -> Result { + let path = ReqPath::new(self.path.as_str()); + let retry_option = if fast_fail { None } else { Some(self.retry_option.clone()) }; + self.client + .post_capture_retry::(path, body, retry_option) } } @@ -236,13 +183,73 @@ pub struct GetTokenBalance721Param { pub block_number: String, } +#[derive(Serialize, Debug)] +pub struct GetTokenBalance1155Param { + // The address of the ERC1155/BEP1155 token + pub token_address: String, + // Account address whose balance will be checked + pub account_address: String, + // The block number in hex format or the string 'latest' or 'earliest' on which the balance will be checked + pub block_number: String, + // The tokenId in hex format of the ERC1155/BEP1155 token + pub token_id: String, +} + +#[derive(Serialize, Debug)] +pub struct GetNFTInventoryParam { + // The address of the account in hex format + pub account_address: String, + // The address of the contract + pub contract_address: String, + // pageSize is hex encoded and should be less equal than 100 (each page return at most pageSize items) + pub page_size: String, + // It should be empty for the first page. If more results are available, a pageKey will be returned in the response. Pass the pageKey to fetch the next pageSize items. + pub page_key: String, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetNFTInventoryResult { + // example: 100_342 + pub page_key: String, + pub details: Vec, +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct GetNFTInventoryResultDetail { + // the address of the token + pub token_address: String, + // the id of the token + pub token_id: String, + // the balance of the token + pub balance: String, +} + pub trait NftApiList { fn get_nft_holdings( &mut self, param: &GetNFTHoldingsParam, + fast_fail: bool, ) -> Result; - fn get_token_balance_721(&mut self, param: &GetTokenBalance721Param) -> Result; + fn get_token_balance_721( + &mut self, + param: &GetTokenBalance721Param, + fast_fail: bool, + ) -> Result; + + fn get_token_balance_1155( + &mut self, + param: &GetTokenBalance1155Param, + fast_fail: bool, + ) -> Result; + + fn get_token_nft_inventory( + &mut self, + param: &GetNFTInventoryParam, + fast_fail: bool, + ) -> Result; } // NFT API @@ -251,6 +258,7 @@ impl NftApiList for NoderealJsonrpcClient { fn get_nft_holdings( &mut self, param: &GetNFTHoldingsParam, + fast_fail: bool, ) -> Result { let params: Vec = vec![ param.account_address.clone(), @@ -265,16 +273,23 @@ impl NftApiList for NoderealJsonrpcClient { params, id: Id::Number(1), }; - self.post(&req_body).map_err(|e| Error::RequestError(format!("{:?}", e))).map( - |resp: RpcResponse| { + self.post(&req_body, fast_fail) + .map_err(|e| { + debug!("get_nft_holdings, error: {:?}", e); + e + }) + .map(|resp: RpcResponse| { debug!("get_nft_holdings, response: {:?}", resp); serde_json::from_value(resp.result).unwrap() - }, - ) + }) } // https://docs.nodereal.io/reference/nr_gettokenbalance721 - fn get_token_balance_721(&mut self, param: &GetTokenBalance721Param) -> Result { + fn get_token_balance_721( + &mut self, + param: &GetTokenBalance721Param, + fast_fail: bool, + ) -> Result { let params: Vec = vec![ param.token_address.clone(), param.account_address.clone(), @@ -288,13 +303,84 @@ impl NftApiList for NoderealJsonrpcClient { id: Id::Number(1), }; - match self.post(&req_body) { + match self.post(&req_body, fast_fail) { Ok(resp) => { // result example: '0x', '0x8' debug!("get_token_balance_721, response: {:?}", resp); - convert_balance_hex_to_u128(resp.result) + convert_balance_hex_json_value_to_u128(resp.result) + }, + Err(e) => { + debug!("get_token_balance_721, error: {:?}", e); + Err(e) + }, + } + } + + // https://docs.nodereal.io/reference/nr_gettokenbalance1155 + fn get_token_balance_1155( + &mut self, + param: &GetTokenBalance1155Param, + fast_fail: bool, + ) -> Result { + let params: Vec = vec![ + param.token_address.clone(), + param.account_address.clone(), + param.block_number.clone(), + param.token_id.clone(), + ]; + debug!("get_token_balance_1155: {:?}", param); + let req_body = RpcRequest { + jsonrpc: "2.0".to_string(), + method: "nr_getTokenBalance1155".to_string(), + params, + id: Id::Number(1), + }; + + match self.post(&req_body, fast_fail) { + Ok(resp) => { + // result example: '0x', '0x8' + debug!("get_token_balance_1155, response: {:?}", resp); + convert_balance_hex_json_value_to_u128(resp.result) + }, + Err(e) => { + debug!("get_token_balance_1155, error: {:?}", e); + Err(e) + }, + } + } + + // https://docs.nodereal.io/reference/nr_getnftinventory + fn get_token_nft_inventory( + &mut self, + param: &GetNFTInventoryParam, + fast_fail: bool, + ) -> Result { + let params: Vec = vec![ + param.account_address.clone(), + param.contract_address.clone(), + param.page_size.clone(), + param.page_key.clone(), + ]; + debug!("get_token_nft_inventory: {:?}", param); + let req_body = RpcRequest { + jsonrpc: "2.0".to_string(), + method: "nr_getNFTInventory".to_string(), + params, + id: Id::Number(1), + }; + + match self.post(&req_body, fast_fail) { + Ok(resp) => { + debug!("get_token_nft_inventory, response: {:?}", resp); + match serde_json::from_value::(resp.result) { + Ok(result) => Ok(result), + Err(e) => Err(Error::RequestError(format!("{:?}", e))), + } + }, + Err(e) => { + debug!("get_token_nft_inventory, error: {:?}", e); + Err(e) }, - Err(e) => Err(Error::RequestError(format!("{:?}", e))), } } } @@ -311,13 +397,21 @@ pub struct GetTokenBalance20Param { // Fungible Tokens API pub trait FungibleApiList { - fn get_token_balance_20(&mut self, param: &GetTokenBalance20Param) -> Result; - fn get_token_holdings(&mut self, address: &str) -> Result; + fn get_token_balance_20( + &mut self, + param: &GetTokenBalance20Param, + fast_fail: bool, + ) -> Result; + fn get_token_holdings(&mut self, address: &str, fast_fail: bool) -> Result; } impl FungibleApiList for NoderealJsonrpcClient { // https://docs.nodereal.io/reference/nr_gettokenbalance20 - fn get_token_balance_20(&mut self, param: &GetTokenBalance20Param) -> Result { + fn get_token_balance_20( + &mut self, + param: &GetTokenBalance20Param, + fast_fail: bool, + ) -> Result { let params: Vec = vec![param.contract_address.clone(), param.address.clone(), param.block_number.clone()]; debug!("get_token_balance_20: {:?}", param); @@ -328,17 +422,20 @@ impl FungibleApiList for NoderealJsonrpcClient { id: Id::Number(1), }; - match self.post(&req_body) { + match self.post(&req_body, fast_fail) { Ok(resp) => { // result example: '0x', '0x8' debug!("get_token_balance_20, response: {:?}", resp); - convert_balance_hex_to_u128(resp.result) + convert_balance_hex_json_value_to_u128(resp.result) + }, + Err(e) => { + debug!("get_token_balance_20, error: {:?}", e); + Err(e) }, - Err(e) => Err(Error::RequestError(format!("{:?}", e))), } } - fn get_token_holdings(&mut self, address: &str) -> Result { + fn get_token_holdings(&mut self, address: &str, fast_fail: bool) -> Result { let params: Vec = vec![address.to_string(), "0x1".to_string(), "0x64".to_string()]; debug!("get_token_holdings: {:?}", params); @@ -349,16 +446,16 @@ impl FungibleApiList for NoderealJsonrpcClient { id: Id::Number(1), }; - self.post(&req_body) + self.post(&req_body, fast_fail) } } pub trait EthBalance { - fn get_balance(&mut self, address: &str) -> Result; + fn get_balance(&mut self, address: &str, fast_fail: bool) -> Result; } impl EthBalance for NoderealJsonrpcClient { - fn get_balance(&mut self, address: &str) -> Result { + fn get_balance(&mut self, address: &str, fast_fail: bool) -> Result { let params = vec![address.to_string(), "latest".to_string()]; let req_body = RpcRequest { @@ -368,23 +465,26 @@ impl EthBalance for NoderealJsonrpcClient { id: Id::Number(1), }; - match self.post(&req_body) { + match self.post(&req_body, fast_fail) { Ok(resp) => { // result example: '0x', '0x8' debug!("eth_getBalance, response: {:?}", resp); - convert_balance_hex_to_u128(resp.result) + convert_balance_hex_json_value_to_u128(resp.result) + }, + Err(e) => { + debug!("eth_getBalance, error: {:?}", e); + Err(e) }, - Err(e) => Err(Error::RequestError(format!("{:?}", e))), } } } pub trait TransactionCount { - fn get_transaction_count(&mut self, address: &str) -> Result; + fn get_transaction_count(&mut self, address: &str, fast_fail: bool) -> Result; } impl TransactionCount for NoderealJsonrpcClient { - fn get_transaction_count(&mut self, address: &str) -> Result { + fn get_transaction_count(&mut self, address: &str, fast_fail: bool) -> Result { let params = vec![address.to_string(), "latest".to_string()]; let req_body = RpcRequest { @@ -394,7 +494,7 @@ impl TransactionCount for NoderealJsonrpcClient { id: Id::Number(1), }; - match self.post(&req_body) { + match self.post(&req_body, fast_fail) { Ok(resp) => { // result example: '0x', '0x8' debug!("eth_getTransactionCount, response: {:?}", resp); @@ -407,12 +507,15 @@ impl TransactionCount for NoderealJsonrpcClient { ))), }, None => Err(Error::RequestError(format!( - "Cannot tansform response result {:?} to &str", + "Cannot transform response result {:?} to &str", resp.result ))), } }, - Err(e) => Err(Error::RequestError(format!("{:?}", e))), + Err(e) => { + debug!("get_transaction_count, error: {:?}", e); + Err(e) + }, } } } @@ -442,7 +545,7 @@ mod tests { page: 1, page_size: 2, }; - let result = client.get_nft_holdings(¶m).unwrap(); + let result = client.get_nft_holdings(¶m, false).unwrap(); assert_eq!(result.total_count, "0x1"); assert_eq!(result.details.len(), 1); assert_eq!(result.details[0].token_address, "0x9401518f4EBBA857BAA879D9f76E1Cc8b31ed197"); @@ -460,7 +563,7 @@ mod tests { account_address: "0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE".into(), block_number: "latest".into(), }; - let result = client.get_token_balance_721(¶m).unwrap(); + let result = client.get_token_balance_721(¶m, false).unwrap(); assert_eq!(result, 1); } @@ -473,7 +576,39 @@ mod tests { address: "0x85Be4e2ccc9c85BE8783798B6e8A101BDaC6467F".into(), block_number: "latest".into(), }; - let result = client.get_token_balance_20(¶m).unwrap(); + let result = client.get_token_balance_20(¶m, false).unwrap(); assert_eq!(result, 800); } + + #[test] + fn does_get_token_balance_1155_works() { + let config = init(); + let mut client = NoderealJsonrpcClient::new(NoderealChain::Eth, &config); + let param = GetTokenBalance1155Param { + token_address: "0x07D971C03553011a48E951a53F48632D37652Ba1".into(), + account_address: "0x49AD262C49C7aA708Cc2DF262eD53B64A17Dd5EE".into(), + block_number: "latest".into(), + token_id: "0x0000000000000000000000000000000000000000f".into(), + }; + let result = client.get_token_balance_1155(¶m, false).unwrap(); + assert_eq!(result, 1); + } + + #[test] + fn does_get_token_nft_inventory_works() { + let config = init(); + let mut client = NoderealJsonrpcClient::new(NoderealChain::Eth, &config); + let param = GetNFTInventoryParam { + account_address: "0x0042f9b78c67eb30c020a56d07f9a2fc83bc2514".into(), + contract_address: "0x64aF96778bA83b7d4509123146E2B3b07F7deF52".into(), + page_size: "0x14".into(), + page_key: "".into(), + }; + let result = client.get_token_nft_inventory(¶m, false).unwrap(); + assert_eq!(result.page_key, "100_342"); + assert_eq!(result.details.len(), 1); + assert_eq!(result.details[0].token_address, "0x5e74094cd416f55179dbd0e45b1a8ed030e396a1"); + assert_eq!(result.details[0].token_id, "0x0000000000000000000000000000000000000000f"); + assert_eq!(result.details[0].balance, "0x00000000000000000000000000000000000000001"); + } } diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index 7e640eb1b1..7d50adcc4e 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -26,6 +26,7 @@ pub mod discord_litentry; pub mod discord_official; pub mod karat_dao; pub mod litentry_archive; +pub mod moralis; pub mod nodereal_jsonrpc; pub mod twitter_litentry; pub mod twitter_official; @@ -66,6 +67,7 @@ pub fn run(port: u16) -> Result { .or(discord_litentry::has_role()) .or(nodereal_jsonrpc::query()) .or(karat_dao::query()) + .or(moralis::query()) .or(achainable::query()) .or(litentry_archive::query_user_joined_evm_campaign()) .or(vip3::query_user_sbt_level()) diff --git a/tee-worker/litentry/core/mock-server/src/moralis.rs b/tee-worker/litentry/core/mock-server/src/moralis.rs new file mode 100644 index 0000000000..ae49497c86 --- /dev/null +++ b/tee-worker/litentry/core/mock-server/src/moralis.rs @@ -0,0 +1,55 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . +#![allow(opaque_hidden_inferred_bound)] + +use std::collections::HashMap; + +use lc_data_providers::moralis::{GetNftsByWalletResult, MoralisPageResponse}; + +use warp::{http::Response, Filter}; + +pub(crate) fn query() -> impl Filter + Clone { + warp::get() + .and(warp::path!("moralis" / String / "nft")) + .and(warp::query::>()) + .map(move |address, _| { + if address == "0x49ad262c49c7aa708cc2df262ed53b64a17dd5ee" { + let body = MoralisPageResponse:: { + status: "SYNCED".into(), + cursor: Some("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9".into()), + page: 1, + page_size: 100, + result: vec![GetNftsByWalletResult { + amount: "1".into(), + token_id: "5021".into(), + token_address: "0xfff54e6fe44fd47c8814c4b1d62c924c54364ad3".into(), + contract_type: "ERC721".into(), + owner_of: "0xff3879b8a363aed92a6eaba8f61f1a96a9ec3c1e".into(), + }], + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } else { + let body = MoralisPageResponse:: { + status: "SYNCED".into(), + cursor: None, + page: 1, + page_size: 100, + result: vec![], + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + } + }) +} diff --git a/tee-worker/litentry/core/mock-server/src/nodereal_jsonrpc.rs b/tee-worker/litentry/core/mock-server/src/nodereal_jsonrpc.rs index 43d97de947..6d85c6a10a 100644 --- a/tee-worker/litentry/core/mock-server/src/nodereal_jsonrpc.rs +++ b/tee-worker/litentry/core/mock-server/src/nodereal_jsonrpc.rs @@ -17,7 +17,8 @@ use itp_rpc::Id; use lc_data_providers::nodereal_jsonrpc::{ - GetNFTHoldingsResult, GetNFTHoldingsResultDetail, RpcResponse, + GetNFTHoldingsResult, GetNFTHoldingsResultDetail, GetNFTInventoryResult, + GetNFTInventoryResultDetail, RpcResponse, }; use warp::{http::Response, hyper::body::Bytes, Filter}; @@ -62,6 +63,14 @@ pub(crate) fn query() -> impl Filter { + let body = RpcResponse { + jsonrpc: "2.0".into(), + id: Id::Number(1), + result: serde_json::to_value("0x1").unwrap(), + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + }, "nr_getTokenBalance20" => { let value = match params[1].as_str() { // 100_000_000_000 @@ -84,6 +93,22 @@ pub(crate) fn query() -> impl Filter { + let result = GetNFTInventoryResult { + page_key: "100_342".into(), + details: vec![GetNFTInventoryResultDetail { + token_address: "0x5e74094cd416f55179dbd0e45b1a8ed030e396a1".into(), + token_id: "0x0000000000000000000000000000000000000000f".into(), + balance: "0x00000000000000000000000000000000000000001".into(), + }], + }; + let body = RpcResponse { + jsonrpc: "2.0".into(), + id: Id::Number(1), + result: serde_json::to_value(result).unwrap(), + }; + Response::builder().body(serde_json::to_string(&body).unwrap()) + }, "eth_getBalance" => { let body = RpcResponse { jsonrpc: "2.0".into(), diff --git a/tee-worker/litentry/core/service/src/lib.rs b/tee-worker/litentry/core/service/src/lib.rs index 281a5595e1..282f25ca30 100644 --- a/tee-worker/litentry/core/service/src/lib.rs +++ b/tee-worker/litentry/core/service/src/lib.rs @@ -28,11 +28,14 @@ pub mod sgx_reexport_prelude { #[cfg(all(feature = "std", feature = "sgx"))] compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); -use std::{string::String, vec::Vec}; +use std::{string::String, vec, vec::Vec}; -use litentry_primitives::{ErrorDetail as Error, IntoErrorDetail, Web3Network, Web3TokenType}; +use litentry_primitives::{ + ErrorDetail as Error, IntoErrorDetail, Web3Network, Web3NftType, Web3TokenType, +}; pub use lc_data_providers::DataProviderConfig; pub mod platform_user; +pub mod web3_nft; pub mod web3_token; diff --git a/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs b/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs index 584d7806d3..055fd7d3ea 100644 --- a/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs +++ b/tee-worker/litentry/core/service/src/platform_user/karat_dao_user.rs @@ -36,7 +36,7 @@ pub fn is_user( let mut is_user = false; let mut client = KaratDaoClient::new(data_provider_config); for address in addresses { - match client.user_verification(address) { + match client.user_verification(address, true) { Ok(response) => { is_user = response.result.is_valid; if is_user { diff --git a/tee-worker/litentry/core/service/src/web3_nft/mod.rs b/tee-worker/litentry/core/service/src/web3_nft/mod.rs new file mode 100644 index 0000000000..0ad2e33690 --- /dev/null +++ b/tee-worker/litentry/core/service/src/web3_nft/mod.rs @@ -0,0 +1,23 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +pub mod nft_holder; diff --git a/tee-worker/litentry/core/service/src/web3_nft/nft_holder/common.rs b/tee-worker/litentry/core/service/src/web3_nft/nft_holder/common.rs new file mode 100644 index 0000000000..4cf51238b8 --- /dev/null +++ b/tee-worker/litentry/core/service/src/web3_nft/nft_holder/common.rs @@ -0,0 +1,124 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use core::result::Result; + +use lc_common::web3_nft::NftAddress; +use lc_data_providers::{ + moralis::{ + GetNftsByWalletParam, MoralisChainParam, MoralisClient, NftApiList as MoralisNftApiList, + }, + nodereal_jsonrpc::{ + GetTokenBalance721Param, NftApiList as NoderealNftApiList, Web3NetworkNoderealJsonrpcClient, + }, +}; +use litentry_primitives::ErrorDetail; + +use crate::*; + +// support ERC721/BEP721 nft token +pub fn has_nft_721( + addresses: Vec<(Web3Network, String)>, + nft_type: Web3NftType, + data_provider_config: &DataProviderConfig, +) -> Result { + for address in addresses.iter() { + let network = address.0; + let token_address = nft_type.get_nft_address(network).unwrap_or_default(); + + match network { + Web3Network::Bsc | Web3Network::Ethereum => { + if let Some(mut client) = + network.create_nodereal_jsonrpc_client(data_provider_config) + { + let param = GetTokenBalance721Param { + token_address: token_address.into(), + account_address: address.1.clone(), + block_number: "latest".into(), + }; + match client.get_token_balance_721(¶m, false) { + Ok(balance) => + if balance > 0 { + return Ok(true) + }, + Err(err) => return Err(err.into_error_detail()), + } + } + }, + _ => {}, + } + } + + Ok(false) +} + +// support ERC1155/BEP1155 nft token +pub fn has_nft_1155( + addresses: Vec<(Web3Network, String)>, + nft_type: Web3NftType, + data_provider_config: &DataProviderConfig, +) -> Result { + let mut client = MoralisClient::new(data_provider_config); + for address in addresses.iter() { + let network = address.0; + let token_address = nft_type.get_nft_address(network).unwrap_or_default(); + + match network { + Web3Network::Bsc + | Web3Network::Ethereum + | Web3Network::Polygon + | Web3Network::Arbitrum => { + let mut cursor: Option = None; + 'inner: loop { + let param = GetNftsByWalletParam { + address: address.1.clone(), + chain: MoralisChainParam::new(&network), + token_addresses: Some(vec![token_address.into()]), + limit: None, + cursor, + }; + match client.get_nfts_by_wallet(¶m, false) { + Ok(resp) => { + cursor = resp.cursor; + for item in &resp.result { + match item.amount.parse::() { + Ok(balance) => + if balance > 0 { + return Ok(true) + }, + Err(_) => return Err(ErrorDetail::ParseError), + } + } + }, + Err(err) => return Err(err.into_error_detail()), + } + if cursor.is_none() { + break 'inner + } + } + }, + _ => {}, + } + } + + Ok(false) +} diff --git a/tee-worker/litentry/core/service/src/web3_nft/nft_holder/mod.rs b/tee-worker/litentry/core/service/src/web3_nft/nft_holder/mod.rs new file mode 100644 index 0000000000..8f24d928ef --- /dev/null +++ b/tee-worker/litentry/core/service/src/web3_nft/nft_holder/mod.rs @@ -0,0 +1,39 @@ +// Copyright 2020-2024 Trust Computing GmbH. +// This file is part of Litentry. +// +// Litentry is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Litentry is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Litentry. If not, see . + +#[cfg(all(feature = "std", feature = "sgx"))] +compile_error!("feature \"std\" and feature \"sgx\" cannot be enabled at the same time"); + +#[cfg(all(not(feature = "std"), feature = "sgx"))] +extern crate sgx_tstd as std; + +use core::result::Result; + +use crate::*; + +mod common; + +pub fn has_nft( + nft_type: Web3NftType, + addresses: Vec<(Web3Network, String)>, + data_provider_config: &DataProviderConfig, +) -> Result { + match nft_type { + Web3NftType::WeirdoGhostGang => + common::has_nft_721(addresses, nft_type, data_provider_config), + Web3NftType::Club3Sbt => common::has_nft_1155(addresses, nft_type, data_provider_config), + } +} diff --git a/tee-worker/litentry/core/service/src/web3_token/token_balance/bnb_balance.rs b/tee-worker/litentry/core/service/src/web3_token/token_balance/bnb_balance.rs index 2b8f19874b..157a7ecf3c 100644 --- a/tee-worker/litentry/core/service/src/web3_token/token_balance/bnb_balance.rs +++ b/tee-worker/litentry/core/service/src/web3_token/token_balance/bnb_balance.rs @@ -47,7 +47,7 @@ pub fn get_balance( network.create_nodereal_jsonrpc_client(data_provider_config) { let result = if network == Web3Network::Bsc { - client.get_balance(address.1.as_str()) + client.get_balance(address.1.as_str(), false) } else { let param = GetTokenBalance20Param { contract_address: Web3TokenType::Bnb @@ -57,7 +57,7 @@ pub fn get_balance( address: address.1.clone(), block_number: "latest".into(), }; - client.get_token_balance_20(¶m) + client.get_token_balance_20(¶m, false) }; match result { diff --git a/tee-worker/litentry/core/service/src/web3_token/token_balance/common.rs b/tee-worker/litentry/core/service/src/web3_token/token_balance/common.rs index 4f4cf1bcf6..21a8244e71 100644 --- a/tee-worker/litentry/core/service/src/web3_token/token_balance/common.rs +++ b/tee-worker/litentry/core/service/src/web3_token/token_balance/common.rs @@ -51,7 +51,7 @@ pub fn get_balance_from_evm( if let Some(mut client) = network.create_nodereal_jsonrpc_client(data_provider_config) { - match client.get_token_balance_20(¶m) { + match client.get_token_balance_20(¶m, false) { Ok(balance) => { total_balance += calculate_balance_with_decimals(balance, decimals); }, diff --git a/tee-worker/litentry/core/service/src/web3_token/token_balance/eth_balance.rs b/tee-worker/litentry/core/service/src/web3_token/token_balance/eth_balance.rs index 63140ce609..b2d9b82357 100644 --- a/tee-worker/litentry/core/service/src/web3_token/token_balance/eth_balance.rs +++ b/tee-worker/litentry/core/service/src/web3_token/token_balance/eth_balance.rs @@ -47,7 +47,7 @@ pub fn get_balance( network.create_nodereal_jsonrpc_client(data_provider_config) { let result = if network == Web3Network::Ethereum { - client.get_balance(address.1.as_str()) + client.get_balance(address.1.as_str(), false) } else { let param = GetTokenBalance20Param { contract_address: Web3TokenType::Eth @@ -57,7 +57,7 @@ pub fn get_balance( address: address.1.clone(), block_number: "latest".into(), }; - client.get_token_balance_20(¶m) + client.get_token_balance_20(¶m, false) }; match result { diff --git a/tee-worker/litentry/core/service/src/web3_token/token_balance/lit_balance.rs b/tee-worker/litentry/core/service/src/web3_token/token_balance/lit_balance.rs index 3dd70bab55..fd43092991 100644 --- a/tee-worker/litentry/core/service/src/web3_token/token_balance/lit_balance.rs +++ b/tee-worker/litentry/core/service/src/web3_token/token_balance/lit_balance.rs @@ -58,7 +58,7 @@ pub fn get_balance( if let Some(mut client) = network.create_nodereal_jsonrpc_client(data_provider_config) { - match client.get_token_balance_20(¶m) { + match client.get_token_balance_20(¶m, false) { Ok(balance) => { total_balance += calculate_balance_with_decimals(balance, decimals); }, diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index 85cd5f3b2c..757ebc1f76 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -266,6 +266,9 @@ where platform_user_type, &context.data_provider_config, ), + + Assertion::NftHolder(nft_type) => + lc_assertion_build_v2::nft_holder::build(req, nft_type, &context.data_provider_config), }?; // post-process the credential diff --git a/tee-worker/litentry/primitives/src/lib.rs b/tee-worker/litentry/primitives/src/lib.rs index f050262683..b9310fb34d 100644 --- a/tee-worker/litentry/primitives/src/lib.rs +++ b/tee-worker/litentry/primitives/src/lib.rs @@ -58,7 +58,8 @@ pub use parentchain_primitives::{ GenericDiscordRoleType, Hash as ParentchainHash, Header as ParentchainHeader, IMPError, Index as ParentchainIndex, IntoErrorDetail, OneBlockCourseType, ParameterString, PlatformUserType, SchemaContentString, SchemaIdString, Signature as ParentchainSignature, - SoraQuizType, VCMPError, VIP3MembershipCardLevel, Web3Network, Web3TokenType, MINUTES, + SoraQuizType, VCMPError, VIP3MembershipCardLevel, Web3Network, Web3NftType, Web3TokenType, + MINUTES, }; use scale_info::TypeInfo; use sp_core::{ecdsa, ed25519, sr25519, ByteArray}; diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index b07e56548d..2e82cc537b 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -31,6 +31,7 @@ ACHAINABLE_AUTH_KEY= ONEBLOCK_NOTION_KEY= NODEREAL_API_KEY= GENIIDATA_API_KEY= +MORALIS_API_KEY= # The followings are default value. # Can be skipped; or overwrite within non-production mode. @@ -50,4 +51,5 @@ CONTEST_POPULARITY_DISCORD_ROLE_ID=CONTEST_POPULARITY_DISCORD_ROLE_ID CONTEST_PARTICIPANT_DISCORD_ROLE_ID=CONTEST_PARTICIPANT_DISCORD_ROLE_ID VIP3_URL=http://localhost:19527 GENIIDATA_URL=https://api.geniidata.com/api/1/brc20/balance? -LITENTRY_ARCHIVE_URL=http://localhost:19527 \ No newline at end of file +LITENTRY_ARCHIVE_URL=http://localhost:19527 +MORALIS_API_URL=https://deep-index.moralis.io/api/v2.2/ diff --git a/tee-worker/service/src/prometheus_metrics.rs b/tee-worker/service/src/prometheus_metrics.rs index b1a4b6880b..65e7ea77d0 100644 --- a/tee-worker/service/src/prometheus_metrics.rs +++ b/tee-worker/service/src/prometheus_metrics.rs @@ -284,6 +284,7 @@ fn handle_stf_call_request(req: RequestType, time: f64) { Assertion::CryptoSummary => "CryptoSummary", Assertion::TokenHoldingAmount(_) => "TokenHoldingAmount", Assertion::PlatformUser(_) => "PlatformUser", + Assertion::NftHolder(_) => "NftHolder", }, }; inc_stf_calls(category, label); From bafacabdf8ce75b39214924b106fe3aa56b86148 Mon Sep 17 00:00:00 2001 From: Zhouhui Tian <125243011+zhouhuitian@users.noreply.github.com> Date: Tue, 20 Feb 2024 17:56:31 +0800 Subject: [PATCH 48/64] Keep the naming of OneBlock consistent (#2432) * rename to OneBlock * fix leftover * fix one-block * small update --------- Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- primitives/core/src/assertion.rs | 8 ++++---- .../commands/litentry/request_vc.rs | 14 +++++++------- .../commands/litentry/request_vc_direct.rs | 12 ++++++------ .../prepare-build/interfaces/vc/definitions.ts | 4 ++-- .../core/assertion-build/src/oneblock/course.rs | 2 +- .../core/assertion-build/src/oneblock/mod.rs | 2 +- .../core/credentials/src/credential_schema.rs | 2 +- .../stf-task/receiver/src/handler/assertion.rs | 2 +- tee-worker/service/src/prometheus_metrics.rs | 2 +- .../integration-tests/common/utils/vc-helper.ts | 6 +++--- 10 files changed, 27 insertions(+), 27 deletions(-) diff --git a/primitives/core/src/assertion.rs b/primitives/core/src/assertion.rs index 38e0cd2c7b..3d983db39f 100644 --- a/primitives/core/src/assertion.rs +++ b/primitives/core/src/assertion.rs @@ -193,9 +193,9 @@ pub enum Assertion { #[codec(index = 12)] A20, - // For Oneblock + // For OneBlock #[codec(index = 13)] - Oneblock(OneBlockCourseType), + OneBlock(OneBlockCourseType), // GenericDiscordRole #[codec(index = 14)] @@ -264,8 +264,8 @@ impl Assertion { Self::A14 => vec![Web3Network::Polkadot], // Achainable Assertions Self::Achainable(arg) => arg.chains(), - // Oneblock Assertion - Self::Oneblock(..) => vec![Web3Network::Polkadot, Web3Network::Kusama], + // OneBlock Assertion + Self::OneBlock(..) => vec![Web3Network::Polkadot, Web3Network::Kusama], // SPACEID Assertions Self::BnbDomainHolding | Self::BnbDigitDomainClub(..) => vec![Web3Network::Bsc], // LITStaking diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs index 022b62f817..097919fa0b 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs @@ -41,9 +41,9 @@ use sp_core::Pair; // ./bin/litentry-cli trusted -d request-vc \ // did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 a8 litentry,litmus // -// oneblock VC: +// OneBlock VC: // ./bin/litentry-cli trusted -d request-vc \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 oneblock completion +// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 one-block completion // // achainable VC: // ./bin/litentry-cli trusted -d request-vc \ @@ -94,7 +94,7 @@ pub enum Command { A20, BnbDomainHolding, #[clap(subcommand)] - Oneblock(OneblockCommand), + OneBlock(OneblockCommand), #[clap(subcommand)] Achainable(AchainableCommand), #[clap(subcommand)] @@ -340,13 +340,13 @@ impl RequestVcCommand { Command::A14 => Assertion::A14, Command::A20 => Assertion::A20, Command::BnbDomainHolding => Assertion::BnbDomainHolding, - Command::Oneblock(c) => match c { + Command::OneBlock(c) => match c { OneblockCommand::Completion => - Assertion::Oneblock(OneBlockCourseType::CourseCompletion), + Assertion::OneBlock(OneBlockCourseType::CourseCompletion), OneblockCommand::Outstanding => - Assertion::Oneblock(OneBlockCourseType::CourseOutstanding), + Assertion::OneBlock(OneBlockCourseType::CourseOutstanding), OneblockCommand::Participation => - Assertion::Oneblock(OneBlockCourseType::CourseParticipation), + Assertion::OneBlock(OneBlockCourseType::CourseParticipation), }, Command::Achainable(c) => match c { AchainableCommand::AmountHolding(arg) => Assertion::Achainable( diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs index 983a7a57e4..654dff59b3 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs @@ -41,9 +41,9 @@ use sp_core::Pair; // ./bin/litentry-cli trusted -d request-vc-direct \ // did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 a8 litentry,litmus // -// oneblock VC: +// OneBlock VC: // ./bin/litentry-cli trusted -d request-vc-direct \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 oneblock completion +// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 one-block completion // // achainable VC: // ./bin/litentry-cli trusted -d request-vc-direct \ @@ -98,13 +98,13 @@ impl RequestVcDirectCommand { Command::A14 => Assertion::A14, Command::A20 => Assertion::A20, Command::BnbDomainHolding => Assertion::BnbDomainHolding, - Command::Oneblock(c) => match c { + Command::OneBlock(c) => match c { OneblockCommand::Completion => - Assertion::Oneblock(OneBlockCourseType::CourseCompletion), + Assertion::OneBlock(OneBlockCourseType::CourseCompletion), OneblockCommand::Outstanding => - Assertion::Oneblock(OneBlockCourseType::CourseOutstanding), + Assertion::OneBlock(OneBlockCourseType::CourseOutstanding), OneblockCommand::Participation => - Assertion::Oneblock(OneBlockCourseType::CourseParticipation), + Assertion::OneBlock(OneBlockCourseType::CourseParticipation), }, Command::Achainable(c) => match c { AchainableCommand::AmountHolding(arg) => Assertion::Achainable( diff --git a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts index a13515f856..ed0c0085b0 100644 --- a/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts +++ b/tee-worker/client-api/parachain-api/prepare-build/interfaces/vc/definitions.ts @@ -20,7 +20,7 @@ export default { A14: "Null", Achainable: "AchainableParams", A20: "Null", - Oneblock: "OneBlockCourseType", + OneBlock: "OneBlockCourseType", GenericDiscordRole: "GenericDiscordRoleType", __Unused15: "Null", BnbDomainHolding: "Null", @@ -119,7 +119,7 @@ export default { chain: "Vec", token: "Bytes", }, - // Oneblock + // OneBlock OneBlockCourseType: { _enum: ["CourseCompletion", "CourseOutstanding", "CourseParticipation"], }, diff --git a/tee-worker/litentry/core/assertion-build/src/oneblock/course.rs b/tee-worker/litentry/core/assertion-build/src/oneblock/course.rs index 42da6bd72a..e913f7689e 100644 --- a/tee-worker/litentry/core/assertion-build/src/oneblock/course.rs +++ b/tee-worker/litentry/core/assertion-build/src/oneblock/course.rs @@ -43,7 +43,7 @@ pub fn build( }, Err(e) => { error!("Generate unsigned credential failed {:?}", e); - Err(Error::RequestVCFailed(Assertion::Oneblock(course_type), e.into_error_detail())) + Err(Error::RequestVCFailed(Assertion::OneBlock(course_type), e.into_error_detail())) }, } } diff --git a/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs b/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs index 545e21ebfe..438f6fd183 100644 --- a/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs +++ b/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs @@ -59,7 +59,7 @@ fn fetch_data_from_notion( client.get::(String::default()).map_err(|e| { Error::RequestVCFailed( - Assertion::Oneblock(course_type.clone()), + Assertion::OneBlock(course_type.clone()), ErrorDetail::DataProviderError(ErrorString::truncate_from( format!("{e:?}").as_bytes().to_vec(), )), diff --git a/tee-worker/litentry/core/credentials/src/credential_schema.rs b/tee-worker/litentry/core/credentials/src/credential_schema.rs index b1d5aafacf..edac8f692e 100644 --- a/tee-worker/litentry/core/credentials/src/credential_schema.rs +++ b/tee-worker/litentry/core/credentials/src/credential_schema.rs @@ -76,7 +76,7 @@ pub fn get_schema_url(assertion: &Assertion) -> String { Assertion::A20 => format!("{BASE_URL}/12-idhub-evm-version-early-bird/1-0-0.json"), - Assertion::Oneblock(_) => format!("{BASE_URL}/13-oneblock-student-phase-12/1-0-0.json"), + Assertion::OneBlock(_) => format!("{BASE_URL}/13-oneblock-student-phase-12/1-0-0.json"), Assertion::GenericDiscordRole(_) => format!("{BASE_URL}/14-generic-discord-role/1-0-0.json"), diff --git a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs index 757ebc1f76..88bc40efd5 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/handler/assertion.rs @@ -204,7 +204,7 @@ where Assertion::A20 => lc_assertion_build::a20::build(req, &context.data_provider_config), - Assertion::Oneblock(course_type) => lc_assertion_build::oneblock::course::build( + Assertion::OneBlock(course_type) => lc_assertion_build::oneblock::course::build( req, course_type, &context.data_provider_config, diff --git a/tee-worker/service/src/prometheus_metrics.rs b/tee-worker/service/src/prometheus_metrics.rs index 65e7ea77d0..f8825dca9b 100644 --- a/tee-worker/service/src/prometheus_metrics.rs +++ b/tee-worker/service/src/prometheus_metrics.rs @@ -272,7 +272,7 @@ fn handle_stf_call_request(req: RequestType, time: f64) { Assertion::A14 => "A14", Assertion::A20 => "A20", Assertion::Achainable(..) => "Achainable", - Assertion::Oneblock(..) => "Oneblock", + Assertion::OneBlock(..) => "OneBlock", Assertion::BnbDomainHolding => "BnbDomainHolding", Assertion::BnbDigitDomainClub(..) => "BnbDigitDomainClub", Assertion::GenericDiscordRole(_) => "GenericDiscordRole", diff --git a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts index 5adc06230d..90d1fff0aa 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts @@ -140,15 +140,15 @@ export const defaultAssertions = [ ]; // In both cases as below, it's sufficient to check if the condition is valid, should be invalid. -// For the 'oneblock' assertion, need to configure the Polkadot/Kusma address, +// For the 'OneBlock' assertion, need to configure the Polkadot/Kusma address, // and for 'bnb,' need to configure the NODEREAL_API_KEY // We cannot submit these two types of data involving privacy(from @zhouhui), so we only need to test that their DI response is invalid and that the RequestVCFailed event is received, which should be tested separately from the defaultAssertions. export const unconfiguredAssertions = [ - // Oneblock + // OneBlock { description: 'A participant to the course co-created by OneBlock+ and Parity', assertion: { - oneblock: 'CourseParticipation', + OneBlock: 'CourseParticipation', }, }, From 8210dd144ad955ee8b4a3f8ed93596d5fa725224 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Tue, 20 Feb 2024 11:57:08 +0200 Subject: [PATCH 49/64] Check all subcommands / option flags of litentry-worker and litentry-cli (#2496) * check all subcommands / option flags of litentry-worker and litentry-cli * remove nondirect link-identity command * cleanup * remove nonexistant arguments --- .../commands/litentry/link_identity.rs | 80 ------ .../cli/src/base_cli/commands/litentry/mod.rs | 4 - tee-worker/cli/src/base_cli/mod.rs | 11 +- tee-worker/service/src/cli.yml | 264 +++++++++--------- tee-worker/service/src/main_impl.rs | 2 +- 5 files changed, 139 insertions(+), 222 deletions(-) delete mode 100644 tee-worker/cli/src/base_cli/commands/litentry/link_identity.rs diff --git a/tee-worker/cli/src/base_cli/commands/litentry/link_identity.rs b/tee-worker/cli/src/base_cli/commands/litentry/link_identity.rs deleted file mode 100644 index 7c7f85a08a..0000000000 --- a/tee-worker/cli/src/base_cli/commands/litentry/link_identity.rs +++ /dev/null @@ -1,80 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use super::IMP; -use crate::{ - command_utils::{get_chain_api, *}, - Cli, CliResult, CliResultOk, -}; -use base58::FromBase58; -use codec::{Decode, Encode}; -use itp_sgx_crypto::ShieldingCryptoEncrypt; -use itp_stf_primitives::types::ShardIdentifier; -use litentry_primitives::Identity; -use log::*; -use sp_application_crypto::Pair; -use sp_core::sr25519 as sr25519_core; -use substrate_api_client::{ac_compose_macros::compose_extrinsic, SubmitAndWatch, XtStatus}; - -#[derive(Parser)] -pub struct LinkIdentityCommand { - /// AccountId in ss58check format - account: String, - /// Identity to be created, in did form - did: String, - /// Shard identifier - shard: String, -} - -impl LinkIdentityCommand { - pub(crate) fn run(&self, cli: &Cli) -> CliResult { - let mut chain_api = get_chain_api(cli); - - let shard_opt = match self.shard.from_base58() { - Ok(s) => ShardIdentifier::decode(&mut &s[..]), - _ => panic!("shard argument must be base58 encoded"), - }; - - let shard = match shard_opt { - Ok(shard) => shard, - Err(e) => panic!("{}", e), - }; - - let who = sr25519_core::Pair::from_string(&self.account, None).unwrap(); - chain_api.set_signer(who.clone().into()); - - let identity = Identity::from_did(self.did.as_str()).unwrap(); - let tee_shielding_key = get_shielding_key(cli).unwrap(); - let encrypted_identity = tee_shielding_key.encrypt(&identity.encode()).unwrap(); - - // TODO: the params are incorrect - and need to be reworked too - let vdata: Option> = None; - let xt = compose_extrinsic!( - chain_api, - IMP, - "link_identity", - shard, - who.public().0, - encrypted_identity.to_vec(), - vdata - ); - - let tx_hash = chain_api.submit_and_watch_extrinsic_until(xt, XtStatus::Finalized).unwrap(); - println!("[+] LinkIdentityCommand TrustedOperation got finalized. Hash: {:?}\n", tx_hash); - - Ok(CliResultOk::None) - } -} diff --git a/tee-worker/cli/src/base_cli/commands/litentry/mod.rs b/tee-worker/cli/src/base_cli/commands/litentry/mod.rs index edbda2b23a..d5be3f9c67 100644 --- a/tee-worker/cli/src/base_cli/commands/litentry/mod.rs +++ b/tee-worker/cli/src/base_cli/commands/litentry/mod.rs @@ -15,7 +15,3 @@ // along with Litentry. If not, see . pub mod id_graph_hash; -pub mod link_identity; - -// TODO: maybe move it to use itp_node_api::api_client -pub const IMP: &str = "IdentityManagement"; diff --git a/tee-worker/cli/src/base_cli/mod.rs b/tee-worker/cli/src/base_cli/mod.rs index 5deb57fe6a..225348d2d5 100644 --- a/tee-worker/cli/src/base_cli/mod.rs +++ b/tee-worker/cli/src/base_cli/mod.rs @@ -17,11 +17,8 @@ use crate::{ base_cli::commands::{ - balance::BalanceCommand, - faucet::FaucetCommand, - listen::ListenCommand, - litentry::{id_graph_hash::IDGraphHashCommand, link_identity::LinkIdentityCommand}, - register_tcb_info::RegisterTcbInfoCommand, + balance::BalanceCommand, faucet::FaucetCommand, listen::ListenCommand, + litentry::id_graph_hash::IDGraphHashCommand, register_tcb_info::RegisterTcbInfoCommand, transfer::TransferCommand, }, command_utils::*, @@ -81,9 +78,6 @@ pub enum BaseCommand { /// we want to keep our changes isolated PrintSgxMetadataRaw, - /// create idenity graph - LinkIdentity(LinkIdentityCommand), - /// get the IDGraph hash of the given identity IDGraphHash(IDGraphHashCommand), } @@ -103,7 +97,6 @@ impl BaseCommand { BaseCommand::RegisterTcbInfo(cmd) => cmd.run(cli), // Litentry's commands below BaseCommand::PrintSgxMetadataRaw => print_sgx_metadata_raw(cli), - BaseCommand::LinkIdentity(cmd) => cmd.run(cli), BaseCommand::IDGraphHash(cmd) => cmd.run(cli), } } diff --git a/tee-worker/service/src/cli.yml b/tee-worker/service/src/cli.yml index fa267c5598..9ffc3d55b0 100644 --- a/tee-worker/service/src/cli.yml +++ b/tee-worker/service/src/cli.yml @@ -13,11 +13,11 @@ settings: # part of a Hash args: - node-url: - short: u - long: node-url - help: Set the url and the protocol of the RPC endpoint. - takes_value: true - default_value: "ws://127.0.0.1" + short: u + long: node-url + help: Set the url and the protocol of the RPC endpoint. + takes_value: true + default_value: "ws://127.0.0.1" - node-port: short: p long: node-port @@ -50,177 +50,185 @@ args: help: Data dir where the worker stores it's keys and other data. takes_value: true - ws-external: - long: ws-external - help: Set this flag in case the worker should listen to external requests. + long: ws-external + help: Set this flag in case the worker should listen to external requests. - mu-ra-port: - short: r - long: mu-ra-port - help: Set the websocket port to listen for mu-ra requests - takes_value: true - default_value: "3443" + short: r + long: mu-ra-port + help: Set the websocket port to listen for mu-ra requests + takes_value: true + default_value: "3443" - trusted-worker-port: - short: P - long: trusted-worker-port - help: Set the trusted websocket port of the worker, running directly in the enclave. - takes_value: true - default_value: "2000" + short: P + long: trusted-worker-port + help: Set the trusted websocket port of the worker, running directly in the enclave. + takes_value: true + default_value: "2000" - untrusted-worker-port: - short: w - long: untrusted-worker-port - help: Set the untrusted websocket port of the worker - takes_value: true - default_value: "2001" + short: w + long: untrusted-worker-port + help: Set the untrusted websocket port of the worker + takes_value: true + default_value: "2001" - trusted-external-address: - short: T - long: trusted-external-address - help: Set the trusted worker address to be advertised on the parentchain. If no port is given, the same as in `trusted-worker-port` will be used. - takes_value: true - required: false + short: T + long: trusted-external-address + help: Set the trusted worker address to be advertised on the parentchain. If no port is given, the same as in `trusted-worker-port` will be used. + takes_value: true + required: false - untrusted-external-address: - short: U - long: untrusted-external-address - help: Set the untrusted worker address to be retrieved by a trusted rpc call. If no port is given, the same as in `untrusted-worker-port` will be used. - takes_value: true - required: false + short: U + long: untrusted-external-address + help: Set the untrusted worker address to be retrieved by a trusted rpc call. If no port is given, the same as in `untrusted-worker-port` will be used. + takes_value: true + required: false - mu-ra-external-address: - short: M - long: mu-ra-external-address - help: Set the mutual remote attestation worker address to be retrieved by a trusted rpc call. If no port is given, the same as in `mu-ra-port` will be used. - takes_value: true - required: false + short: M + long: mu-ra-external-address + help: Set the mutual remote attestation worker address to be retrieved by a trusted rpc call. If no port is given, the same as in `mu-ra-port` will be used. + takes_value: true + required: false - enable-metrics: - long: enable-metrics - help: Enable the metrics HTTP server to serve metrics + long: enable-metrics + help: Enable the metrics HTTP server to serve metrics - metrics-port: - short: i - long: metrics-port - help: Set the port on which the metrics are served. - takes_value: true - default_value: "8787" - required: false + short: i + long: metrics-port + help: Set the port on which the metrics are served. + takes_value: true + default_value: "8787" + required: false - untrusted-http-port: - short: h - long: untrusted-http-port - help: Set the port for the untrusted HTTP server - takes_value: true - required: false + short: h + long: untrusted-http-port + help: Set the port for the untrusted HTTP server + takes_value: true + required: false - clean-reset: - long: clean-reset - short: c - help: Cleans and purges any previous state and key files and generates them anew before starting. + long: clean-reset + short: c + help: Cleans and purges any previous state and key files and generates them anew before starting. - enable-mock-server: - long: enable-mock-server - takes_value: false - help: Set this flag to enable the HTTP mock server for testing + long: enable-mock-server + takes_value: false + help: Set this flag to enable the HTTP mock server for testing - mock-server-port: - long: mock-server-port - help: Set the port for HTTP mock server - takes_value: true - required: false - default_value: "19527" + long: mock-server-port + help: Set the port for HTTP mock server + takes_value: true + required: false + default_value: "19527" - parentchain-start-block: - long: parentchain-start-block - help: Set the parentchain block number to start syncing with - takes_value: true - required: false - default_value: "0" + long: parentchain-start-block + help: Set the parentchain block number to start syncing with + takes_value: true + required: false + default_value: "0" - fail-slot-mode: - long: fail-slot-mode - help: Set the mode of failing a slot, values [BeforeOnSlot, AfterOnSlot] - takes_value: true - required: false + long: fail-slot-mode + help: Set the mode of failing a slot, values [BeforeOnSlot, AfterOnSlot] + takes_value: true + required: false - fail-at: - long: fail-at - help: Set the slot to fail on - takes_value: true - required: false - default_value: "0" + long: fail-at + help: Set the slot to fail on + takes_value: true + required: false + default_value: "0" subcommands: - run: - about: Start the litentry-worker - args: - - skip-ra: - long: skip-ra - help: skip remote attestation. Set this flag if running enclave in SW mode - - shard: - required: false - index: 1 - help: shard identifier base58 encoded. Defines the state that this worker shall operate on. Default is mrenclave - - dev: - long: dev - short: d - help: Set this flag if running in development mode to bootstrap enclave account on parentchain via //Alice. - - request-state: - long: request-state - short: r - help: Run the worker and request key and state provisioning from another worker. + about: Start the litentry-worker + args: + - skip-ra: + long: skip-ra + help: skip remote attestation. Set this flag if running enclave in SW mode + - shard: + required: false + index: 1 + help: shard identifier base58 encoded. Defines the state that this worker shall operate on. Default is mrenclave + - dev: + long: dev + short: d + help: Set this flag if running in development mode to bootstrap enclave account on parentchain via //Alice. + - request-state: + long: request-state + short: r + help: Run the worker and request key and state provisioning from another worker. + - marblerun-url: + required: false + long: marblerun-url + help: Set the URL for prometheus metrics. Probably put some meaningful description + takes_value: true - request-state: - about: join a shard by requesting key provisioning from another worker - args: - - shard: - long: shard - required: false - help: shard identifier base58 encoded. Defines the state that this worker shall operate on. Default is mrenclave - - skip-ra: - long: skip-ra - help: skip remote attestation. Set this flag if running enclave in SW mode + about: join a shard by requesting key provisioning from another worker + args: + - shard: + long: shard + required: false + help: shard identifier base58 encoded. Defines the state that this worker shall operate on. Default is mrenclave + - skip-ra: + long: skip-ra + help: skip remote attestation. Set this flag if running enclave in SW mode - shielding-key: - about: Get the public RSA3072 key from the TEE to be used to encrypt requests + about: Get the public RSA3072 key from the TEE to be used to encrypt requests - signing-key: - about: Get the public ed25519 key the TEE uses to sign messages and extrinsics + about: Get the public ed25519 key the TEE uses to sign messages and extrinsics - dump-ra: - about: Perform RA and dump cert to disk + about: Perform RA and dump cert to disk - mrenclave: - about: Dump mrenclave to stdout. base58 encoded. + about: Dump mrenclave to stdout. base58 encoded. - init-shard: - about: Initialize new shard (do this only if you run the first worker for that shard). if shard is not specified, the MRENCLAVE is used instead - args: - - shard: - required: false - multiple: true - index: 1 - help: shard identifier base58 encoded + about: Initialize new shard (do this only if you run the first worker for that shard). if shard is not specified, the MRENCLAVE is used instead + args: + - shard: + required: false + multiple: true + index: 1 + help: shard identifier base58 encoded - migrate-shard: - about: Migrate shard - args: - - old-shard: - long: old-shard - help: shard identifier hex encoded - takes_value: true - - new-shard: - long: new-shard - help: shard identifier hex encoded - takes_value: true + about: Migrate shard + args: + - old-shard: + long: old-shard + help: shard identifier hex encoded + takes_value: true + - new-shard: + long: new-shard + help: shard identifier hex encoded + takes_value: true - test: about: Run tests involving the enclave takes_value: true args: - - all: + - all: short: a long: all help: Run all tests (beware, all corrupts the counter state for some whatever reason...) takes_value: false - - unit: + - unit: short: u long: unit help: Run unit tests takes_value: false - - ecall: + - ecall: short: e long: ecall help: Run enclave ecall tests takes_value: false - - integration: + - integration: short: i long: integration help: Run integration tests takes_value: false - - provisioning-server: + - provisioning-server: long: provisioning-server help: Run TEE server for MU-RA key provisioning takes_value: false - - provisioning-client: + - provisioning-client: long: provisioning-client help: Run TEE client for MU-RA key provisioning takes_value: false + - skip-ra: + long: skip-ra + help: skip remote attestation. Set this flag if running enclave in SW mode diff --git a/tee-worker/service/src/main_impl.rs b/tee-worker/service/src/main_impl.rs index 33d05871ac..603ba62bf0 100644 --- a/tee-worker/service/src/main_impl.rs +++ b/tee-worker/service/src/main_impl.rs @@ -968,7 +968,7 @@ fn subscribe_to_parentchain_new_headers( // - we might have multiple `sync_parentchain` running concurrently, which causes chaos in enclave side // - I still feel it's only a workaround, not a perfect solution // - // TODO: now the sync will panic if disconnected - it heavily relys on the worker-restart to work (even manually) + // TODO: now the sync will panic if disconnected - it heavily relies on the worker-restart to work (even manually) let parentchain_id = parentchain_handler.parentchain_id(); loop { let new_header = subscription From bf74bfdedc86f0138a1b723992fb4b321425a482 Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Tue, 20 Feb 2024 13:20:36 +0100 Subject: [PATCH 50/64] remove evm feature (#2506) --- bitacross-worker/Cargo.lock | 185 ++------ .../app-libs/sgx-runtime/Cargo.toml | 10 - .../app-libs/sgx-runtime/src/evm.rs | 91 ---- .../app-libs/sgx-runtime/src/lib.rs | 33 -- bitacross-worker/app-libs/stf/Cargo.toml | 2 - .../app-libs/stf/src/evm_helpers.rs | 66 --- bitacross-worker/app-libs/stf/src/getter.rs | 55 --- bitacross-worker/app-libs/stf/src/lib.rs | 2 - .../app-libs/stf/src/test_genesis.rs | 19 - .../app-libs/stf/src/trusted_call.rs | 190 --------- bitacross-worker/cli/Cargo.toml | 2 - .../cli/src/evm/commands/evm_call.rs | 87 ---- .../cli/src/evm/commands/evm_command_utils.rs | 32 -- .../cli/src/evm/commands/evm_create.rs | 89 ---- .../cli/src/evm/commands/evm_read.rs | 69 --- bitacross-worker/cli/src/evm/commands/mod.rs | 6 - bitacross-worker/cli/src/evm/mod.rs | 49 --- bitacross-worker/cli/src/lib.rs | 31 +- bitacross-worker/cli/src/trusted_cli.rs | 8 - bitacross-worker/enclave-runtime/Cargo.lock | 128 +----- bitacross-worker/enclave-runtime/Cargo.toml | 4 - .../src/test/evm_pallet_tests.rs | 401 ------------------ .../enclave-runtime/src/test/mod.rs | 2 - .../enclave-runtime/src/test/tests_main.rs | 16 - bitacross-worker/service/Cargo.toml | 1 - bitacross-worker/service/src/main_impl.rs | 4 - scripts/pre-commit.sh | 1 - 27 files changed, 51 insertions(+), 1532 deletions(-) delete mode 100644 bitacross-worker/app-libs/sgx-runtime/src/evm.rs delete mode 100644 bitacross-worker/app-libs/stf/src/evm_helpers.rs delete mode 100644 bitacross-worker/cli/src/evm/commands/evm_call.rs delete mode 100644 bitacross-worker/cli/src/evm/commands/evm_command_utils.rs delete mode 100644 bitacross-worker/cli/src/evm/commands/evm_create.rs delete mode 100644 bitacross-worker/cli/src/evm/commands/evm_read.rs delete mode 100644 bitacross-worker/cli/src/evm/commands/mod.rs delete mode 100644 bitacross-worker/cli/src/evm/mod.rs delete mode 100644 bitacross-worker/enclave-runtime/src/test/evm_pallet_tests.rs diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index be6734a409..530543a702 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -727,7 +727,6 @@ dependencies = [ "litentry-primitives", "log 0.4.20", "pallet-balances", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "pallet-teerex", "parity-scale-codec", "rand 0.8.5", @@ -1457,7 +1456,7 @@ dependencies = [ "litentry-hex-utils", "litentry-macros", "litentry-proc-macros", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "parity-scale-codec", "ring 0.16.20", "scale-info", @@ -2707,27 +2706,6 @@ dependencies = [ "sha3", ] -[[package]] -name = "evm" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49a4e11987c51220aa89dbe1a5cc877f5079fa6864c0a5b4533331db44e9365" -dependencies = [ - "auto_impl", - "environmental 1.1.4", - "ethereum", - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-gasometer 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-runtime 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log 0.4.20", - "parity-scale-codec", - "primitive-types", - "rlp", - "scale-info", - "serde 1.0.193", - "sha3", -] - [[package]] name = "evm" version = "0.39.1" @@ -2736,9 +2714,9 @@ dependencies = [ "auto_impl", "environmental 1.1.4", "ethereum", - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-gasometer 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-runtime 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core 0.39.0", + "evm-gasometer 0.39.0", + "evm-runtime 0.39.0", "log 0.4.20", "parity-scale-codec", "primitive-types", @@ -2760,18 +2738,6 @@ dependencies = [ "serde 1.0.193", ] -[[package]] -name = "evm-core" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1f13264b044cb66f0602180f0bc781c29accb41ff560669a3ec15858d5b606" -dependencies = [ - "parity-scale-codec", - "primitive-types", - "scale-info", - "serde 1.0.193", -] - [[package]] name = "evm-core" version = "0.39.0" @@ -2795,26 +2761,14 @@ dependencies = [ "primitive-types", ] -[[package]] -name = "evm-gasometer" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d43eadc395bd1a52990787ca1495c26b0248165444912be075c28909a853b8c" -dependencies = [ - "environmental 1.1.4", - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-runtime 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "primitive-types", -] - [[package]] name = "evm-gasometer" version = "0.39.0" source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" dependencies = [ "environmental 1.1.4", - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-runtime 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core 0.39.0", + "evm-runtime 0.39.0", "primitive-types", ] @@ -2831,19 +2785,6 @@ dependencies = [ "sha3", ] -[[package]] -name = "evm-runtime" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa5b32f59ec582a5651978004e5c784920291263b7dcb6de418047438e37f4f" -dependencies = [ - "auto_impl", - "environmental 1.1.4", - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "primitive-types", - "sha3", -] - [[package]] name = "evm-runtime" version = "0.39.0" @@ -2851,7 +2792,7 @@ source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa dependencies = [ "auto_impl", "environmental 1.1.4", - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core 0.39.0", "primitive-types", "sha3", ] @@ -3167,24 +3108,6 @@ dependencies = [ "percent-encoding 2.3.0", ] -[[package]] -name = "fp-account" -version = "1.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "hex 0.4.3", - "impl-serde", - "libsecp256k1", - "log 0.4.20", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", - "sp-std", -] - [[package]] name = "fp-account" version = "1.0.0-dev" @@ -3223,34 +3146,19 @@ source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#249 dependencies = [ "ethereum", "ethereum-types", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "frame-support", "num_enum 0.6.1", "parity-scale-codec", "sp-std", ] -[[package]] -name = "fp-evm" -version = "3.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "evm 0.39.1 (registry+https://github.com/rust-lang/crates.io-index)", - "frame-support", - "parity-scale-codec", - "scale-info", - "serde 1.0.193", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "fp-evm" version = "3.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm 0.39.1", "frame-support", "parity-scale-codec", "scale-info", @@ -3267,7 +3175,7 @@ source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#249 dependencies = [ "ethereum", "ethereum-types", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "parity-scale-codec", "scale-info", "sp-api", @@ -4665,7 +4573,6 @@ dependencies = [ "frame-system", "itp-sgx-runtime-primitives", "pallet-balances", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "pallet-parentchain", "pallet-sudo", "pallet-timestamp", @@ -6977,9 +6884,9 @@ dependencies = [ "evm-gasometer 0.37.0", "evm-runtime 0.37.0", "evm-tracing-events", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "moonbeam-primitives-ext", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "parity-scale-codec", "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", @@ -8225,41 +8132,17 @@ source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#249 dependencies = [ "ethereum", "ethereum-types", - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm 0.39.1", "fp-consensus", "fp-ethereum", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "fp-rpc", "fp-storage", "frame-support", "frame-system", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", - "parity-scale-codec", - "scale-info", - "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", - "sp-runtime", - "sp-std", -] - -[[package]] -name = "pallet-evm" -version = "6.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "environmental 1.1.4", - "evm 0.39.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fp-account 1.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "fp-evm 3.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "frame-benchmarking", - "frame-support", - "frame-system", - "hex 0.4.3", - "impl-trait-for-tuples", - "log 0.4.20", + "pallet-evm", "parity-scale-codec", - "rlp", "scale-info", - "sp-core", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", "sp-runtime", "sp-std", @@ -8271,9 +8154,9 @@ version = "6.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ "environmental 1.1.4", - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "fp-account 1.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "evm 0.39.1", + "fp-account", + "fp-evm", "frame-benchmarking", "frame-support", "frame-system", @@ -8295,7 +8178,7 @@ name = "pallet-evm-precompile-blake2" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", ] [[package]] @@ -8303,7 +8186,7 @@ name = "pallet-evm-precompile-bn128" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "sp-core", "substrate-bn", ] @@ -8312,14 +8195,14 @@ dependencies = [ name = "pallet-evm-precompile-bridge-transfer" version = "0.9.17" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "frame-support", "frame-system", "log 0.4.20", "num_enum 0.7.2", "pallet-bridge", "pallet-bridge-transfer", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "parity-scale-codec", "precompile-utils", "rustc-hex", @@ -8334,9 +8217,9 @@ name = "pallet-evm-precompile-dispatch" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "frame-support", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", ] [[package]] @@ -8345,7 +8228,7 @@ version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ "ed25519-dalek", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", ] [[package]] @@ -8353,7 +8236,7 @@ name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "num 0.4.1", ] @@ -8361,12 +8244,12 @@ dependencies = [ name = "pallet-evm-precompile-parachain-staking" version = "0.9.17" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "frame-support", "frame-system", "log 0.4.20", "num_enum 0.7.2", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "pallet-parachain-staking", "parity-scale-codec", "precompile-utils", @@ -8382,7 +8265,7 @@ name = "pallet-evm-precompile-sha3fips" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "tiny-keccak", ] @@ -8391,7 +8274,7 @@ name = "pallet-evm-precompile-simple" version = "2.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "fp-evm", "ripemd", "sp-io 7.0.0 (git+https://github.com/paritytech/substrate.git?branch=polkadot-v0.9.42)", ] @@ -9622,14 +9505,14 @@ name = "precompile-utils" version = "0.9.17" dependencies = [ "assert_matches", - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "evm 0.39.1", + "fp-evm", "frame-support", "frame-system", "impl-trait-for-tuples", "log 0.4.20", "num_enum 0.7.2", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "parity-scale-codec", "precompile-utils-macro", "sha3", @@ -10534,7 +10417,7 @@ dependencies = [ "pallet-democracy", "pallet-drop3", "pallet-ethereum", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-bridge-transfer", diff --git a/bitacross-worker/app-libs/sgx-runtime/Cargo.toml b/bitacross-worker/app-libs/sgx-runtime/Cargo.toml index 7b94576e4c..5b7f0b1038 100644 --- a/bitacross-worker/app-libs/sgx-runtime/Cargo.toml +++ b/bitacross-worker/app-libs/sgx-runtime/Cargo.toml @@ -28,21 +28,11 @@ sp-runtime = { default-features = false, git = "https://github.com/paritytech/su sp-std = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } sp-version = { default-features = false, git = "https://github.com/paritytech/substrate.git", branch = "polkadot-v0.9.42" } -# Integritee dependencies -pallet-evm = { default-features = false, optional = true, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } pallet-parentchain = { path = "../../../pallets/parentchain", default-features = false } [features] default = ["std"] -# Compile the sgx-runtime with evm-pallet support in `no_std`. -evm = ["pallet-evm"] -# Compile the sgx-runtime with evm-pallet support in `std`. -evm_std = [ - "evm", # Activate the `feature = evm` for the compiler flags. - "std", - "pallet-evm/std", -] runtime-benchmarks = [ "frame-support/runtime-benchmarks", "frame-system/runtime-benchmarks", diff --git a/bitacross-worker/app-libs/sgx-runtime/src/evm.rs b/bitacross-worker/app-libs/sgx-runtime/src/evm.rs deleted file mode 100644 index 990fdd1492..0000000000 --- a/bitacross-worker/app-libs/sgx-runtime/src/evm.rs +++ /dev/null @@ -1,91 +0,0 @@ -//! Adds the `pallet-evm` support for the `sgx-runtime. - -// Import types from the crate root including the ones generated by the `construct_runtime!` macro. -use crate::{Balances, Runtime, RuntimeEvent, Timestamp, NORMAL_DISPATCH_RATIO}; -use frame_support::{ - pallet_prelude::Weight, parameter_types, weights::constants::WEIGHT_REF_TIME_PER_SECOND, -}; -use sp_core::{H160, U256}; -use sp_runtime::traits::BlakeTwo256; - -pub use pallet_evm::{ - AddressMapping, Call as EvmCall, EnsureAddressTruncated, FeeCalculator, GasWeightMapping, - HashedAddressMapping as GenericHashedAddressMapping, SubstrateBlockHashMapping, -}; - -pub type HashedAddressMapping = GenericHashedAddressMapping; - -/// Maximum weight per block -pub const MAXIMUM_BLOCK_WEIGHT: Weight = - Weight::from_parts(WEIGHT_REF_TIME_PER_SECOND.saturating_div(2), u64::MAX); - -// FIXME: For now just a random value. -pub struct FixedGasPrice; -impl FeeCalculator for FixedGasPrice { - fn min_gas_price() -> (U256, Weight) { - (1.into(), Weight::from_parts(1, 0u64)) - } -} - -/// Current approximation of the gas/s consumption considering -/// EVM execution over compiled WASM (on 4.4Ghz CPU). -/// Given the 500ms Weight, from which 75% only are used for transactions, -/// the total EVM execution gas limit is: GAS_PER_SECOND * 0.500 * 0.75 ~= 15_000_000. -pub const GAS_PER_SECOND: u64 = 40_000_000; - -/// Approximate ratio of the amount of Weight per Gas. -/// u64 works for approximations because Weight is a very small unit compared to gas. -pub const WEIGHT_PER_GAS: u64 = WEIGHT_REF_TIME_PER_SECOND / GAS_PER_SECOND; - -pub struct FixedGasWeightMapping; - -impl GasWeightMapping for FixedGasWeightMapping { - fn gas_to_weight(gas: u64, _without_base_weight: bool) -> Weight { - Weight::from_parts(gas.saturating_mul(WEIGHT_PER_GAS), 0u64) - } - fn weight_to_gas(weight: Weight) -> u64 { - weight.ref_time().wrapping_div(WEIGHT_PER_GAS) - } -} - -/// An ipmlementation of Frontier's AddressMapping trait for Sgx Accounts. -/// This is basically identical to Frontier's own IdentityAddressMapping, but it works for any type -/// that is Into like AccountId20 for example. -pub struct IntoAddressMapping; - -impl> AddressMapping for IntoAddressMapping { - fn into_account_id(address: H160) -> T { - address.into() - } -} - -parameter_types! { - pub const ChainId: u64 = 42; - pub BlockGasLimit: U256 = U256::from(NORMAL_DISPATCH_RATIO * MAXIMUM_BLOCK_WEIGHT.ref_time() / WEIGHT_PER_GAS); - pub const GasLimitPovSizeRatio: u64 = 150_000_000 / (5 * 1024 * 1024); - //pub PrecompilesValue: FrontierPrecompiles = FrontierPrecompiles::<_>::new(); -} - -impl pallet_evm::Config for Runtime { - type FeeCalculator = FixedGasPrice; - type GasWeightMapping = FixedGasWeightMapping; - type BlockHashMapping = SubstrateBlockHashMapping; - type CallOrigin = EnsureAddressTruncated; - type WithdrawOrigin = EnsureAddressTruncated; - type AddressMapping = HashedAddressMapping; - type Currency = Balances; - type RuntimeEvent = RuntimeEvent; - type Runner = pallet_evm::runner::stack::Runner; - type PrecompilesType = (); - type PrecompilesValue = (); - type ChainId = ChainId; - type OnChargeTransaction = (); - type BlockGasLimit = BlockGasLimit; - type FindAuthor = (); // Currently not available. Would need some more thoughts how prioritisation fees could be handled. - // BlockGasLimit / MAX_POV_SIZE - // type GasLimitPovSizeRatio = GasLimitPovSizeRatio; - type WeightPerGas = (); - type OnCreate = (); - type Timestamp = Timestamp; - type WeightInfo = (); -} diff --git a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs index e293dea94e..ba21f75ffb 100644 --- a/bitacross-worker/app-libs/sgx-runtime/src/lib.rs +++ b/bitacross-worker/app-libs/sgx-runtime/src/lib.rs @@ -28,15 +28,6 @@ // `construct_runtime!` does a lot of recursion and requires us to increase the limit to 256. #![recursion_limit = "256"] -#[cfg(feature = "evm")] -mod evm; -#[cfg(feature = "evm")] -pub use evm::{ - AddressMapping, EnsureAddressTruncated, EvmCall, FeeCalculator, FixedGasPrice, - FixedGasWeightMapping, GasWeightMapping, HashedAddressMapping, IntoAddressMapping, - SubstrateBlockHashMapping, GAS_PER_SECOND, MAXIMUM_BLOCK_WEIGHT, WEIGHT_PER_GAS, -}; - use core::convert::{TryFrom, TryInto}; use frame_support::{traits::ConstU32, weights::ConstantMultiplier}; use pallet_transaction_payment::CurrencyAdapter; @@ -263,28 +254,6 @@ impl pallet_parentchain::Config for Runtime { type WeightInfo = (); } -// The plain sgx-runtime without the `evm-pallet` -#[cfg(not(feature = "evm"))] -construct_runtime!( - pub enum Runtime where - Block = Block, - NodeBlock = opaque::Block, - UncheckedExtrinsic = UncheckedExtrinsic - { - System: frame_system::{Pallet, Call, Config, Storage, Event}, - Timestamp: pallet_timestamp::{Pallet, Call, Storage, Inherent}, - Balances: pallet_balances::{Pallet, Call, Storage, Config, Event}, - TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, - Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event}, - Parentchain: pallet_parentchain::{Pallet, Call, Storage}, - } -); - -// Runtime constructed with the evm pallet. -// -// We need add the compiler-flag for the whole macro because it does not support -// compiler flags withing the macro. -#[cfg(feature = "evm")] construct_runtime!( pub enum Runtime where Block = Block, @@ -297,8 +266,6 @@ construct_runtime!( TransactionPayment: pallet_transaction_payment::{Pallet, Storage, Event}, Sudo: pallet_sudo::{Pallet, Call, Config, Storage, Event}, Parentchain: pallet_parentchain::{Pallet, Call, Storage}, - - Evm: pallet_evm::{Pallet, Call, Storage, Config, Event}, } ); diff --git a/bitacross-worker/app-libs/stf/Cargo.toml b/bitacross-worker/app-libs/stf/Cargo.toml index c08164736c..0f6156a68e 100644 --- a/bitacross-worker/app-libs/stf/Cargo.toml +++ b/bitacross-worker/app-libs/stf/Cargo.toml @@ -47,8 +47,6 @@ sp-keyring = { git = "https://github.com/paritytech/substrate.git", branch = "po [features] default = ["std"] -evm = ["ita-sgx-runtime/evm"] -evm_std = ["evm", "ita-sgx-runtime/evm_std"] sgx = [ "sgx_tstd", "itp-sgx-externalities/sgx", diff --git a/bitacross-worker/app-libs/stf/src/evm_helpers.rs b/bitacross-worker/app-libs/stf/src/evm_helpers.rs deleted file mode 100644 index f0e96d1f87..0000000000 --- a/bitacross-worker/app-libs/stf/src/evm_helpers.rs +++ /dev/null @@ -1,66 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ -use crate::helpers::{get_storage_double_map, get_storage_map}; -use itp_stf_primitives::types::Nonce; -use itp_storage::StorageHasher; -use itp_types::AccountId; -use sha3::{Digest, Keccak256}; -use sp_core::{H160, H256}; -use std::prelude::v1::*; - -pub fn get_evm_account_codes(evm_account: &H160) -> Option> { - get_storage_map("Evm", "AccountCodes", evm_account, &StorageHasher::Blake2_128Concat) -} - -pub fn get_evm_account_storages(evm_account: &H160, index: &H256) -> Option { - get_storage_double_map( - "Evm", - "AccountStorages", - evm_account, - &StorageHasher::Blake2_128Concat, - index, - &StorageHasher::Blake2_128Concat, - ) -} - -// FIXME: Once events are available, these addresses should be read from events. -pub fn evm_create_address(caller: H160, nonce: Nonce) -> H160 { - let mut stream = rlp::RlpStream::new_list(2); - stream.append(&caller); - stream.append(&nonce); - H256::from_slice(Keccak256::digest(&stream.out()).as_slice()).into() -} - -// FIXME: Once events are available, these addresses should be read from events. -pub fn evm_create2_address(caller: H160, salt: H256, code_hash: H256) -> H160 { - let mut hasher = Keccak256::new(); - hasher.update([0xff]); - hasher.update(&caller[..]); - hasher.update(&salt[..]); - hasher.update(&code_hash[..]); - H256::from_slice(hasher.finalize().as_slice()).into() -} - -pub fn create_code_hash(code: &[u8]) -> H256 { - H256::from_slice(Keccak256::digest(code).as_slice()) -} - -pub fn get_evm_account(account: &AccountId) -> H160 { - let mut evm_acc_slice: [u8; 20] = [0; 20]; - evm_acc_slice.copy_from_slice((<[u8; 32]>::from(account.clone())).get(0..20).unwrap()); - evm_acc_slice.into() -} diff --git a/bitacross-worker/app-libs/stf/src/getter.rs b/bitacross-worker/app-libs/stf/src/getter.rs index 9a4319b962..ba4c9ec363 100644 --- a/bitacross-worker/app-libs/stf/src/getter.rs +++ b/bitacross-worker/app-libs/stf/src/getter.rs @@ -26,15 +26,7 @@ use log::*; use sp_std::vec; use std::prelude::v1::*; -#[cfg(feature = "evm")] -use ita_sgx_runtime::{AddressMapping, HashedAddressMapping}; - -#[cfg(feature = "evm")] -use crate::evm_helpers::{get_evm_account, get_evm_account_codes, get_evm_account_storages}; - use itp_stf_primitives::traits::PoolTransactionValidation; -#[cfg(feature = "evm")] -use sp_core::{H160, H256}; use sp_runtime::transaction_validity::{ TransactionValidityError, UnknownTransaction, ValidTransaction, }; @@ -109,15 +101,6 @@ pub enum TrustedGetter { free_balance(Identity), #[codec(index = 1)] reserved_balance(Identity), - #[cfg(feature = "evm")] - #[codec(index = 2)] - evm_nonce(Identity), - #[cfg(feature = "evm")] - #[codec(index = 3)] - evm_account_codes(Identity, H160), - #[cfg(feature = "evm")] - #[codec(index = 4)] - evm_account_storages(Identity, H160, H256), } impl TrustedGetter { @@ -125,12 +108,6 @@ impl TrustedGetter { match self { TrustedGetter::free_balance(sender_identity) => sender_identity, TrustedGetter::reserved_balance(sender_identity) => sender_identity, - #[cfg(feature = "evm")] - TrustedGetter::evm_nonce(sender_identity) => sender_identity, - #[cfg(feature = "evm")] - TrustedGetter::evm_account_codes(sender_identity, _) => sender_identity, - #[cfg(feature = "evm")] - TrustedGetter::evm_account_storages(sender_identity, ..) => sender_identity, } } @@ -208,38 +185,6 @@ impl ExecuteGetter for TrustedGetterSigned { } else { None }, - #[cfg(feature = "evm")] - TrustedGetter::evm_nonce(who) => - if let Some(account_id) = who.to_account_id() { - let evm_account = get_evm_account(&account_id); - let evm_account = HashedAddressMapping::into_account_id(evm_account); - let nonce = System::account_nonce(&evm_account); - debug!("TrustedGetter evm_nonce"); - debug!("Account nonce is {}", nonce); - Some(nonce.encode()) - } else { - None - }, - #[cfg(feature = "evm")] - TrustedGetter::evm_account_codes(_who, evm_account) => - // TODO: This probably needs some security check if who == evm_account (or assosciated) - if let Some(info) = get_evm_account_codes(&evm_account) { - debug!("TrustedGetter Evm Account Codes"); - debug!("AccountCodes for {} is {:?}", evm_account, info); - Some(info) // TOOD: encoded? - } else { - None - }, - #[cfg(feature = "evm")] - TrustedGetter::evm_account_storages(_who, evm_account, index) => - // TODO: This probably needs some security check if who == evm_account (or assosciated) - if let Some(value) = get_evm_account_storages(&evm_account, &index) { - debug!("TrustedGetter Evm Account Storages"); - debug!("AccountStorages for {} is {:?}", evm_account, value); - Some(value.encode()) - } else { - None - }, } } diff --git a/bitacross-worker/app-libs/stf/src/lib.rs b/bitacross-worker/app-libs/stf/src/lib.rs index 894633b49f..4aa3ff1db4 100644 --- a/bitacross-worker/app-libs/stf/src/lib.rs +++ b/bitacross-worker/app-libs/stf/src/lib.rs @@ -33,8 +33,6 @@ pub use getter::*; pub use stf_sgx_primitives::{types::*, Stf}; pub use trusted_call::*; -#[cfg(feature = "evm")] -pub mod evm_helpers; pub mod getter; pub mod hash; pub mod helpers; diff --git a/bitacross-worker/app-libs/stf/src/test_genesis.rs b/bitacross-worker/app-libs/stf/src/test_genesis.rs index 161dec8e5e..507fb6ffb5 100644 --- a/bitacross-worker/app-libs/stf/src/test_genesis.rs +++ b/bitacross-worker/app-libs/stf/src/test_genesis.rs @@ -25,12 +25,6 @@ use sp_core::{crypto::AccountId32, ed25519, Pair}; use sp_runtime::MultiAddress; use std::{format, vec, vec::Vec}; -#[cfg(feature = "evm")] -use ita_sgx_runtime::{AddressMapping, HashedAddressMapping}; - -#[cfg(feature = "evm")] -use crate::evm_helpers::get_evm_account; - type Seed = [u8; 32]; const ALICE_ENCODED: Seed = [ @@ -68,22 +62,9 @@ pub fn test_genesis_setup(state: &mut impl SgxExternalitiesTrait) { (ALICE_ENCODED.into(), ALICE_FUNDS), ]; - append_funded_alice_evm_account(&mut endowees); - endow(state, endowees); } -#[cfg(feature = "evm")] -fn append_funded_alice_evm_account(endowees: &mut Vec<(AccountId32, Balance)>) { - let alice_evm = get_evm_account(&ALICE_ENCODED.into()); - let alice_evm_substrate_version = HashedAddressMapping::into_account_id(alice_evm); - let mut other: Vec<(AccountId32, Balance)> = vec![(alice_evm_substrate_version, ALICE_FUNDS)]; - endowees.append(other.as_mut()); -} - -#[cfg(not(feature = "evm"))] -fn append_funded_alice_evm_account(_: &mut Vec<(AccountId32, Balance)>) {} - fn set_sudo_account(state: &mut impl SgxExternalitiesTrait, account_encoded: &[u8]) { state.execute_with(|| { sp_io::storage::set(&storage_value_key("Sudo", "Key"), account_encoded); diff --git a/bitacross-worker/app-libs/stf/src/trusted_call.rs b/bitacross-worker/app-libs/stf/src/trusted_call.rs index 028c9e1d0d..de6c01d03a 100644 --- a/bitacross-worker/app-libs/stf/src/trusted_call.rs +++ b/bitacross-worker/app-libs/stf/src/trusted_call.rs @@ -15,14 +15,6 @@ */ -#[cfg(feature = "evm")] -use sp_core::{H160, U256}; - -#[cfg(feature = "evm")] -use std::vec::Vec; - -#[cfg(feature = "evm")] -use crate::evm_helpers::{create_code_hash, evm_create2_address, evm_create_address}; use crate::{ helpers::{enclave_signer_account, ensure_enclave_signer_account, get_storage_by_key_hash}, trusted_call_result::TrustedCallResult, @@ -30,8 +22,6 @@ use crate::{ }; use codec::{Compact, Decode, Encode}; use frame_support::{ensure, traits::UnfilteredDispatchable}; -#[cfg(feature = "evm")] -use ita_sgx_runtime::{AddressMapping, HashedAddressMapping}; pub use ita_sgx_runtime::{Balance, Index, Runtime, System}; use itp_node_api::metadata::{provider::AccessNodeMetadata, NodeMetadataTrait}; use itp_node_api_metadata::{pallet_balances::BalancesCallIndexes, pallet_proxy::ProxyCallIndexes}; @@ -74,53 +64,6 @@ pub enum TrustedCall { balance_unshield(Identity, AccountId, Balance, ShardIdentifier), // (AccountIncognito, BeneficiaryPublicAccount, Amount, Shard) #[codec(index = 54)] balance_shield(Identity, AccountId, Balance), // (Root, AccountIncognito, Amount) - #[cfg(feature = "evm")] - #[codec(index = 55)] - evm_withdraw(Identity, H160, Balance), // (Origin, Address EVM Account, Value) - // (Origin, Source, Target, Input, Value, Gas limit, Max fee per gas, Max priority fee per gas, Nonce, Access list) - #[cfg(feature = "evm")] - #[codec(index = 56)] - evm_call( - Identity, - H160, - H160, - Vec, - U256, - u64, - U256, - Option, - Option, - Vec<(H160, Vec)>, - ), - // (Origin, Source, Init, Value, Gas limit, Max fee per gas, Max priority fee per gas, Nonce, Access list) - #[cfg(feature = "evm")] - #[codec(index = 57)] - evm_create( - Identity, - H160, - Vec, - U256, - u64, - U256, - Option, - Option, - Vec<(H160, Vec)>, - ), - // (Origin, Source, Init, Salt, Value, Gas limit, Max fee per gas, Max priority fee per gas, Nonce, Access list) - #[cfg(feature = "evm")] - #[codec(index = 58)] - evm_create2( - Identity, - H160, - Vec, - H256, - U256, - u64, - U256, - Option, - Option, - Vec<(H160, Vec)>, - ), } impl TrustedCall { @@ -131,14 +74,6 @@ impl TrustedCall { Self::balance_transfer(sender_identity, ..) => sender_identity, Self::balance_unshield(sender_identity, ..) => sender_identity, Self::balance_shield(sender_identity, ..) => sender_identity, - #[cfg(feature = "evm")] - Self::evm_withdraw(sender_identity, ..) => sender_identity, - #[cfg(feature = "evm")] - Self::evm_call(sender_identity, ..) => sender_identity, - #[cfg(feature = "evm")] - Self::evm_create(sender_identity, ..) => sender_identity, - #[cfg(feature = "evm")] - Self::evm_create2(sender_identity, ..) => sender_identity, } } } @@ -413,131 +348,6 @@ where // Litentry: we don't have publish_hash call in teebag Ok(TrustedCallResult::Empty) }, - #[cfg(feature = "evm")] - TrustedCall::evm_withdraw(from, address, value) => { - debug!("evm_withdraw({}, {}, {})", account_id_to_string(&from), address, value); - ita_sgx_runtime::EvmCall::::withdraw { address, value } - .dispatch_bypass_filter(ita_sgx_runtime::RuntimeOrigin::signed( - from.to_account_id().ok_or(Self::Error::InvalidAccount)?, - )) - .map_err(|e| { - Self::Error::Dispatch(format!("Evm Withdraw error: {:?}", e.error)) - })?; - Ok(TrustedCallResult::Empty) - }, - #[cfg(feature = "evm")] - TrustedCall::evm_call( - from, - source, - target, - input, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - ) => { - debug!( - "evm_call(from: {}, source: {}, target: {})", - account_id_to_string(&from), - source, - target - ); - ita_sgx_runtime::EvmCall::::call { - source, - target, - input, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - } - .dispatch_bypass_filter(ita_sgx_runtime::RuntimeOrigin::signed( - from.to_account_id().ok_or(Self::Error::InvalidAccount)?, - )) - .map_err(|e| Self::Error::Dispatch(format!("Evm Call error: {:?}", e.error)))?; - Ok(TrustedCallResult::Empty) - }, - #[cfg(feature = "evm")] - TrustedCall::evm_create( - from, - source, - init, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - ) => { - debug!( - "evm_create(from: {}, source: {}, value: {})", - account_id_to_string(&from), - source, - value - ); - let nonce_evm_account = - System::account_nonce(&HashedAddressMapping::into_account_id(source)); - ita_sgx_runtime::EvmCall::::create { - source, - init, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - } - .dispatch_bypass_filter(ita_sgx_runtime::RuntimeOrigin::signed( - from.to_account_id().ok_or(Self::Error::InvalidAccount)?, - )) - .map_err(|e| Self::Error::Dispatch(format!("Evm Create error: {:?}", e.error)))?; - let contract_address = evm_create_address(source, nonce_evm_account); - info!("Trying to create evm contract with address {:?}", contract_address); - Ok(TrustedCallResult::Empty) - }, - #[cfg(feature = "evm")] - TrustedCall::evm_create2( - from, - source, - init, - salt, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - ) => { - debug!( - "evm_create2(from: {}, source: {}, value: {})", - account_id_to_string(&from), - source, - value - ); - let code_hash = create_code_hash(&init); - ita_sgx_runtime::EvmCall::::create2 { - source, - init, - salt, - value, - gas_limit, - max_fee_per_gas, - max_priority_fee_per_gas, - nonce, - access_list, - } - .dispatch_bypass_filter(ita_sgx_runtime::RuntimeOrigin::signed( - from.to_account_id().ok_or(Self::Error::InvalidAccount)?, - )) - .map_err(|e| Self::Error::Dispatch(format!("Evm Create2 error: {:?}", e.error)))?; - let contract_address = evm_create2_address(source, salt, code_hash); - info!("Trying to create evm contract with address {:?}", contract_address); - Ok(TrustedCallResult::Empty) - }, } } diff --git a/bitacross-worker/cli/Cargo.toml b/bitacross-worker/cli/Cargo.toml index eb02dc1117..50119e7939 100644 --- a/bitacross-worker/cli/Cargo.toml +++ b/bitacross-worker/cli/Cargo.toml @@ -25,7 +25,6 @@ thiserror = "1.0" urlencoding = "2.1.3" # scs / integritee -pallet-evm = { optional = true, git = "https://github.com/integritee-network/frontier.git", branch = "bar/polkadot-v0.9.42" } # `default-features = false` to remove the jsonrpsee dependency. substrate-api-client = { default-features = false, features = ["std", "sync-api"], git = "https://github.com/scs/substrate-api-client.git", branch = "polkadot-v0.9.42-tag-v0.14.0" } substrate-client-keystore = { git = "https://github.com/scs/substrate-api-client.git", branch = "polkadot-v0.9.42-tag-v0.14.0" } @@ -56,7 +55,6 @@ pallet-teerex = { path = "../../pallets/teerex", default-features = false } [features] default = [] -evm = ["ita-stf/evm_std", "pallet-evm"] offchain-worker = [] production = [] # dcap feature flag is not used in this crate, but for easier build purposes only it present here as well diff --git a/bitacross-worker/cli/src/evm/commands/evm_call.rs b/bitacross-worker/cli/src/evm/commands/evm_call.rs deleted file mode 100644 index 04a7b56879..0000000000 --- a/bitacross-worker/cli/src/evm/commands/evm_call.rs +++ /dev/null @@ -1,87 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - get_layer_two_evm_nonce, get_layer_two_nonce, - trusted_cli::TrustedCli, - trusted_command_utils::{get_identifiers, get_pair_from_str}, - trusted_operation::perform_trusted_operation, - Cli, CliResult, CliResultOk, -}; -use ita_stf::{Index, TrustedCall, TrustedGetter}; -use itp_stf_primitives::{ - traits::TrustedCallSigning, - types::{KeyPair, TrustedOperation}, -}; -use itp_types::AccountId; -use log::*; -use sp_core::{crypto::Ss58Codec, Pair, H160, U256}; -use std::{boxed::Box, vec::Vec}; -#[derive(Parser)] -pub struct EvmCallCommands { - /// Sender's incognito AccountId in ss58check format - from: String, - - /// Execution address of the smart contract - execution_address: String, - - /// Function hash - function: String, -} - -impl EvmCallCommands { - pub(crate) fn run(&self, cli: &Cli, trusted_args: &TrustedCli) -> CliResult { - let sender = get_pair_from_str(trusted_args, &self.from, cli); - let sender_acc: AccountId = sender.public().into(); - - info!("senders ss58 is {}", sender.public().to_ss58check()); - - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - - info!("senders evm account is {}", sender_evm_acc); - - let execution_address = - H160::from_slice(&array_bytes::hex2bytes(&self.execution_address).unwrap()); - - let function_hash = array_bytes::hex2bytes(&self.function).unwrap(); - - let (mrenclave, shard) = get_identifiers(trusted_args, cli); - let nonce = get_layer_two_nonce!(sender, cli, trusted_args); - let evm_nonce = get_layer_two_evm_nonce!(sender, cli, trusted_args); - - println!("calling smart contract function"); - let function_call = TrustedCall::evm_call( - sender_acc.into(), - sender_evm_acc, - execution_address, - function_hash, - U256::from(0), - 10_000_000, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - Some(U256::from(evm_nonce)), - Vec::new(), - ) - .sign(&KeyPair::Sr25519(Box::new(sender)), nonce, &mrenclave, &shard) - .into_trusted_operation(trusted_args.direct); - Ok(perform_trusted_operation::<()>(cli, trusted_args, &function_call) - .map(|_| CliResultOk::None)?) - } -} diff --git a/bitacross-worker/cli/src/evm/commands/evm_command_utils.rs b/bitacross-worker/cli/src/evm/commands/evm_command_utils.rs deleted file mode 100644 index cc8c5fff34..0000000000 --- a/bitacross-worker/cli/src/evm/commands/evm_command_utils.rs +++ /dev/null @@ -1,32 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -#[macro_export] -macro_rules! get_layer_two_evm_nonce { - ($signer_pair:ident, $cli:ident, $trusted_args:ident ) => {{ - use ita_stf::{Getter, TrustedCallSigned}; - - let top = TrustedOperation::::get(Getter::trusted( - TrustedGetter::evm_nonce($signer_pair.public().into()) - .sign(&KeyPair::Sr25519(Box::new($signer_pair.clone()))), - )); - let res = perform_trusted_operation::($cli, $trusted_args, &top); - let nonce = res.ok().unwrap_or(0); - debug!("got evm nonce: {:?}", nonce); - nonce - }}; -} diff --git a/bitacross-worker/cli/src/evm/commands/evm_create.rs b/bitacross-worker/cli/src/evm/commands/evm_create.rs deleted file mode 100644 index acce77e3e5..0000000000 --- a/bitacross-worker/cli/src/evm/commands/evm_create.rs +++ /dev/null @@ -1,89 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - get_layer_two_evm_nonce, get_layer_two_nonce, - trusted_cli::TrustedCli, - trusted_command_utils::{get_identifiers, get_pair_from_str}, - trusted_operation::perform_trusted_operation, - Cli, CliResult, CliResultOk, -}; -use ita_stf::{evm_helpers::evm_create_address, Index, TrustedCall, TrustedGetter}; -use itp_stf_primitives::{ - traits::TrustedCallSigning, - types::{KeyPair, TrustedOperation}, -}; -use itp_types::AccountId; -use log::*; -use pallet_evm::{AddressMapping, HashedAddressMapping}; -use sp_core::{crypto::Ss58Codec, Pair, H160, U256}; -use sp_runtime::traits::BlakeTwo256; -use std::vec::Vec; -#[derive(Parser)] -pub struct EvmCreateCommands { - /// Sender's incognito AccountId in ss58check format - from: String, - - /// Smart Contract in Hex format - smart_contract: String, -} - -impl EvmCreateCommands { - pub(crate) fn run(&self, cli: &Cli, trusted_args: &TrustedCli) -> CliResult { - let from = get_pair_from_str(trusted_args, &self.from, cli); - let from_acc: AccountId = from.public().into(); - println!("from ss58 is {}", from.public().to_ss58check()); - - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(from_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - - let (mrenclave, shard) = get_identifiers(trusted_args, cli); - - let sender_evm_substrate_addr = - HashedAddressMapping::::into_account_id(sender_evm_acc); - println!( - "Trying to get nonce of evm account {:?}", - sender_evm_substrate_addr.to_ss58check() - ); - - let nonce = get_layer_two_nonce!(from, cli, trusted_args); - let evm_account_nonce = get_layer_two_evm_nonce!(from, cli, trusted_args); - - let top = TrustedCall::evm_create( - from_acc.into(), - sender_evm_acc, - array_bytes::hex2bytes(&self.smart_contract).unwrap().to_vec(), - U256::from(0), - 967295, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - None, - Vec::new(), - ) - .sign(&from.into(), nonce, &mrenclave, &shard) - .into_trusted_operation(trusted_args.direct); - - perform_trusted_operation::<()>(cli, trusted_args, &top)?; - - let execution_address = evm_create_address(sender_evm_acc, evm_account_nonce); - info!("trusted call evm_create executed"); - println!("Created the smart contract with address {:?}", execution_address); - Ok(CliResultOk::H160 { hash: execution_address }) - } -} diff --git a/bitacross-worker/cli/src/evm/commands/evm_read.rs b/bitacross-worker/cli/src/evm/commands/evm_read.rs deleted file mode 100644 index b863533860..0000000000 --- a/bitacross-worker/cli/src/evm/commands/evm_read.rs +++ /dev/null @@ -1,69 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - trusted_cli::TrustedCli, trusted_command_utils::get_pair_from_str, - trusted_operation::perform_trusted_operation, Cli, CliError, CliResult, CliResultOk, -}; -use ita_stf::{Getter, TrustedCallSigned, TrustedGetter}; -use itp_stf_primitives::types::{KeyPair, TrustedOperation}; -use itp_types::AccountId; -use log::*; -use sp_core::{crypto::Ss58Codec, Pair, H160, H256}; - -#[derive(Parser)] -pub struct EvmReadCommands { - /// Sender's incognito AccountId in ss58check format - from: String, - - /// Execution address of the smart contract - execution_address: String, -} - -impl EvmReadCommands { - pub(crate) fn run(&self, cli: &Cli, trusted_args: &TrustedCli) -> CliResult { - let sender = get_pair_from_str(trusted_args, &self.from, cli); - let sender_acc: AccountId = sender.public().into(); - - info!("senders ss58 is {}", sender.public().to_ss58check()); - - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - - info!("senders evm account is {}", sender_evm_acc); - - let execution_address = - H160::from_slice(&array_bytes::hex2bytes(&self.execution_address).unwrap()); - - let top = TrustedOperation::::get(Getter::trusted( - TrustedGetter::evm_account_storages(sender_acc.into(), execution_address, H256::zero()) - .sign(&KeyPair::Sr25519(Box::new(sender))), - )); - match perform_trusted_operation::(cli, trusted_args, &top) { - Ok(hash) => { - println!("{:?}", hash); - Ok(CliResultOk::H256 { hash }) - }, - Err(e) => { - error!("Nothing in state! Reason: {:?} !", e); - Err(CliError::EvmRead { msg: "Nothing in state!".to_string() }) - }, - } - } -} diff --git a/bitacross-worker/cli/src/evm/commands/mod.rs b/bitacross-worker/cli/src/evm/commands/mod.rs deleted file mode 100644 index 014b093832..0000000000 --- a/bitacross-worker/cli/src/evm/commands/mod.rs +++ /dev/null @@ -1,6 +0,0 @@ -pub mod evm_call; -pub mod evm_command_utils; -pub mod evm_create; -pub mod evm_read; - -pub use crate::get_layer_two_evm_nonce; diff --git a/bitacross-worker/cli/src/evm/mod.rs b/bitacross-worker/cli/src/evm/mod.rs deleted file mode 100644 index 0b1ff31d47..0000000000 --- a/bitacross-worker/cli/src/evm/mod.rs +++ /dev/null @@ -1,49 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - - 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. - -*/ - -use crate::{ - evm::commands::{ - evm_call::EvmCallCommands, evm_create::EvmCreateCommands, evm_read::EvmReadCommands, - }, - trusted_cli::TrustedCli, - Cli, CliResult, -}; - -mod commands; - -#[allow(clippy::enum_variant_names)] -#[derive(Subcommand)] -pub enum EvmCommand { - /// Create smart contract - EvmCreate(EvmCreateCommands), - - /// Read smart contract storage - EvmRead(EvmReadCommands), - - /// Create smart contract - EvmCall(EvmCallCommands), -} - -impl EvmCommand { - pub fn run(&self, cli: &Cli, trusted_args: &TrustedCli) -> CliResult { - match self { - EvmCommand::EvmCreate(cmd) => cmd.run(cli, trusted_args), - EvmCommand::EvmRead(cmd) => cmd.run(cli, trusted_args), - EvmCommand::EvmCall(cmd) => cmd.run(cli, trusted_args), - } - } -} diff --git a/bitacross-worker/cli/src/lib.rs b/bitacross-worker/cli/src/lib.rs index e07909c196..16fb72fd42 100644 --- a/bitacross-worker/cli/src/lib.rs +++ b/bitacross-worker/cli/src/lib.rs @@ -33,8 +33,6 @@ mod base_cli; mod benchmark; mod command_utils; mod error; -#[cfg(feature = "evm")] -mod evm; mod trusted_base_cli; mod trusted_cli; mod trusted_command_utils; @@ -46,7 +44,7 @@ use crate::commands::Commands; use clap::Parser; use itp_node_api::api_client::Metadata; use sp_application_crypto::KeyTypeId; -use sp_core::{H160, H256}; +use sp_core::H256; use thiserror::Error; const VERSION: &str = env!("CARGO_PKG_VERSION"); @@ -82,26 +80,11 @@ pub struct Cli { } pub enum CliResultOk { - PubKeysBase58 { - pubkeys_sr25519: Option>, - pubkeys_ed25519: Option>, - }, - Balance { - balance: u128, - }, - MrEnclaveBase58 { - mr_enclaves: Vec, - }, - Metadata { - metadata: Metadata, - }, - H256 { - hash: H256, - }, - /// Result of "EvmCreateCommands": execution_address - H160 { - hash: H160, - }, + PubKeysBase58 { pubkeys_sr25519: Option>, pubkeys_ed25519: Option> }, + Balance { balance: u128 }, + MrEnclaveBase58 { mr_enclaves: Vec }, + Metadata { metadata: Metadata }, + H256 { hash: H256 }, // TODO should ideally be removed; or at least drastically less used // We WANT all commands exposed by the cli to return something useful for the caller(ie instead of printing) None, @@ -113,8 +96,6 @@ pub enum CliError { Extrinsic { msg: String }, #[error("trusted operation error: {:?}", msg)] TrustedOp { msg: String }, - #[error("EvmReadCommands error: {:?}", msg)] - EvmRead { msg: String }, #[error("worker rpc api error: {:?}", msg)] WorkerRpcApi { msg: String }, } diff --git a/bitacross-worker/cli/src/trusted_cli.rs b/bitacross-worker/cli/src/trusted_cli.rs index 5c1f5d6553..f0fffe8b96 100644 --- a/bitacross-worker/cli/src/trusted_cli.rs +++ b/bitacross-worker/cli/src/trusted_cli.rs @@ -17,8 +17,6 @@ use crate::{benchmark::BenchmarkCommand, Cli, CliResult}; -#[cfg(feature = "evm")] -use crate::evm::EvmCommand; use crate::trusted_base_cli::TrustedBaseCommand; #[derive(Args)] @@ -48,10 +46,6 @@ pub enum TrustedCommand { #[clap(flatten)] BaseTrusted(TrustedBaseCommand), - #[cfg(feature = "evm")] - #[clap(flatten)] - EvmCommands(EvmCommand), - /// Run Benchmark Benchmark(BenchmarkCommand), } @@ -61,8 +55,6 @@ impl TrustedCli { match &self.command { TrustedCommand::BaseTrusted(cmd) => cmd.run(cli, self), TrustedCommand::Benchmark(cmd) => cmd.run(cli, self), - #[cfg(feature = "evm")] - TrustedCommand::EvmCommands(cmd) => cmd.run(cli, self), } } } diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index 7b78fec982..becf26aa46 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -630,7 +630,7 @@ dependencies = [ "litentry-hex-utils", "litentry-macros", "litentry-proc-macros", - "pallet-evm 6.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "pallet-evm", "parity-scale-codec", "ring 0.16.20", "scale-info", @@ -1055,25 +1055,6 @@ dependencies = [ "uint", ] -[[package]] -name = "evm" -version = "0.39.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a49a4e11987c51220aa89dbe1a5cc877f5079fa6864c0a5b4533331db44e9365" -dependencies = [ - "auto_impl", - "ethereum", - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-gasometer 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-runtime 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "log", - "parity-scale-codec", - "primitive-types", - "rlp", - "scale-info", - "sha3 0.10.8", -] - [[package]] name = "evm" version = "0.39.1" @@ -1081,9 +1062,9 @@ source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa dependencies = [ "auto_impl", "ethereum", - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-gasometer 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-runtime 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core", + "evm-gasometer", + "evm-runtime", "log", "parity-scale-codec", "primitive-types", @@ -1092,17 +1073,6 @@ dependencies = [ "sha3 0.10.8", ] -[[package]] -name = "evm-core" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f1f13264b044cb66f0602180f0bc781c29accb41ff560669a3ec15858d5b606" -dependencies = [ - "parity-scale-codec", - "primitive-types", - "scale-info", -] - [[package]] name = "evm-core" version = "0.39.0" @@ -1113,46 +1083,23 @@ dependencies = [ "scale-info", ] -[[package]] -name = "evm-gasometer" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d43eadc395bd1a52990787ca1495c26b0248165444912be075c28909a853b8c" -dependencies = [ - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "evm-runtime 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "primitive-types", -] - [[package]] name = "evm-gasometer" version = "0.39.0" source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" dependencies = [ - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "evm-runtime 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core", + "evm-runtime", "primitive-types", ] -[[package]] -name = "evm-runtime" -version = "0.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2aa5b32f59ec582a5651978004e5c784920291263b7dcb6de418047438e37f4f" -dependencies = [ - "auto_impl", - "evm-core 0.39.0 (registry+https://github.com/rust-lang/crates.io-index)", - "primitive-types", - "sha3 0.10.8", -] - [[package]] name = "evm-runtime" version = "0.39.0" source = "git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65#b7b82c7e1fc57b7449d6dfa6826600de37cc1e65" dependencies = [ "auto_impl", - "evm-core 0.39.0 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm-core", "primitive-types", "sha3 0.10.8", ] @@ -1230,22 +1177,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "fp-account" -version = "1.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "hex 0.4.3", - "libsecp256k1", - "log", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-io", - "sp-runtime", - "sp-std", -] - [[package]] name = "fp-account" version = "1.0.0-dev" @@ -1263,26 +1194,12 @@ dependencies = [ "sp-std", ] -[[package]] -name = "fp-evm" -version = "3.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "evm 0.39.1 (registry+https://github.com/rust-lang/crates.io-index)", - "frame-support", - "parity-scale-codec", - "scale-info", - "sp-core", - "sp-runtime", - "sp-std", -] - [[package]] name = "fp-evm" version = "3.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", + "evm", "frame-support", "parity-scale-codec", "scale-info", @@ -1903,7 +1820,6 @@ dependencies = [ "frame-system", "itp-sgx-runtime-primitives", "pallet-balances", - "pallet-evm 6.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", "pallet-parentchain", "pallet-sudo", "pallet-timestamp", @@ -3062,36 +2978,14 @@ dependencies = [ "sp-std", ] -[[package]] -name = "pallet-evm" -version = "6.0.0-dev" -source = "git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42#a5a5e1e6ec08cd542a6084c310863150fb8841b1" -dependencies = [ - "evm 0.39.1 (registry+https://github.com/rust-lang/crates.io-index)", - "fp-account 1.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "fp-evm 3.0.0-dev (git+https://github.com/integritee-network/frontier.git?branch=bar/polkadot-v0.9.42)", - "frame-support", - "frame-system", - "hex 0.4.3", - "impl-trait-for-tuples", - "log", - "parity-scale-codec", - "rlp", - "scale-info", - "sp-core", - "sp-io", - "sp-runtime", - "sp-std", -] - [[package]] name = "pallet-evm" version = "6.0.0-dev" source = "git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42#2499d18c936edbcb7fcb711827db7abb9b4f4da4" dependencies = [ - "evm 0.39.1 (git+https://github.com/rust-blockchain/evm?rev=b7b82c7e1fc57b7449d6dfa6826600de37cc1e65)", - "fp-account 1.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", - "fp-evm 3.0.0-dev (git+https://github.com/paritytech/frontier?branch=polkadot-v0.9.42)", + "evm", + "fp-account", + "fp-evm", "frame-support", "frame-system", "hex 0.4.3", diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 19acc0e263..0375e663e3 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -13,10 +13,6 @@ crate-type = ["staticlib"] [features] default = [] -evm = [ - "ita-sgx-runtime/evm", - "ita-stf/evm", -] production = [ "ita-stf/production", "itp-settings/production", diff --git a/bitacross-worker/enclave-runtime/src/test/evm_pallet_tests.rs b/bitacross-worker/enclave-runtime/src/test/evm_pallet_tests.rs deleted file mode 100644 index 61a8912e2e..0000000000 --- a/bitacross-worker/enclave-runtime/src/test/evm_pallet_tests.rs +++ /dev/null @@ -1,401 +0,0 @@ -/* - Copyright 2021 Integritee AG and Supercomputing Systems AG - 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. -*/ - -use crate::test::fixtures::test_setup::{test_setup, TestStf}; -use core::str::FromStr; -use ita_sgx_runtime::{AddressMapping, HashedAddressMapping, Index, System}; -use ita_stf::{ - evm_helpers::{ - create_code_hash, evm_create2_address, evm_create_address, get_evm_account_codes, - get_evm_account_storages, - }, - test_genesis::{endow, endowed_account as funded_pair}, - State, TrustedCall, -}; -use itp_node_api::metadata::{metadata_mocks::NodeMetadataMock, provider::NodeMetadataRepository}; -use itp_sgx_externalities::SgxExternalitiesTrait; -use itp_stf_interface::StateCallInterface; -use itp_stf_primitives::{traits::TrustedCallSigning, types::KeyPair}; -use itp_types::{parentchain::ParentchainCall, AccountId, ShardIdentifier}; -use primitive_types::H256; -use sp_core::{crypto::Pair, H160, U256}; -use std::{sync::Arc, vec::Vec}; - -pub fn test_evm_call() { - // given - let (_, mut state, shard, mrenclave, ..) = test_setup(); - let mut parentchain_calls = Vec::new(); - - // Create the sender account. - let sender = funded_pair(); - let sender_acc: AccountId = sender.public().into(); - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - // Ensure the substrate version of the evm account has some money. - let sender_evm_substrate_addr = - ita_sgx_runtime::HashedAddressMapping::into_account_id(sender_evm_acc); - endow(&mut state, vec![(sender_evm_substrate_addr, 51_777_000_000_000)]); - - // Create the receiver account. - let destination_evm_acc = H160::from_str("1000000000000000000000000000000000000001").unwrap(); - let destination_evm_substrate_addr = - ita_sgx_runtime::HashedAddressMapping::into_account_id(destination_evm_acc); - assert_eq!( - state.execute_with(|| System::account(&destination_evm_substrate_addr).data.free), - 0 - ); - - let transfer_value: u128 = 1_000_000_000; - - let trusted_call = TrustedCall::evm_call( - sender_acc, - sender_evm_acc, - destination_evm_acc, - Vec::new(), - U256::from(transfer_value), - 21776, // gas limit - U256::from(1_000_000_000), - None, - Some(U256::from(0)), - Vec::new(), - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - - // when - let repo = Arc::new(NodeMetadataRepository::::default()); - let shard = ShardIdentifier::default(); - TestStf::execute_call( - &mut state, - &shard, - trusted_call, - Default::default(), - &mut parentchain_calls, - repo, - ) - .unwrap(); - - // then - assert_eq!( - transfer_value, - state.execute_with(|| System::account(&destination_evm_substrate_addr).data.free) - ); -} - -pub fn test_evm_counter() { - // given - let (_, mut state, shard, mrenclave, ..) = test_setup(); - let mut parentchain_calls = Vec::new(); - - // Create the sender account. - let sender = funded_pair(); - let sender_acc: AccountId = sender.public().into(); - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - // Ensure the substrate version of the evm account has some money. - let sender_evm_substrate_addr = - ita_sgx_runtime::HashedAddressMapping::into_account_id(sender_evm_acc); - endow(&mut state, vec![(sender_evm_substrate_addr, 51_777_000_000_000)]); - - // Smart Contract from Counter.sol. - let smart_contract = "608060405234801561001057600080fd5b50600160008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610377806100696000396000f3fe6080604052600436106100435760003560e01c80631003e2d21461004d57806333cf508014610076578063371303c0146100a157806358992216146100b857610044565b5b60056000819055005b34801561005957600080fd5b50610074600480360381019061006f9190610209565b6100e3565b005b34801561008257600080fd5b5061008b61013f565b6040516100989190610245565b60405180910390f35b3480156100ad57600080fd5b506100b6610148565b005b3480156100c457600080fd5b506100cd6101a4565b6040516100da91906102a1565b60405180910390f35b806000808282546100f491906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054905090565b600160008082825461015a91906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b6000819050919050565b6101e6816101d3565b81146101f157600080fd5b50565b600081359050610203816101dd565b92915050565b60006020828403121561021f5761021e6101ce565b5b600061022d848285016101f4565b91505092915050565b61023f816101d3565b82525050565b600060208201905061025a6000830184610236565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028b82610260565b9050919050565b61029b81610280565b82525050565b60006020820190506102b66000830184610292565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102f6826101d3565b9150610301836101d3565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610336576103356102bc565b5b82820190509291505056fea2646970667358221220b37e993e133ed19c840809cc8acbbba8116dee3744ba01c81044d75146805c9364736f6c634300080f0033"; - - let trusted_call = TrustedCall::evm_create( - sender_acc.clone(), - sender_evm_acc, - array_bytes::hex2bytes(smart_contract).unwrap().to_vec(), - U256::from(0), - 10_000_000, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - Some(U256::from(0)), - Vec::new(), - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - - // when - let execution_address = evm_create_address(sender_evm_acc, 0); - let repo = Arc::new(NodeMetadataRepository::::default()); - let shard = ShardIdentifier::default(); - TestStf::execute_call( - &mut state, - &shard, - trusted_call, - Default::default(), - &mut parentchain_calls, - repo, - ) - .unwrap(); - - // then - assert_eq!( - execution_address, - H160::from_slice( - &array_bytes::hex2bytes("0xce2c9e7f9c10049996173b2ca2d9a6815a70e890").unwrap(), - ) - ); - - assert!(state.execute_with(|| get_evm_account_codes(&execution_address).is_some())); - - let counter_value = state - .execute_with(|| get_evm_account_storages(&execution_address, &H256::zero())) - .unwrap(); - assert_eq!(H256::from_low_u64_be(1), counter_value); - let last_caller = state - .execute_with(|| get_evm_account_storages(&execution_address, &H256::from_low_u64_be(1))) - .unwrap(); - assert_eq!(H256::from(sender_evm_acc), last_caller); - - // Call to inc() function - // in solidity compile information you get the hash of the call - let inc_function_input = array_bytes::hex2bytes("371303c0").unwrap(); - - execute_and_verify_evm_call( - sender_acc.clone(), - sender_evm_acc, - execution_address, - inc_function_input.to_vec(), - 1, - 1, - sender.into(), - &mrenclave, - &shard, - &mut state, - &mut parentchain_calls, - 2, - ); - - // Call the fallback function - execute_and_verify_evm_call( - sender_acc.clone(), - sender_evm_acc, - execution_address, - Vec::new(), // Empty input calls the fallback function. - 2, - 2, - sender.into(), - &mrenclave, - &shard, - &mut state, - &mut parentchain_calls, - 5, - ); - - // Call to inc() function - // in solidity compile information you get the hash of the call - execute_and_verify_evm_call( - sender_acc.clone(), - sender_evm_acc, - execution_address, - inc_function_input, - 3, - 3, - sender.into(), - &mrenclave, - &shard, - &mut state, - &mut parentchain_calls, - 6, - ); - - // Call to add() function - // in solidity compile information you get the hash of the call - let function_hash = "1003e2d2"; - // 32 byte string of the value to add in hex - let add_value = "0000000000000000000000000000000000000000000000000000000000000002"; - let add_function_input = - array_bytes::hex2bytes(&format!("{}{}", function_hash, add_value)).unwrap(); - - execute_and_verify_evm_call( - sender_acc, - sender_evm_acc, - execution_address, - add_function_input, - 4, - 4, - sender.into(), - &mrenclave, - &shard, - &mut state, - &mut parentchain_calls, - 8, - ); -} - -#[allow(clippy::too_many_arguments)] -fn execute_and_verify_evm_call( - sender_acc: AccountId, - sender_evm_acc: H160, - execution_address: H160, - function_input: Vec, - evm_nonce: i8, - nonce: Index, - pair: KeyPair, - mrenclave: &[u8; 32], - shard: &ShardIdentifier, - state: &mut State, - calls: &mut Vec, - counter_expected: u64, -) { - let inc_call = TrustedCall::evm_call( - sender_acc, - sender_evm_acc, - execution_address, - function_input, - U256::from(0), - 10_000_000, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - Some(U256::from(evm_nonce)), - Vec::new(), - ) - .sign(&pair, nonce, mrenclave, shard); - let repo = Arc::new(NodeMetadataRepository::::default()); - let shard = ShardIdentifier::default(); - TestStf::execute_call(state, &shard, inc_call, Default::default(), calls, repo).unwrap(); - - let counter_value = state - .execute_with(|| get_evm_account_storages(&execution_address, &H256::zero())) - .unwrap(); - assert_eq!(counter_value, H256::from_low_u64_be(counter_expected)); -} - -pub fn test_evm_create() { - // given - let (_, mut state, shard, mrenclave, ..) = test_setup(); - let mut parentchain_calls = Vec::new(); - - // Create the sender account. - let sender = funded_pair(); - let sender_acc: AccountId = sender.public().into(); - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - // Ensure the substrate version of the evm account has some money. - let sender_evm_substrate_addr = HashedAddressMapping::into_account_id(sender_evm_acc); - endow(&mut state, vec![(sender_evm_substrate_addr.clone(), 51_777_000_000_000)]); - - // Bytecode from Counter.sol - let smart_contract = "608060405234801561001057600080fd5b50600160008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610377806100696000396000f3fe6080604052600436106100435760003560e01c80631003e2d21461004d57806333cf508014610076578063371303c0146100a157806358992216146100b857610044565b5b60056000819055005b34801561005957600080fd5b50610074600480360381019061006f9190610209565b6100e3565b005b34801561008257600080fd5b5061008b61013f565b6040516100989190610245565b60405180910390f35b3480156100ad57600080fd5b506100b6610148565b005b3480156100c457600080fd5b506100cd6101a4565b6040516100da91906102a1565b60405180910390f35b806000808282546100f491906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054905090565b600160008082825461015a91906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b6000819050919050565b6101e6816101d3565b81146101f157600080fd5b50565b600081359050610203816101dd565b92915050565b60006020828403121561021f5761021e6101ce565b5b600061022d848285016101f4565b91505092915050565b61023f816101d3565b82525050565b600060208201905061025a6000830184610236565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028b82610260565b9050919050565b61029b81610280565b82525050565b60006020820190506102b66000830184610292565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102f6826101d3565b9150610301836101d3565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610336576103356102bc565b5b82820190509291505056fea2646970667358221220b37e993e133ed19c840809cc8acbbba8116dee3744ba01c81044d75146805c9364736f6c634300080f0033"; - let smart_contract = array_bytes::hex2bytes(smart_contract).unwrap(); - - let trusted_call = TrustedCall::evm_create( - sender_acc, - sender_evm_acc, - smart_contract, - U256::from(0), // value - 10_000_000, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - Some(U256::from(0)), - Vec::new(), - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - - // Should be the first call of the evm account - let nonce = state.execute_with(|| System::account_nonce(&sender_evm_substrate_addr)); - assert_eq!(nonce, 0); - let execution_address = evm_create_address(sender_evm_acc, nonce); - let repo = Arc::new(NodeMetadataRepository::::default()); - let shard = ShardIdentifier::default(); - TestStf::execute_call( - &mut state, - &shard, - trusted_call, - Default::default(), - &mut parentchain_calls, - repo, - ) - .unwrap(); - - assert_eq!( - execution_address, - H160::from_slice( - &array_bytes::hex2bytes("0xce2c9e7f9c10049996173b2ca2d9a6815a70e890").unwrap(), - ) - ); - assert!(state.execute_with(|| get_evm_account_codes(&execution_address).is_some())); - - // Ensure the nonce of the evm account has been increased by one - // Should be the first call of the evm account - let nonce = state.execute_with(|| System::account_nonce(&sender_evm_substrate_addr)); - assert_eq!(nonce, 1); -} - -pub fn test_evm_create2() { - // given - let (_, mut state, shard, mrenclave, ..) = test_setup(); - let mut parentchain_calls = Vec::new(); - - // Create the sender account. - let sender = funded_pair(); - let sender_acc: AccountId = sender.public().into(); - let mut sender_evm_acc_slice: [u8; 20] = [0; 20]; - sender_evm_acc_slice - .copy_from_slice((<[u8; 32]>::from(sender_acc.clone())).get(0..20).unwrap()); - let sender_evm_acc: H160 = sender_evm_acc_slice.into(); - // Ensure the substrate version of the evm account has some money. - let sender_evm_substrate_addr = HashedAddressMapping::into_account_id(sender_evm_acc); - endow(&mut state, vec![(sender_evm_substrate_addr, 51_777_000_000_000)]); - - let salt = H256::from_low_u64_be(20); - // Bytecode from Counter.sol - let smart_contract = "608060405234801561001057600080fd5b50600160008190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550610377806100696000396000f3fe6080604052600436106100435760003560e01c80631003e2d21461004d57806333cf508014610076578063371303c0146100a157806358992216146100b857610044565b5b60056000819055005b34801561005957600080fd5b50610074600480360381019061006f9190610209565b6100e3565b005b34801561008257600080fd5b5061008b61013f565b6040516100989190610245565b60405180910390f35b3480156100ad57600080fd5b506100b6610148565b005b3480156100c457600080fd5b506100cd6101a4565b6040516100da91906102a1565b60405180910390f35b806000808282546100f491906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff16021790555050565b60008054905090565b600160008082825461015a91906102eb565b9250508190555033600160006101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550565b6000600160009054906101000a900473ffffffffffffffffffffffffffffffffffffffff16905090565b600080fd5b6000819050919050565b6101e6816101d3565b81146101f157600080fd5b50565b600081359050610203816101dd565b92915050565b60006020828403121561021f5761021e6101ce565b5b600061022d848285016101f4565b91505092915050565b61023f816101d3565b82525050565b600060208201905061025a6000830184610236565b92915050565b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b600061028b82610260565b9050919050565b61029b81610280565b82525050565b60006020820190506102b66000830184610292565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006102f6826101d3565b9150610301836101d3565b9250827fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03821115610336576103356102bc565b5b82820190509291505056fea2646970667358221220b37e993e133ed19c840809cc8acbbba8116dee3744ba01c81044d75146805c9364736f6c634300080f0033"; - let smart_contract = array_bytes::hex2bytes(smart_contract).unwrap(); - - let trusted_call = TrustedCall::evm_create2( - sender_acc, - sender_evm_acc, - smart_contract.clone(), - salt, - U256::from(0), // value - 10_000_000, // gas limit - U256::from(1), // max_fee_per_gas !>= min_gas_price defined in runtime - None, - Some(U256::from(0)), - Vec::new(), - ) - .sign(&sender.into(), 0, &mrenclave, &shard); - - // when - let code_hash = create_code_hash(&smart_contract); - let execution_address = evm_create2_address(sender_evm_acc, salt, code_hash); - let repo = Arc::new(NodeMetadataRepository::::default()); - let shard = ShardIdentifier::default(); - TestStf::execute_call( - &mut state, - &shard, - trusted_call, - Default::default(), - &mut parentchain_calls, - repo, - ) - .unwrap(); - - // then - assert_eq!( - execution_address, - H160::from_slice( - &array_bytes::hex2bytes("0xe07ad7925f6b2b10c5a7653fb16db7a984059d11").unwrap(), - ) - ); - - assert!(state.execute_with(|| get_evm_account_codes(&execution_address).is_some())); -} diff --git a/bitacross-worker/enclave-runtime/src/test/mod.rs b/bitacross-worker/enclave-runtime/src/test/mod.rs index 2e341e5064..8bdbe375c9 100644 --- a/bitacross-worker/enclave-runtime/src/test/mod.rs +++ b/bitacross-worker/enclave-runtime/src/test/mod.rs @@ -19,8 +19,6 @@ pub mod cert_tests; pub mod direct_rpc_tests; pub mod enclave_signer_tests; -#[cfg(feature = "evm")] -pub mod evm_pallet_tests; pub mod fixtures; pub mod ipfs_tests; pub mod mocks; diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index 9c5a86d6dd..9917244ffa 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -14,9 +14,6 @@ limitations under the License. */ -#[cfg(feature = "evm")] -use crate::test::evm_pallet_tests; - use crate::{ rpc, sync::tests::{enclave_rw_lock_works, sidechain_rw_lock_works}, @@ -151,9 +148,6 @@ pub extern "C" fn test_main_entrance() -> size_t { // RPC tests direct_rpc_tests::get_state_request_works, - // EVM tests - run_evm_tests, - // light-client-test itc_parentchain::light_client::io::sgx_tests::init_parachain_light_client_works, itc_parentchain::light_client::io::sgx_tests::sealing_creates_backup, @@ -166,16 +160,6 @@ pub extern "C" fn test_main_entrance() -> size_t { ) } -#[cfg(feature = "evm")] -fn run_evm_tests() { - evm_pallet_tests::test_evm_call(); - evm_pallet_tests::test_evm_counter(); - evm_pallet_tests::test_evm_create(); - evm_pallet_tests::test_evm_create2(); -} -#[cfg(not(feature = "evm"))] -fn run_evm_tests() {} - fn test_submit_trusted_call_to_top_pool() { // given let (top_pool_author, _, shard, mrenclave, shielding_key, ..) = test_setup(); diff --git a/bitacross-worker/service/Cargo.toml b/bitacross-worker/service/Cargo.toml index b3a05aefb0..9a9caf29d9 100644 --- a/bitacross-worker/service/Cargo.toml +++ b/bitacross-worker/service/Cargo.toml @@ -68,7 +68,6 @@ my-node-runtime = { package = "rococo-parachain-runtime", path = "../../runtime/ [features] default = [] -evm = [] offchain-worker = ["itp-settings/offchain-worker"] production = [ "itp-settings/production", diff --git a/bitacross-worker/service/src/main_impl.rs b/bitacross-worker/service/src/main_impl.rs index 9c50baff94..f6099eaa52 100644 --- a/bitacross-worker/service/src/main_impl.rs +++ b/bitacross-worker/service/src/main_impl.rs @@ -314,10 +314,6 @@ fn start_worker( println!(" Production Mode is enabled"); #[cfg(not(feature = "production"))] println!(" Production Mode is disabled"); - #[cfg(feature = "evm")] - println!(" EVM is enabled"); - #[cfg(not(feature = "evm"))] - println!(" EVM is disabled"); info!("starting worker on shard {}", shard.encode().to_base58()); // ------------------------------------------------------------------------ diff --git a/scripts/pre-commit.sh b/scripts/pre-commit.sh index c53e335759..7d8c45ee59 100755 --- a/scripts/pre-commit.sh +++ b/scripts/pre-commit.sh @@ -13,7 +13,6 @@ function worker_clippy() { function bitacross_clippy() { cargo clippy --release -- -D warnings - cargo clippy --release --features evm -- -D warnings cargo clippy --release --features offchain-worker -- -D warnings } From 5156a9737a898c3ae4444993ff7419aae03ceb2c Mon Sep 17 00:00:00 2001 From: Kasper Ziemianek Date: Tue, 20 Feb 2024 15:05:21 +0100 Subject: [PATCH 51/64] fix state_getMrenclave (#2507) --- .../enclave-runtime/src/initialization/mod.rs | 8 +- .../src/rpc/worker_api_direct.rs | 25 +++--- .../src/test/direct_rpc_tests.rs | 77 +++++++++++++++++-- .../enclave-runtime/src/test/tests_main.rs | 1 + 4 files changed, 86 insertions(+), 25 deletions(-) diff --git a/bitacross-worker/enclave-runtime/src/initialization/mod.rs b/bitacross-worker/enclave-runtime/src/initialization/mod.rs index fbbd7ab234..56415e0921 100644 --- a/bitacross-worker/enclave-runtime/src/initialization/mod.rs +++ b/bitacross-worker/enclave-runtime/src/initialization/mod.rs @@ -205,8 +205,12 @@ pub(crate) fn init_enclave( DIRECT_RPC_REQUEST_SINK_COMPONENT.initialize(request_sink); let getter_executor = Arc::new(EnclaveGetterExecutor::new(state_observer)); - let io_handler = - public_api_rpc_handler(top_pool_author, getter_executor, shielding_key_repository); + let io_handler = public_api_rpc_handler( + top_pool_author, + getter_executor, + shielding_key_repository, + ocall_api.clone(), + ); let rpc_handler = Arc::new(RpcWsHandler::new(io_handler, watch_extractor, connection_registry)); GLOBAL_RPC_WS_HANDLER_COMPONENT.initialize(rpc_handler); diff --git a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs index 009a19786d..61b5ed443c 100644 --- a/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs +++ b/bitacross-worker/enclave-runtime/src/rpc/worker_api_direct.rs @@ -33,6 +33,7 @@ use futures_sgx::channel::oneshot; use ita_sgx_runtime::Runtime; use ita_stf::{Getter, TrustedCallSigned}; use itc_parentchain::light_client::{concurrent_access::ValidatorAccess, ExtrinsicSender}; +use itp_ocall_api::EnclaveAttestationOCallApi; use itp_primitives_cache::{GetPrimitives, GLOBAL_PRIMITIVES_CACHE}; use itp_rpc::RpcReturnValue; use itp_sgx_crypto::{ @@ -68,10 +69,11 @@ fn get_all_rpc_methods_string(io_handler: &IoHandler) -> String { format!("methods: [{}]", method_string) } -pub fn public_api_rpc_handler( +pub fn public_api_rpc_handler( top_pool_author: Arc, getter_executor: Arc, shielding_key: Arc, + ocall_api: Arc, ) -> IoHandler where Author: AuthorApi + Send + Sync + 'static, @@ -79,6 +81,7 @@ where AccessShieldingKey: AccessPubkey + AccessKey + Send + Sync + 'static, ::KeyType: ShieldingCryptoDecrypt + ShieldingCryptoEncrypt + DeriveEd25519 + Send + Sync + 'static, + OcallApi: EnclaveAttestationOCallApi + Send + Sync + 'static, { let mut io = IoHandler::new(); @@ -261,17 +264,16 @@ where }); // state_getMrenclave - io.add_sync_method("state_getMrenclave", |_: Params| { - let json_value = match GLOBAL_SCHEDULED_ENCLAVE.get_current_mrenclave() { - Ok(mrenclave) => RpcReturnValue { + io.add_sync_method("state_getMrenclave", move |_: Params| { + let json_value = match ocall_api.get_mrenclave_of_self() { + Ok(m) => RpcReturnValue { do_watch: false, - value: mrenclave.encode(), + value: m.m.encode(), status: DirectRequestStatus::Ok, } .to_hex(), - Err(error) => { - let error_msg: String = - format!("Could not get current mrenclave due to: {}", error); + Err(e) => { + let error_msg: String = format!("Could not get current mrenclave due to: {}", e); compute_hex_encoded_return_error(error_msg.as_str()) }, }; @@ -467,13 +469,6 @@ fn get_request_payload(params: Params) -> Result { Ok(s.to_owned()) } -pub fn sidechain_io_handler() -> IoHandler -where - Error: std::fmt::Debug, -{ - IoHandler::new() -} - #[cfg(feature = "test")] pub mod tests { use super::*; diff --git a/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs b/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs index 6a88de3a7b..9d9ac24ff5 100644 --- a/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs +++ b/bitacross-worker/enclave-runtime/src/test/direct_rpc_tests.rs @@ -16,13 +16,21 @@ */ -use crate::{rpc::worker_api_direct::public_api_rpc_handler, Hash}; +use crate::{ + rpc::worker_api_direct::public_api_rpc_handler, + test::{ + fixtures::components::create_ocall_api, + mocks::types::{TestOCallApi, TestSigner}, + }, + Hash, +}; use codec::{Decode, Encode}; use ita_stf::{Getter, PublicGetter}; use itc_direct_rpc_server::{ create_determine_watch, rpc_connection_registry::ConnectionRegistry, rpc_ws_handler::RpcWsHandler, }; +use itc_parentchain_test::ParentchainHeaderBuilder; use itc_tls_websocket_server::{ConnectionToken, WebSocketMessageHandler}; use itp_rpc::{Id, RpcRequest, RpcReturnValue}; use itp_sgx_crypto::get_rsa3072_repository; @@ -33,8 +41,57 @@ use itp_top_pool_author::mocks::AuthorApiMock; use itp_types::{DirectRequestStatus, RsaRequest, ShardIdentifier}; use itp_utils::{FromHexPrefixed, ToHexPrefixed}; use litentry_primitives::{Address32, Identity}; +use sp_core::Pair; use std::{string::ToString, sync::Arc, vec::Vec}; +pub fn state_get_mrenclave_works() { + type TestState = u64; + + let temp_dir = TempDir::with_prefix("get_state_request_works").unwrap(); + + let connection_registry = Arc::new(ConnectionRegistry::::new()); + let watch_extractor = Arc::new(create_determine_watch::()); + let rsa_repository = get_rsa3072_repository(temp_dir.path().to_path_buf()).unwrap(); + + let mr_enclave = [1; 32]; + + let ocall_api = TestOCallApi::default().with_mr_enclave(mr_enclave.clone()); + + let state: TestState = 78234u64; + let state_observer = Arc::new(ObserveStateMock::::new(state)); + let getter_executor = + Arc::new(GetterExecutor::<_, GetStateMock, Getter>::new(state_observer)); + let top_pool_author = Arc::new(AuthorApiMock::default()); + + let io_handler = public_api_rpc_handler( + top_pool_author, + getter_executor, + Arc::new(rsa_repository), + ocall_api.into(), + ); + let rpc_handler = Arc::new(RpcWsHandler::new(io_handler, watch_extractor, connection_registry)); + + let request_string = RpcRequest::compose_jsonrpc_call( + Id::Text("1".to_string()), + "state_getMrenclave".to_string(), + vec![], + ) + .unwrap(); + + let response_string = + rpc_handler.handle_message(ConnectionToken(1), request_string).unwrap().unwrap(); + + assert!(!response_string.is_empty()); + + const EXPECTED_HEX_RETURN_VALUE: &str = + "0x8001010101010101010101010101010101010101010101010101010101010101010000"; + assert!(response_string.contains(EXPECTED_HEX_RETURN_VALUE)); + let rpc_return_value = RpcReturnValue::from_hex(EXPECTED_HEX_RETURN_VALUE).unwrap(); + assert_eq!(rpc_return_value.status, DirectRequestStatus::Ok); + let decoded_value: [u8; 32] = Decode::decode(&mut rpc_return_value.value.as_slice()).unwrap(); + assert_eq!(decoded_value, mr_enclave); +} + pub fn get_state_request_works() { type TestState = u64; @@ -44,14 +101,23 @@ pub fn get_state_request_works() { let watch_extractor = Arc::new(create_determine_watch::()); let rsa_repository = get_rsa3072_repository(temp_dir.path().to_path_buf()).unwrap(); + let signer = TestSigner::from_seed(b"42315678901234567890123456789012"); + let header = ParentchainHeaderBuilder::default().build(); + + let ocall_api = create_ocall_api(&header, &signer); + let state: TestState = 78234u64; let state_observer = Arc::new(ObserveStateMock::::new(state)); let getter_executor = Arc::new(GetterExecutor::<_, GetStateMock, Getter>::new(state_observer)); let top_pool_author = Arc::new(AuthorApiMock::default()); - let io_handler = - public_api_rpc_handler(top_pool_author, getter_executor, Arc::new(rsa_repository)); + let io_handler = public_api_rpc_handler( + top_pool_author, + getter_executor, + Arc::new(rsa_repository), + ocall_api, + ); let rpc_handler = Arc::new(RpcWsHandler::new(io_handler, watch_extractor, connection_registry)); let getter = @@ -71,11 +137,6 @@ pub fn get_state_request_works() { assert!(!response_string.is_empty()); - // Because we cannot de-serialize the RpcResponse here (unresolved serde_json and std/sgx feature issue), - // we hard-code the expected response. - //error!("{}", response_string); - //let response: RpcResponse = serde_json::from_str(&response_string).unwrap(); - const EXPECTED_HEX_RETURN_VALUE: &str = "0x2801209a310100000000000000"; assert!(response_string.contains(EXPECTED_HEX_RETURN_VALUE)); let rpc_return_value = RpcReturnValue::from_hex(EXPECTED_HEX_RETURN_VALUE).unwrap(); diff --git a/bitacross-worker/enclave-runtime/src/test/tests_main.rs b/bitacross-worker/enclave-runtime/src/test/tests_main.rs index 9917244ffa..f677b9ec76 100644 --- a/bitacross-worker/enclave-runtime/src/test/tests_main.rs +++ b/bitacross-worker/enclave-runtime/src/test/tests_main.rs @@ -147,6 +147,7 @@ pub extern "C" fn test_main_entrance() -> size_t { tls_ra::tests::test_tls_ra_server_client_networking, // RPC tests direct_rpc_tests::get_state_request_works, + direct_rpc_tests::state_get_mrenclave_works, // light-client-test itc_parentchain::light_client::io::sgx_tests::init_parachain_light_client_works, From c52a49f40ff1975f917dd9d6f743043c5eecc44f Mon Sep 17 00:00:00 2001 From: Jonathan Alvarez Date: Tue, 20 Feb 2024 12:36:02 -0500 Subject: [PATCH 52/64] fix(request_vc_inner): remove duplicated String encoding (#2509) --- tee-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tee-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs b/tee-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs index d832e91024..afd5990600 100644 --- a/tee-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs +++ b/tee-worker/sidechain/rpc-handler/src/direct_top_pool_api.rs @@ -349,11 +349,11 @@ async fn request_vc_inner(params: Params) -> Result { Ok(RpcReturnValue { do_watch: false, value: response, status: DirectRequestStatus::Ok }), Ok(Err(e)) => { log::error!("Received error in jsonresponse: {:?} ", e); - Err(compute_hex_encoded_return_error(&e)) + Err(e) }, Err(_) => { // This case will only happen if the sender has been dropped - Err(compute_hex_encoded_return_error("The sender has been dropped")) + Err("The sender has been dropped".to_string()) }, } } From e39d7fa70f754492320322b59515d76bd87b5475 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Wed, 21 Feb 2024 14:48:20 +0200 Subject: [PATCH 53/64] Bump docker/login-action to v3 (#2512) --- .github/workflows/benchmark-runtime-weights.yml | 4 ++-- .github/workflows/build-docker-with-args.yml | 2 +- .github/workflows/ci.yml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/benchmark-runtime-weights.yml b/.github/workflows/benchmark-runtime-weights.yml index 77543590ac..2037670b31 100644 --- a/.github/workflows/benchmark-runtime-weights.yml +++ b/.github/workflows/benchmark-runtime-weights.yml @@ -30,7 +30,7 @@ on: required: true env: - INSTANCE_ID: ${{ secrets.BENCHMARK_INSTANCE_ID }} # remote AWS host to run benchmarking + INSTANCE_ID: ${{ secrets.BENCHMARK_INSTANCE_ID }} # remote AWS host to run benchmarking BENCHMARK_SSH_USER: ${{ secrets.BENCHMARK_SSH_USER }} BENCHMARK_SSH_KEYPATH: ${{ secrets.BENCHMARK_SSH_KEYPATH }} DOCKER_BUILDKIT: 1 @@ -66,7 +66,7 @@ jobs: ./scripts/build-docker.sh production runtime-benchmarks --features=runtime-benchmarks - name: Dockerhub login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} diff --git a/.github/workflows/build-docker-with-args.yml b/.github/workflows/build-docker-with-args.yml index ffe1ca1bca..296009c25e 100644 --- a/.github/workflows/build-docker-with-args.yml +++ b/.github/workflows/build-docker-with-args.yml @@ -42,7 +42,7 @@ jobs: docker images - name: Dockerhub login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5fa48e1ae8..ebb86df115 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -881,7 +881,7 @@ jobs: docker load < litentry-tee.tar.gz - name: Dockerhub login - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_PASSWORD }} From 15f13f43b0c023d37afcc7eb4c4f7846da25f882 Mon Sep 17 00:00:00 2001 From: Igor Trofimov Date: Wed, 21 Feb 2024 17:05:02 +0200 Subject: [PATCH 54/64] Fix CI worker integration tests (#2515) * Fix CI worker integration tests * explicit run command --- tee-worker/cli/lit_ts_integration_test.sh | 2 +- tee-worker/cli/lit_ts_worker_test.sh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tee-worker/cli/lit_ts_integration_test.sh b/tee-worker/cli/lit_ts_integration_test.sh index eee140cbf5..9a29ce291a 100755 --- a/tee-worker/cli/lit_ts_integration_test.sh +++ b/tee-worker/cli/lit_ts_integration_test.sh @@ -68,4 +68,4 @@ pnpm run build cd /ts-tests pnpm install -NODE_ENV=staging pnpm --filter integration-tests run $TEST +NODE_ENV=staging pnpm --filter integration-tests run test $TEST diff --git a/tee-worker/cli/lit_ts_worker_test.sh b/tee-worker/cli/lit_ts_worker_test.sh index ed96e23518..0c836b2e6e 100755 --- a/tee-worker/cli/lit_ts_worker_test.sh +++ b/tee-worker/cli/lit_ts_worker_test.sh @@ -22,4 +22,4 @@ echo "Using node endpoint: $NODE_ENDPOINT" cd /ts-tests pnpm install -NODE_ENV=staging pnpm --filter worker run $TEST +NODE_ENV=staging pnpm --filter worker run test $TEST From 5ab7b2915309ff58b2c522218edf5d4e7dec5d02 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:06:45 +0100 Subject: [PATCH 55/64] Populate ScheduledEnclave upon registration for dev worker (#2514) * Populate ScheduledEnclave entry in dev mode * add test * comment --- pallets/teebag/src/lib.rs | 9 +++++++-- pallets/teebag/src/tests.rs | 4 ++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/pallets/teebag/src/lib.rs b/pallets/teebag/src/lib.rs index 3463520278..4424227944 100644 --- a/pallets/teebag/src/lib.rs +++ b/pallets/teebag/src/lib.rs @@ -222,7 +222,7 @@ pub mod pallet { #[pallet::storage] #[pallet::getter(fn scheduled_enclave)] pub type ScheduledEnclave = - StorageMap<_, Blake2_128Concat, (WorkerType, SidechainBlockNumber), MrEnclave>; + StorageMap<_, Blake2_128Concat, (WorkerType, SidechainBlockNumber), MrEnclave, OptionQuery>; #[pallet::storage] #[pallet::getter(fn latest_sidechain_block_confirmation)] @@ -468,7 +468,12 @@ pub mod pallet { Error::::EnclaveNotInSchedule ); }, - OperationalMode::Development => (), + OperationalMode::Development => { + // populate the registry if the entry doesn't exist + if !ScheduledEnclave::::contains_key((worker_type, 0)) { + ScheduledEnclave::::insert((worker_type, 0), enclave.mrenclave); + } + }, }; Self::add_enclave(&sender, &enclave)?; Ok(().into()) diff --git a/pallets/teebag/src/tests.rs b/pallets/teebag/src/tests.rs index 086ea4eb74..1763242c85 100644 --- a/pallets/teebag/src/tests.rs +++ b/pallets/teebag/src/tests.rs @@ -103,6 +103,10 @@ fn register_enclave_dev_works_with_no_scheduled_enclave() { assert_eq!(Teebag::enclave_count(WorkerType::Identity), 1); assert_eq!(Teebag::enclave_count(WorkerType::BitAcross), 0); assert_eq!(EnclaveRegistry::::get(alice()).unwrap(), enclave); + assert_eq!( + ScheduledEnclave::::get((WorkerType::default(), 0)).unwrap().to_vec(), + TEST4_MRENCLAVE.to_vec() + ); }) } From 081ef033a6ab29f6a1f6cbe7345a82354f559f21 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Wed, 21 Feb 2024 17:53:25 +0100 Subject: [PATCH 56/64] Use `sign_prehash_recoverable` for ETH signing (#2510) * add unittest * sign recoverable * fix tests * update --- bitacross-worker/Cargo.lock | 1 + .../app-libs/stf/src/test_genesis.rs | 2 +- .../core/bc-task-receiver/src/lib.rs | 4 +- .../bitacross/direct_call_sign_ethereum.rs | 6 ++- .../core-primitives/sgx/crypto/Cargo.toml | 2 +- .../core-primitives/sgx/crypto/src/ecdsa.rs | 26 ++++++---- bitacross-worker/enclave-runtime/Cargo.lock | 36 ++++++++++--- bitacross-worker/enclave-runtime/Cargo.toml | 1 - .../litentry/core/direct-call/Cargo.toml | 1 + .../direct-call/src/handler/sign_ethereum.rs | 50 +++++++++++++------ .../litentry/core/direct-call/src/lib.rs | 4 +- 11 files changed, 94 insertions(+), 39 deletions(-) diff --git a/bitacross-worker/Cargo.lock b/bitacross-worker/Cargo.lock index 530543a702..c4ca057004 100644 --- a/bitacross-worker/Cargo.lock +++ b/bitacross-worker/Cargo.lock @@ -5801,6 +5801,7 @@ version = "0.1.0" dependencies = [ "bc-relayer-registry", "core-primitives", + "hex 0.4.3", "itp-sgx-crypto", "itp-stf-primitives", "k256", diff --git a/bitacross-worker/app-libs/stf/src/test_genesis.rs b/bitacross-worker/app-libs/stf/src/test_genesis.rs index 507fb6ffb5..fd70e4cc72 100644 --- a/bitacross-worker/app-libs/stf/src/test_genesis.rs +++ b/bitacross-worker/app-libs/stf/src/test_genesis.rs @@ -56,7 +56,7 @@ pub fn test_genesis_setup(state: &mut impl SgxExternalitiesTrait) { set_sudo_account(state, &ALICE_ENCODED); trace!("Set new sudo account: {:?}", &ALICE_ENCODED); - let mut endowees: Vec<(AccountId32, Balance)> = vec![ + let endowees: Vec<(AccountId32, Balance)> = vec![ (endowed_account().public().into(), ENDOWED_ACC_FUNDS), (second_endowed_account().public().into(), SECOND_ENDOWED_ACC_FUNDS), (ALICE_ENCODED.into(), ALICE_FUNDS), diff --git a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs index 49e50c97ca..55cfdcb211 100644 --- a/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs +++ b/bitacross-worker/bitacross/core/bc-task-receiver/src/lib.rs @@ -183,9 +183,9 @@ where context.bitcoin_key_repository.deref(), ) .map(|r| aes_encrypt_default(&aes_key, &r).encode()), - DirectCall::SignEthereum(signer, aes_key, payload) => sign_ethereum::handle( + DirectCall::SignEthereum(signer, aes_key, msg) => sign_ethereum::handle( signer, - payload, + msg, context.relayer_registry_lookup.deref(), context.ethereum_key_repository.deref(), ) diff --git a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs index c53b6bf351..ebd5568e17 100644 --- a/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs +++ b/bitacross-worker/cli/src/trusted_base_cli/commands/bitacross/direct_call_sign_ethereum.rs @@ -23,7 +23,7 @@ use crate::{ use itp_rpc::{RpcResponse, RpcReturnValue}; use itp_stf_primitives::types::KeyPair; use itp_utils::FromHexPrefixed; -use lc_direct_call::DirectCall; +use lc_direct_call::{DirectCall, PrehashedEthereumMessage}; use sp_core::Pair; #[derive(Parser)] @@ -36,8 +36,10 @@ impl RequestDirectCallSignEthereumCommand { let alice = get_pair_from_str(trusted_cli, "//Alice", cli); let (mrenclave, shard) = get_identifiers(trusted_cli, cli); let key: [u8; 32] = random_aes_key(); + let msg: PrehashedEthereumMessage = + self.payload.clone().try_into().expect("Unable to convert payload to [u8; 32]"); - let dc = DirectCall::SignEthereum(alice.public().into(), key, self.payload.clone()).sign( + let dc = DirectCall::SignEthereum(alice.public().into(), key, msg).sign( &KeyPair::Sr25519(Box::new(alice)), &mrenclave, &shard, diff --git a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml index 17f23d5810..1663c6c5f9 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml +++ b/bitacross-worker/core-primitives/sgx/crypto/Cargo.toml @@ -9,7 +9,7 @@ edition = "2021" aes = { version = "0.6.0" } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } derive_more = { version = "0.99.5" } -k256 = { version = "0.13.3", default-features = false, features = ["ecdsa-core", "schnorr"] } +k256 = { version = "0.13.3", default-features = false, features = ["ecdsa-core", "schnorr", "alloc"] } log = { version = "0.4", default-features = false } ofb = { version = "0.4.0" } serde_json = { version = "1.0", default-features = false, features = ["alloc"], optional = true } diff --git a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs index 74f8039d39..7bb8ce0cb7 100644 --- a/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs +++ b/bitacross-worker/core-primitives/sgx/crypto/src/ecdsa.rs @@ -18,7 +18,7 @@ pub use sgx::*; use crate::error::{Error, Result}; use k256::{ - ecdsa::{signature::Signer, Signature, SigningKey, VerifyingKey}, + ecdsa::{SigningKey, VerifyingKey}, elliptic_curve::group::GroupEncoding, PublicKey, }; @@ -53,10 +53,16 @@ impl Pair { self.private.to_bytes().as_slice().try_into().unwrap() } - pub fn sign(&self, payload: &[u8]) -> Result<[u8; 64]> { - let signature: Signature = - self.private.try_sign(payload).map_err(|e| Error::Other(e.to_string().into()))?; - Ok(signature.to_bytes().into()) + // sign the prehashed message + pub fn sign_prehash_recoverable(&self, payload: &[u8]) -> Result<[u8; 65]> { + let (signature, rid) = self + .private + .sign_prehash_recoverable(payload) + .map_err(|e| Error::Other(e.to_string().into()))?; + let mut bytes = [0u8; 65]; + bytes[..64].copy_from_slice(signature.to_vec().as_slice()); + bytes[64] = rid.to_byte(); + Ok(bytes) } } @@ -145,7 +151,7 @@ pub mod sgx_tests { create_ecdsa_repository, key_repository::AccessKey, std::string::ToString, Pair, Seal, }; use itp_sgx_temp_dir::TempDir; - use k256::ecdsa::{signature::Verifier, Signature, VerifyingKey}; + use k256::ecdsa::VerifyingKey; use sgx_tstd::path::PathBuf; pub fn ecdsa_creating_repository_with_same_path_and_prefix_results_in_same_key() { @@ -201,13 +207,13 @@ pub mod sgx_tests { let temp_dir = TempDir::with_prefix("ecdsa_sign_should_produce_valid_signature").unwrap(); let seal = Seal::new(temp_dir.path().to_path_buf(), "test".to_string()); let pair = seal.init().unwrap(); - let message = [1; 32]; + let message = [1u8; 32]; //when - let signature = Signature::from_slice(&pair.sign(&message).unwrap()).unwrap(); + let (signature, rid) = &pair.private.sign_prehash_recoverable(&message).unwrap(); //then - let verifying_key = VerifyingKey::from(&pair.private); - assert!(verifying_key.verify(&message, &signature).is_ok()); + let verifying_key = VerifyingKey::recover_from_prehash(&message, signature, *rid).unwrap(); + assert_eq!(verifying_key, VerifyingKey::from(&pair.private)); } } diff --git a/bitacross-worker/enclave-runtime/Cargo.lock b/bitacross-worker/enclave-runtime/Cargo.lock index becf26aa46..95451674f4 100644 --- a/bitacross-worker/enclave-runtime/Cargo.lock +++ b/bitacross-worker/enclave-runtime/Cargo.lock @@ -248,6 +248,12 @@ version = "0.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + [[package]] name = "bc-relayer-registry" version = "0.1.0" @@ -854,6 +860,7 @@ dependencies = [ "elliptic-curve", "rfc6979", "signature", + "spki 0.7.3", ] [[package]] @@ -888,6 +895,7 @@ dependencies = [ "ff", "generic-array 0.14.7", "group", + "pkcs8", "rand_core 0.6.4", "sec1", "subtle", @@ -3145,6 +3153,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der 0.7.8", + "spki 0.7.3", +] + [[package]] name = "postcard" version = "0.7.3" @@ -3688,6 +3706,7 @@ dependencies = [ "base16ct", "der 0.7.8", "generic-array 0.14.7", + "pkcs8", "subtle", "zeroize", ] @@ -4489,6 +4508,16 @@ dependencies = [ "der 0.6.1", ] +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der 0.7.8", +] + [[package]] name = "ss58-registry" version = "1.43.0" @@ -5038,7 +5067,7 @@ dependencies = [ "const-oid", "der 0.6.1", "flagset", - "spki", + "spki 0.6.0", ] [[package]] @@ -5071,8 +5100,3 @@ dependencies = [ "quote 1.0.33", "syn 2.0.37", ] - -[[patch.unused]] -name = "getrandom" -version = "0.2.3" -source = "git+https://github.com/integritee-network/getrandom-sgx?branch=update-v2.3#0a4af01fe1df0e6200192e7a709fd18da413466e" diff --git a/bitacross-worker/enclave-runtime/Cargo.toml b/bitacross-worker/enclave-runtime/Cargo.toml index 0375e663e3..79127983eb 100644 --- a/bitacross-worker/enclave-runtime/Cargo.toml +++ b/bitacross-worker/enclave-runtime/Cargo.toml @@ -146,7 +146,6 @@ itp-sgx-temp-dir = { version = "0.1", default-features = false, optional = true, [patch.crates-io] env_logger = { git = "https://github.com/integritee-network/env_logger-sgx" } -getrandom = { git = "https://github.com/integritee-network/getrandom-sgx", branch = "update-v2.3" } log = { git = "https://github.com/integritee-network/log-sgx" } [patch."https://github.com/mesalock-linux/log-sgx"] diff --git a/bitacross-worker/litentry/core/direct-call/Cargo.toml b/bitacross-worker/litentry/core/direct-call/Cargo.toml index 9e055e6c80..ba8a61b8c4 100644 --- a/bitacross-worker/litentry/core/direct-call/Cargo.toml +++ b/bitacross-worker/litentry/core/direct-call/Cargo.toml @@ -22,6 +22,7 @@ sgx_tstd = { git = "https://github.com/apache/teaclave-sgx-sdk.git", branch = "m [dev-dependencies] k256 = { version = "0.13.3", features = ["ecdsa-core", "schnorr"] } rand = { version = "0.7" } +hex = { version = "0.4" } [features] default = ["std"] diff --git a/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs index 91be8d636f..254826ba3e 100644 --- a/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs +++ b/bitacross-worker/litentry/core/direct-call/src/handler/sign_ethereum.rs @@ -1,20 +1,22 @@ +use crate::PrehashedEthereumMessage; use bc_relayer_registry::RelayerRegistryLookup; use itp_sgx_crypto::{ecdsa::Pair, key_repository::AccessKey}; use parentchain_primitives::Identity; use std::{ + format, string::{String, ToString}, - vec::Vec, }; -pub fn handle>( +pub fn handle>( signer: Identity, - payload: Vec, + msg: PrehashedEthereumMessage, relayer_registry: &RRL, - key_repository: &BKR, -) -> Result<[u8; 64], String> { + key_repository: &EKR, +) -> Result<[u8; 65], String> { if relayer_registry.contains_key(signer) { - let key = key_repository.retrieve_key().unwrap(); - Ok(key.sign(&payload).unwrap()) + let key = key_repository.retrieve_key().map_err(|e| format!("{}", e))?; + let sig = key.sign_prehash_recoverable(&msg).map_err(|e| format!("{}", e))?; + Ok(sig) } else { Err("Unauthorized: Signer is not a valid relayer".to_string()) } @@ -24,8 +26,8 @@ pub fn handle>( pub mod test { use crate::handler::sign_ethereum::handle; use bc_relayer_registry::{RelayerRegistry, RelayerRegistryUpdater}; - use itp_sgx_crypto::mocks::KeyRepositoryMock; - use k256::elliptic_curve::rand_core; + use itp_sgx_crypto::{ecdsa::Pair as EcdsaPair, mocks::KeyRepositoryMock}; + use k256::{ecdsa::SigningKey, elliptic_curve::rand_core}; use parentchain_primitives::Identity; use sp_core::{sr25519, Pair}; @@ -37,13 +39,14 @@ pub mod test { let relayer_account = Identity::Substrate(alice_key_pair.public().into()); relayer_registry.update(relayer_account.clone()).unwrap(); - let private = k256::ecdsa::SigningKey::random(&mut rand_core::OsRng); - let signing_key = itp_sgx_crypto::ecdsa::Pair::new(private); + let private = SigningKey::random(&mut rand_core::OsRng); + let signing_key = EcdsaPair::new(private); let key_repository = KeyRepositoryMock::new(signing_key); //when - let result = handle(relayer_account, vec![], &relayer_registry, &key_repository); + let result = + handle(relayer_account, Default::default(), &relayer_registry, &key_repository); //then assert!(result.is_ok()) @@ -56,15 +59,32 @@ pub mod test { let alice_key_pair = sr25519::Pair::from_string("//Alice", None).unwrap(); let non_relayer_account = Identity::Substrate(alice_key_pair.public().into()); - let private = k256::ecdsa::SigningKey::random(&mut rand_core::OsRng); - let signing_key = itp_sgx_crypto::ecdsa::Pair::new(private); + let private = SigningKey::random(&mut rand_core::OsRng); + let signing_key = EcdsaPair::new(private); let key_repository = KeyRepositoryMock::new(signing_key); //when - let result = handle(non_relayer_account, vec![], &relayer_registry, &key_repository); + let result = + handle(non_relayer_account, Default::default(), &relayer_registry, &key_repository); //then assert!(result.is_err()) } + + #[test] + pub fn sign_ethereum_works() { + // test vector from bc team, verified with sp_core::ecdsa::Pair::sign_prehashed + let private_key = + hex::decode("038a5c907573ea7f61a7dcce5ebb2e233a6e9376e5a6f077729bd732d6cab620") + .unwrap(); + let key_pair = EcdsaPair::from_bytes(&private_key).unwrap(); + let payload = + hex::decode("3b08e117290fdd2617ea0e457a8eeebe373c456ecd3f6dc6dc4089380f486516") + .unwrap(); + let result = key_pair.sign_prehash_recoverable(&payload).unwrap(); + let expected_result = hex::decode("e733e8e3cd4f90d8fc10c2f8eeb7183623451b8e1d55b5ab6c4724c5428264955289fac3da7ce2095e12f19b4eb157c55be5c58a09ac8ae3358af0b7ec266a7201").unwrap(); + + assert_eq!(&result, expected_result.as_slice()) + } } diff --git a/bitacross-worker/litentry/core/direct-call/src/lib.rs b/bitacross-worker/litentry/core/direct-call/src/lib.rs index 20b402561a..88583c4fdd 100644 --- a/bitacross-worker/litentry/core/direct-call/src/lib.rs +++ b/bitacross-worker/litentry/core/direct-call/src/lib.rs @@ -28,6 +28,8 @@ use std::vec::Vec; pub mod handler; +pub type PrehashedEthereumMessage = [u8; 32]; + #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub struct DirectCallSigned { pub call: DirectCall, @@ -47,7 +49,7 @@ impl DirectCallSigned { #[derive(Encode, Decode, Clone, Debug, PartialEq, Eq)] pub enum DirectCall { SignBitcoin(Identity, RequestAesKey, Vec), - SignEthereum(Identity, RequestAesKey, Vec), + SignEthereum(Identity, RequestAesKey, PrehashedEthereumMessage), } impl DirectCall { From 6ad83aa755cdc5118fbecb3240881c170e1f2cb7 Mon Sep 17 00:00:00 2001 From: Verin1005 <104152026+Verin1005@users.noreply.github.com> Date: Thu, 22 Feb 2024 01:38:27 +0800 Subject: [PATCH 57/64] P-508 Avoid using as sometype (#2513) * Remove as assertion from createType * remove as sometype from stress tests * remove assertFailedEvent --------- Co-authored-by: Kai <7630809+Kailai-Wang@users.noreply.github.com> --- .../integration-tests/common/di-utils.ts | 13 +++-- .../common/utils/assertion.ts | 51 +++---------------- .../common/utils/identity-helper.ts | 23 ++++----- .../integration-tests/common/utils/storage.ts | 5 +- .../integration-tests/data-provider.test.ts | 2 +- .../di_bitcoin_identity.test.ts | 13 ++--- .../integration-tests/di_evm_identity.test.ts | 14 ++--- .../di_substrate_identity.test.ts | 28 ++++------ .../ts-tests/integration-tests/di_vc.test.ts | 15 ++---- .../ts-tests/integration-tests/dr_vc.test.ts | 15 ++---- .../integration-tests/ii_batch.test.ts | 8 +-- .../integration-tests/ii_identity.test.ts | 24 +++------ .../ts-tests/stress/src/litentry-api.ts | 14 ++--- 13 files changed, 71 insertions(+), 154 deletions(-) diff --git a/tee-worker/ts-tests/integration-tests/common/di-utils.ts b/tee-worker/ts-tests/integration-tests/common/di-utils.ts index af673e5ca8..8ea4199eae 100644 --- a/tee-worker/ts-tests/integration-tests/common/di-utils.ts +++ b/tee-worker/ts-tests/integration-tests/common/di-utils.ts @@ -11,7 +11,6 @@ import { createPublicKey, KeyObject } from 'crypto'; import WebSocketAsPromised from 'websocket-as-promised'; import { H256, Index } from '@polkadot/types/interfaces'; import { blake2AsHex } from '@polkadot/util-crypto'; -import type { PalletIdentityManagementTeeIdentityContext } from 'sidechain-api'; import { createJsonRpcRequest, nextRequestId } from './helpers'; // Send the request to worker ws @@ -35,7 +34,7 @@ async function sendRequest( const parsed = JSON.parse(data); if (parsed.id === request.id) { const result = parsed.result; - const res: WorkerRpcReturnValue = api.createType('WorkerRpcReturnValue', result) as any; + const res = api.createType('WorkerRpcReturnValue', result); console.log('Got response: ' + JSON.stringify(res.toHuman())); @@ -105,7 +104,7 @@ export const createSignedTrustedCall = async ( call: call, index: nonce, signature: signature, - }) as unknown as TrustedCallSigned; + }); }; export const createSignedTrustedGetter = async ( @@ -265,7 +264,7 @@ export async function createSignedTrustedGetterIdGraph( signer, primeIdentity.toHuman() ); - return parachainApi.createType('Getter', { trusted: getterSigned }) as unknown as Getter; // @fixme 1878; + return parachainApi.createType('Getter', { trusted: getterSigned }); } export const getSidechainNonce = async ( @@ -274,7 +273,7 @@ export const getSidechainNonce = async ( primeIdentity: CorePrimitivesIdentity ): Promise => { const getterPublic = createPublicGetter(context.api, ['nonce', '(LitentryIdentity)'], primeIdentity.toHuman()); - const getter = context.api.createType('Getter', { public: getterPublic }) as unknown as Getter; // @fixme 1878 + const getter = context.api.createType('Getter', { public: getterPublic }); const res = await sendRequestFromGetter(context, teeShieldingKey, getter); const nonce = context.api.createType('Option', hexToU8a(res.value.toHex())).unwrap(); return context.api.createType('Index', nonce); @@ -290,7 +289,7 @@ export const getIdGraphHash = async ( ['id_graph_hash', '(LitentryIdentity)'], primeIdentity.toHuman() ); - const getter = context.api.createType('Getter', { public: getterPublic }) as unknown as Getter; // @fixme 1878 + const getter = context.api.createType('Getter', { public: getterPublic }); const res = await sendRequestFromGetter(context, teeShieldingKey, getter); const hash = context.api.createType('Option', hexToU8a(res.value.toHex())).unwrap(); return context.api.createType('H256', hash); @@ -407,7 +406,7 @@ export function decodeIdGraph(sidechainRegistry: TypeRegistry, value: Bytes) { return sidechainRegistry.createType( 'Vec<(CorePrimitivesIdentity, PalletIdentityManagementTeeIdentityContext)>', idgraphBytes.unwrap() - ) as unknown as [CorePrimitivesIdentity, PalletIdentityManagementTeeIdentityContext][]; + ); } export function getTopHash(parachainApi: ApiPromise, call: TrustedCallSigned) { diff --git a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts index 7062d0503e..cfafe52359 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts @@ -1,25 +1,17 @@ -import { ApiPromise } from '@polkadot/api'; import { Event } from '@polkadot/types/interfaces'; import { hexToU8a, u8aToHex } from '@polkadot/util'; import Ajv from 'ajv'; -import { assert, expect } from 'chai'; +import { assert } from 'chai'; import * as ed from '@noble/ed25519'; import { parseIdGraph } from './identity-helper'; -import type { PalletIdentityManagementTeeError } from 'sidechain-api'; -import { PalletTeebagEnclave, CorePrimitivesIdentity } from 'parachain-api'; +import { CorePrimitivesIdentity } from 'parachain-api'; import type { IntegrationTestContext } from '../common-types'; import { getIdGraphHash } from '../di-utils'; import type { HexString } from '@polkadot/util/types'; import { jsonSchema } from './vc-helper'; import { aesKey } from '../call'; import colors from 'colors'; -import { - CorePrimitivesErrorErrorDetail, - FrameSystemEventRecord, - WorkerRpcReturnValue, - RequestVCResult, - StfError, -} from 'parachain-api'; +import { WorkerRpcReturnValue, StfError } from 'parachain-api'; import { Bytes } from '@polkadot/types-codec'; import { Signer, decryptWithAes } from './crypto'; import { blake2AsHex } from '@polkadot/util-crypto'; @@ -27,37 +19,6 @@ import { PalletIdentityManagementTeeIdentityContext } from 'sidechain-api'; import { KeyObject } from 'crypto'; import * as base58 from 'micro-base58'; -export async function assertFailedEvent( - context: IntegrationTestContext, - events: FrameSystemEventRecord[], - eventType: 'LinkIdentityFailed' | 'DeactivateIdentityFailed', - expectedEvent: CorePrimitivesErrorErrorDetail['type'] | PalletIdentityManagementTeeError['type'] -) { - const failedType = context.api.events.identityManagement[eventType]; - const isFailed = failedType.is.bind(failedType); - type EventLike = Parameters[0]; - const ievents: EventLike[] = events.map(({ event }) => event); - const failedEvent = ievents.filter(isFailed); - /* - @fix Why this type don't work?????? https://github.com/litentry/litentry-parachain/issues/1917 - */ - const eventData = failedEvent[0].data[1] as CorePrimitivesErrorErrorDetail; - assert.lengthOf(failedEvent, 1); - if (eventData.isStfError) { - assert.equal( - eventData.asStfError.toHuman(), - expectedEvent, - `check event detail is ${expectedEvent}, but is ${eventData.asStfError.toHuman()}` - ); - } else { - assert.equal( - eventData.type, - expectedEvent, - `check event detail is ${expectedEvent}, but is ${eventData.type}` - ); - } -} - export function assertIdGraph( actual: [CorePrimitivesIdentity, PalletIdentityManagementTeeIdentityContext][], expected: [CorePrimitivesIdentity, boolean][] @@ -162,7 +123,7 @@ export function assertWorkerError( check: (returnValue: StfError) => void, returnValue: WorkerRpcReturnValue ) { - const errValueDecoded = context.api.createType('StfError', returnValue.value) as unknown as StfError; + const errValueDecoded = context.api.createType('StfError', returnValue.value); check(errValueDecoded); } @@ -180,7 +141,7 @@ export async function assertIdGraphMutationResult( | 'SetIdentityNetworksResult', expectedIdGraph: [CorePrimitivesIdentity, boolean][] ): Promise { - const decodedResult = context.api.createType(resultType, returnValue.value) as any; + const decodedResult = context.api.createType(resultType, returnValue.value); assert.isNotNull(decodedResult.mutated_id_graph); const idGraph = parseIdGraph(context.sidechainRegistry, decodedResult.mutated_id_graph, aesKey); assertIdGraph(idGraph, expectedIdGraph); @@ -205,7 +166,7 @@ export async function assertIdGraphMutationResult( */ export async function assertVc(context: IntegrationTestContext, subject: CorePrimitivesIdentity, data: Bytes) { - const results = context.api.createType('RequestVCResult', data) as unknown as RequestVCResult; + const results = context.api.createType('RequestVCResult', data); // step 1 // decryptWithAes function added 0x prefix const vcPayload = results.vc_payload; diff --git a/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts index 233e24d986..1ff8766949 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/identity-helper.ts @@ -4,7 +4,7 @@ import type { IntegrationTestContext } from '../common-types'; import { AesOutput } from 'parachain-api'; import { decryptWithAes, encryptWithTeeShieldingKey, Signer } from './crypto'; import { ethers } from 'ethers'; -import type { TypeRegistry } from '@polkadot/types'; +import type { Bytes, TypeRegistry, Vec } from '@polkadot/types'; import type { PalletIdentityManagementTeeIdentityContext } from 'sidechain-api'; import type { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; import type { ApiTypes, SubmittableExtrinsic } from '@polkadot/api/types'; @@ -35,7 +35,7 @@ export async function buildIdentityHelper( const identity = { [type]: address, }; - return context.api.createType('CorePrimitivesIdentity', identity) as unknown as CorePrimitivesIdentity; + return context.api.createType('CorePrimitivesIdentity', identity); } export async function buildIdentityFromKeypair( @@ -64,7 +64,7 @@ export async function buildIdentityFromKeypair( [type]: address, }; - return context.api.createType('CorePrimitivesIdentity', identity) as unknown as CorePrimitivesIdentity; + return context.api.createType('CorePrimitivesIdentity', identity); } // If multiple transactions are built from multiple accounts, pass the signers as an array. @@ -77,7 +77,7 @@ export async function buildIdentityTxs( identities: CorePrimitivesIdentity[], method: 'linkIdentity' | 'deactivateIdentity' | 'activateIdentity', validations?: LitentryValidationData[], - web3networks?: Web3Network[][] + web3networks?: (Bytes | Vec)[] ): Promise { const txs: { tx: SubmittableExtrinsic; @@ -138,7 +138,7 @@ export function parseIdGraph( sidechainRegistry.createType( 'Vec<(CorePrimitivesIdentity, PalletIdentityManagementTeeIdentityContext)>', decryptedIdGraph - ) as unknown as [CorePrimitivesIdentity, PalletIdentityManagementTeeIdentityContext][]; + ); return idGraph; } @@ -182,10 +182,7 @@ export async function buildValidations( evmValidationData!.Web3Validation.Evm.signature.Ethereum = evmSignature; console.log('evmValidationData', evmValidationData); - const encodedVerifyIdentityValidation = context.api.createType( - 'LitentryValidationData', - evmValidationData - ) as unknown as LitentryValidationData; + const encodedVerifyIdentityValidation = context.api.createType('LitentryValidationData', evmValidationData); validations.push(encodedVerifyIdentityValidation); } else if (network === 'substrate') { @@ -207,7 +204,7 @@ export async function buildValidations( const encodedVerifyIdentityValidation: LitentryValidationData = context.api.createType( 'LitentryValidationData', substrateValidationData - ) as unknown as LitentryValidationData; + ); validations.push(encodedVerifyIdentityValidation); } else if (network === 'bitcoin') { const bitcoinValidationData = { @@ -233,7 +230,7 @@ export async function buildValidations( const encodedVerifyIdentityValidation: LitentryValidationData = context.api.createType( 'LitentryValidationData', bitcoinValidationData - ) as unknown as LitentryValidationData; + ); validations.push(encodedVerifyIdentityValidation); } else if (network === 'bitcoinPrettified') { const bitcoinValidationData = { @@ -258,7 +255,7 @@ export async function buildValidations( const encodedVerifyIdentityValidation: LitentryValidationData = context.api.createType( 'LitentryValidationData', bitcoinValidationData - ) as unknown as LitentryValidationData; + ); validations.push(encodedVerifyIdentityValidation); } else if (network === 'twitter') { console.log('post verification msg to twitter: ', msg); @@ -273,7 +270,7 @@ export async function buildValidations( const encodedVerifyIdentityValidation = context.api.createType( 'LitentryValidationData', twitterValidationData - ) as unknown as LitentryValidationData; + ); validations.push(encodedVerifyIdentityValidation); } } diff --git a/tee-worker/ts-tests/integration-tests/common/utils/storage.ts b/tee-worker/ts-tests/integration-tests/common/utils/storage.ts index ede4aced87..33491b10af 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/storage.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/storage.ts @@ -105,9 +105,6 @@ export async function checkIdGraph( const request = createJsonRpcRequest('state_getStorage', [base58mrEnclave, storageKey], nextRequestId(context)); const resp = await sendRequest(context.tee, request, context.api); - const idGraph = context.sidechainRegistry.createType( - 'PalletIdentityManagementTeeIdentityContext', - resp.value - ) as unknown as PalletIdentityManagementTeeIdentityContext; + const idGraph = context.sidechainRegistry.createType('PalletIdentityManagementTeeIdentityContext', resp.value); return idGraph; } diff --git a/tee-worker/ts-tests/integration-tests/data-provider.test.ts b/tee-worker/ts-tests/integration-tests/data-provider.test.ts index f1a5f6f834..bf34106353 100644 --- a/tee-worker/ts-tests/integration-tests/data-provider.test.ts +++ b/tee-worker/ts-tests/integration-tests/data-provider.test.ts @@ -104,7 +104,7 @@ describe('Test Vc (direct invocation)', function () { const res = await sendRequestFromTrustedCall(context, teeShieldingKey, requestVcCall); await assertIsInSidechainBlock(`${Object.keys(assertion)[0]} requestVcCall`, res); - const vcResults = context.api.createType('RequestVCResult', res.value) as unknown as RequestVCResult; + const vcResults = context.api.createType('RequestVCResult', res.value); const decryptVcPayload = decryptWithAes(aesKey, vcResults.vc_payload, 'utf-8').replace('0x', ''); const vcPayloadJson = JSON.parse(decryptVcPayload); console.log('vcPayload: ', vcPayloadJson); diff --git a/tee-worker/ts-tests/integration-tests/di_bitcoin_identity.test.ts b/tee-worker/ts-tests/integration-tests/di_bitcoin_identity.test.ts index 9faa4af64f..cf5c23492f 100644 --- a/tee-worker/ts-tests/integration-tests/di_bitcoin_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_bitcoin_identity.test.ts @@ -27,7 +27,7 @@ import { import type { IntegrationTestContext } from './common/common-types'; import { aesKey } from './common/call'; import { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Bytes, Vec } from '@polkadot/types'; import { subscribeToEventsWithExtHash } from './common/transactions'; describe('Test Identity (bitcoin direct invocation)', function () { @@ -44,7 +44,7 @@ describe('Test Identity (bitcoin direct invocation)', function () { nonce: number; identity: CorePrimitivesIdentity; validation: LitentryValidationData; - networks: Vec; + networks: Bytes | Vec; }[] = []; const deactivateIdentityRequestParams: { @@ -103,10 +103,7 @@ describe('Test Identity (bitcoin direct invocation)', function () { undefined, [context.ethersWallet.alice] ); - const aliceEvmNetworks = context.api.createType('Vec', [ - 'Ethereum', - 'Bsc', - ]) as unknown as Vec; // @fixme #1878 + const aliceEvmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); linkIdentityRequestParams.push({ nonce: aliceEvmNonce, identity: aliceEvmIdentity, @@ -126,9 +123,7 @@ describe('Test Identity (bitcoin direct invocation)', function () { undefined, context.bitcoinWallet.bob ); - const bobBitcoinNetowrks = context.api.createType('Vec', [ - 'BitcoinP2tr', - ]) as unknown as Vec; // @fixme #1878 + const bobBitcoinNetowrks = context.api.createType('Vec', ['BitcoinP2tr']); linkIdentityRequestParams.push({ nonce: bobBitcoinNonce, identity: bobBitcoinIdentity, diff --git a/tee-worker/ts-tests/integration-tests/di_evm_identity.test.ts b/tee-worker/ts-tests/integration-tests/di_evm_identity.test.ts index 36a6d947ec..b2009c942c 100644 --- a/tee-worker/ts-tests/integration-tests/di_evm_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_evm_identity.test.ts @@ -26,7 +26,7 @@ import { import type { IntegrationTestContext } from './common/common-types'; import { aesKey } from './common/call'; import { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Vec, Bytes } from '@polkadot/types'; import { subscribeToEventsWithExtHash } from './common/transactions'; describe('Test Identity (evm direct invocation)', function () { @@ -43,7 +43,7 @@ describe('Test Identity (evm direct invocation)', function () { nonce: number; identity: CorePrimitivesIdentity; validation: LitentryValidationData; - networks: Vec; + networks: Bytes | Vec; }[] = []; this.timeout(6000000); @@ -85,10 +85,7 @@ describe('Test Identity (evm direct invocation)', function () { undefined, [context.ethersWallet.bob] ); - const bobEvmNetworks = context.api.createType('Vec', [ - 'Ethereum', - 'Bsc', - ]) as unknown as Vec; // @fixme #1878 + const bobEvmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); linkIdentityRequestParams.push({ nonce: bobEvmNonce, identity: bobEvmIdentity, @@ -110,10 +107,7 @@ describe('Test Identity (evm direct invocation)', function () { 'substrate', context.substrateWallet.eve ); - const eveSubstrateNetworks = context.api.createType('Vec', [ - 'Litentry', - 'Khala', - ]) as unknown as Vec; // @fixme #1878 + const eveSubstrateNetworks = context.api.createType('Vec', ['Litentry', 'Khala']); linkIdentityRequestParams.push({ nonce: eveSubstrateNonce, identity: eveSubstrateIdentity, diff --git a/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts b/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts index 8f19167d5e..e01e532bbc 100644 --- a/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_substrate_identity.test.ts @@ -12,7 +12,7 @@ import { initIntegrationTestContext, PolkadotSigner, } from './common/utils'; -import { assertFailedEvent, assertIsInSidechainBlock, assertIdGraphMutationEvent } from './common/utils/assertion'; +import { assertIsInSidechainBlock, assertIdGraphMutationEvent } from './common/utils/assertion'; import { createSignedTrustedCallLinkIdentity, createSignedTrustedGetterIdGraph, @@ -28,7 +28,7 @@ import { import type { IntegrationTestContext } from './common/common-types'; import { aesKey } from './common/call'; import { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Vec, Bytes } from '@polkadot/types'; import { ethers } from 'ethers'; import type { HexString } from '@polkadot/util/types'; import { subscribeToEventsWithExtHash } from './common/transactions'; @@ -47,7 +47,7 @@ describe('Test Identity (direct invocation)', function () { nonce: number; identity: CorePrimitivesIdentity; validation: LitentryValidationData; - networks: Vec; + networks: Bytes | Vec; }[] = []; this.timeout(6000000); @@ -89,7 +89,7 @@ describe('Test Identity (direct invocation)', function () { twitterNonce, 'twitter' ); - const twitterNetworks = context.api.createType('Vec', []) as unknown as Vec; // @fixme #1878 + const twitterNetworks = context.api.createType('Vec', []); linkIdentityRequestParams.push({ nonce: twitterNonce, identity: twitterIdentity, @@ -108,10 +108,7 @@ describe('Test Identity (direct invocation)', function () { undefined, [context.ethersWallet.alice] ); - const evmNetworks = context.api.createType('Vec', [ - 'Ethereum', - 'Bsc', - ]) as unknown as Vec; // @fixme #1878 + const evmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); linkIdentityRequestParams.push({ nonce: evmNonce, identity: evmIdentity, @@ -133,10 +130,7 @@ describe('Test Identity (direct invocation)', function () { 'substrate', context.substrateWallet.eve ); - const eveSubstrateNetworks = context.api.createType('Vec', [ - 'Polkadot', - 'Litentry', - ]) as unknown as Vec; // @fixme #1878 + const eveSubstrateNetworks = context.api.createType('Vec', ['Polkadot', 'Litentry']); linkIdentityRequestParams.push({ nonce: eveSubstrateNonce, identity: eveSubstrateIdentity, @@ -161,9 +155,7 @@ describe('Test Identity (direct invocation)', function () { undefined, context.bitcoinWallet.alice ); - const bitcoinNetworks = context.api.createType('Vec', [ - 'BitcoinP2tr', - ]) as unknown as Vec; // @fixme #1878 + const bitcoinNetworks = context.api.createType('Vec', ['BitcoinP2tr']); linkIdentityRequestParams.push({ nonce: bitcoinNonce, identity: bitcoinIdentity, @@ -325,7 +317,7 @@ describe('Test Identity (direct invocation)', function () { res ); const events = await eventsPromise; - await assertFailedEvent(context, events, 'LinkIdentityFailed', 'InvalidIdentity'); + assert.lengthOf(events, 1); }); step('linking identity with wrong signature', async function () { @@ -384,7 +376,7 @@ describe('Test Identity (direct invocation)', function () { ); const events = await eventsPromise; - await assertFailedEvent(context, events, 'LinkIdentityFailed', 'UnexpectedMessage'); + assert.lengthOf(events, 1); }); step('linking already linked identity', async function () { @@ -433,7 +425,7 @@ describe('Test Identity (direct invocation)', function () { res ); const events = await eventsPromise; - await assertFailedEvent(context, events, 'LinkIdentityFailed', 'IdentityAlreadyLinked'); + assert.lengthOf(events, 1); }); step('deactivating linked identities', async function () { diff --git a/tee-worker/ts-tests/integration-tests/di_vc.test.ts b/tee-worker/ts-tests/integration-tests/di_vc.test.ts index f0c72d0f49..f69984dd5c 100644 --- a/tee-worker/ts-tests/integration-tests/di_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/di_vc.test.ts @@ -18,7 +18,7 @@ import { CorePrimitivesIdentity } from 'parachain-api'; import { subscribeToEventsWithExtHash } from './common/transactions'; import { defaultAssertions, unconfiguredAssertions } from './common/utils/vc-helper'; import { LitentryValidationData, Web3Network } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Vec, Bytes } from '@polkadot/types'; describe('Test Vc (direct invocation)', function () { let context: IntegrationTestContext = undefined as any; @@ -35,7 +35,7 @@ describe('Test Vc (direct invocation)', function () { nonce: number; identity: CorePrimitivesIdentity; validation: LitentryValidationData; - networks: Vec; + networks: Bytes | Vec; }[] = []; this.timeout(6000000); @@ -64,7 +64,7 @@ describe('Test Vc (direct invocation)', function () { twitterNonce, 'twitter' ); - const twitterNetworks = context.api.createType('Vec', []) as unknown as Vec; // @fixme #1878 + const twitterNetworks = context.api.createType('Vec', []); linkIdentityRequestParams.push({ nonce: twitterNonce, identity: twitterIdentity, @@ -83,10 +83,7 @@ describe('Test Vc (direct invocation)', function () { undefined, [context.ethersWallet.alice] ); - const evmNetworks = context.api.createType('Vec', [ - 'Ethereum', - 'Bsc', - ]) as unknown as Vec; // @fixme #1878 + const evmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); linkIdentityRequestParams.push({ nonce: evmNonce, identity: evmIdentity, @@ -111,9 +108,7 @@ describe('Test Vc (direct invocation)', function () { undefined, context.bitcoinWallet.alice ); - const bitcoinNetworks = context.api.createType('Vec', [ - 'BitcoinP2tr', - ]) as unknown as Vec; // @fixme #1878 + const bitcoinNetworks = context.api.createType('Vec', ['BitcoinP2tr']); linkIdentityRequestParams.push({ nonce: bitcoinNonce, identity: bitcoinIdentity, diff --git a/tee-worker/ts-tests/integration-tests/dr_vc.test.ts b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts index 2ec385b174..f228e6ac24 100644 --- a/tee-worker/ts-tests/integration-tests/dr_vc.test.ts +++ b/tee-worker/ts-tests/integration-tests/dr_vc.test.ts @@ -18,7 +18,7 @@ import { CorePrimitivesIdentity } from 'parachain-api'; import { subscribeToEventsWithExtHash } from './common/transactions'; import { defaultAssertions, unconfiguredAssertions } from './common/utils/vc-helper'; import { LitentryValidationData, Web3Network } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Vec, Bytes } from '@polkadot/types'; describe('Test Vc (direct request)', function () { let context: IntegrationTestContext = undefined as any; @@ -35,7 +35,7 @@ describe('Test Vc (direct request)', function () { nonce: number; identity: CorePrimitivesIdentity; validation: LitentryValidationData; - networks: Vec; + networks: Bytes | Vec; }[] = []; this.timeout(6000000); @@ -64,7 +64,7 @@ describe('Test Vc (direct request)', function () { twitterNonce, 'twitter' ); - const twitterNetworks = context.api.createType('Vec', []) as unknown as Vec; // @fixme #1878 + const twitterNetworks = context.api.createType('Vec', []); linkIdentityRequestParams.push({ nonce: twitterNonce, identity: twitterIdentity, @@ -83,10 +83,7 @@ describe('Test Vc (direct request)', function () { undefined, [context.ethersWallet.alice] ); - const evmNetworks = context.api.createType('Vec', [ - 'Ethereum', - 'Bsc', - ]) as unknown as Vec; // @fixme #1878 + const evmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); linkIdentityRequestParams.push({ nonce: evmNonce, identity: evmIdentity, @@ -111,9 +108,7 @@ describe('Test Vc (direct request)', function () { undefined, context.bitcoinWallet.alice ); - const bitcoinNetworks = context.api.createType('Vec', [ - 'BitcoinP2tr', - ]) as unknown as Vec; // @fixme #1878 + const bitcoinNetworks = context.api.createType('Vec', ['BitcoinP2tr']); linkIdentityRequestParams.push({ nonce: bitcoinNonce, identity: bitcoinIdentity, diff --git a/tee-worker/ts-tests/integration-tests/ii_batch.test.ts b/tee-worker/ts-tests/integration-tests/ii_batch.test.ts index c7522fcfea..d03845a69c 100644 --- a/tee-worker/ts-tests/integration-tests/ii_batch.test.ts +++ b/tee-worker/ts-tests/integration-tests/ii_batch.test.ts @@ -12,13 +12,13 @@ import { sendTxsWithUtility } from './common/transactions'; import { generateWeb3Wallets, assertIdGraphMutationEvent, assertIdentityDeactivated } from './common/utils'; import { ethers } from 'ethers'; import type { LitentryValidationData, Web3Network, CorePrimitivesIdentity } from 'parachain-api'; -import { Vec } from '@polkadot/types'; +import { Vec, Bytes } from '@polkadot/types'; describeLitentry('Test Batch Utility', (context) => { let identities: CorePrimitivesIdentity[] = []; let validations: LitentryValidationData[] = []; let evmSigners: ethers.Wallet[] = []; - const we3networks: Web3Network[][] = []; + const web3networks: (Bytes | Vec)[] = []; const signerIdentities: CorePrimitivesIdentity[] = []; step('generate web3 wallets', async function () { @@ -39,7 +39,7 @@ describeLitentry('Test Batch Utility', (context) => { const signer = evmSigners[index]; const evmIdentity = await buildIdentityHelper(signer.address, 'Evm', context); identities.push(evmIdentity); - we3networks.push(defaultNetworks as unknown as Vec); // @fixme #1878 + web3networks.push(defaultNetworks); signerIdentities.push(aliceSubstrateIdentity); } @@ -60,7 +60,7 @@ describeLitentry('Test Batch Utility', (context) => { identities, 'linkIdentity', validations, - we3networks + web3networks ); const events = await sendTxsWithUtility(context, context.substrateWallet.alice, txs, 'identityManagement', [ 'IdentityLinked', diff --git a/tee-worker/ts-tests/integration-tests/ii_identity.test.ts b/tee-worker/ts-tests/integration-tests/ii_identity.test.ts index 281a8c9b63..eaa987b117 100644 --- a/tee-worker/ts-tests/integration-tests/ii_identity.test.ts +++ b/tee-worker/ts-tests/integration-tests/ii_identity.test.ts @@ -24,6 +24,7 @@ import { sendRequest } from './common/call'; import * as base58 from 'micro-base58'; import { decodeRpcBytesAsString } from './common/call'; import { createJsonRpcRequest, nextRequestId } from './common/helpers'; +import { Bytes, Vec } from '@polkadot/types'; async function getEnclaveSignerPublicKey(context: IntegrationTestContext): Promise { const request = createJsonRpcRequest('author_getEnclaveSignerAccount', [], nextRequestId(context)); @@ -58,7 +59,7 @@ describeLitentry('Test Identity', (context) => { let charlieIdentities: CorePrimitivesIdentity[] = []; let eveValidations: LitentryValidationData[] = []; let bobValidations: LitentryValidationData[] = []; - let web3networks: Web3Network[][] = []; + let web3networks: (Bytes | Vec)[] = []; let base58mrEnclave: string; let workerAddress: string; let identityLinkedEvents; @@ -127,12 +128,9 @@ describeLitentry('Test Identity', (context) => { eveValidations = [...evmValidations, ...eveSubstrateValidations, ...twitterValidations]; - const twitterNetworks = context.api.createType('Vec', []) as unknown as Web3Network[]; - const evmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']) as unknown as Web3Network[]; - const eveSubstrateNetworks = context.api.createType('Vec', [ - 'Litentry', - 'Polkadot', - ]) as unknown as Web3Network[]; + const twitterNetworks = context.api.createType('Vec', []); + const evmNetworks = context.api.createType('Vec', ['Ethereum', 'Bsc']); + const eveSubstrateNetworks = context.api.createType('Vec', ['Litentry', 'Polkadot']); web3networks = [evmNetworks, eveSubstrateNetworks, twitterNetworks]; @@ -194,13 +192,10 @@ describeLitentry('Test Identity', (context) => { const bobSubstrateValidation = context.api.createType( 'LitentryValidationData', substrateExtensionValidationData - ) as unknown as LitentryValidationData; + ); bobValidations = [bobSubstrateValidation]; - const bobSubstrateNetworks = context.api.createType('Vec', [ - 'Litentry', - 'Polkadot', - ]) as unknown as Web3Network[]; + const bobSubstrateNetworks = context.api.createType('Vec', ['Litentry', 'Polkadot']); const bobTxs = await buildIdentityTxs( context, @@ -290,10 +285,7 @@ describeLitentry('Test Identity', (context) => { }, }, }; - const evmValidationData: LitentryValidationData = context.api.createType( - 'LitentryValidationData', - validation - ) as unknown as LitentryValidationData; + const evmValidationData: LitentryValidationData = context.api.createType('LitentryValidationData', validation); const aliceTxs = await buildIdentityTxs( context, context.substrateWallet.alice, diff --git a/tee-worker/ts-tests/stress/src/litentry-api.ts b/tee-worker/ts-tests/stress/src/litentry-api.ts index 7f8e9f79a1..17a7e2630c 100644 --- a/tee-worker/ts-tests/stress/src/litentry-api.ts +++ b/tee-worker/ts-tests/stress/src/litentry-api.ts @@ -107,7 +107,7 @@ export async function buildValidation( }, }, }, - }) as unknown as LitentryValidationData; + }); } export async function buildIdentityFromWallet( @@ -119,7 +119,7 @@ export async function buildIdentityFromWallet( Evm: wallet.wallet.address, }; - return api.createType('CorePrimitivesIdentity', identity) as unknown as CorePrimitivesIdentity; + return api.createType('CorePrimitivesIdentity', identity); } const { keyringPair } = wallet; @@ -143,7 +143,7 @@ export async function buildIdentityFromWallet( [type]: address, }; - return api.createType('CorePrimitivesIdentity', identity) as unknown as CorePrimitivesIdentity; + return api.createType('CorePrimitivesIdentity', identity); } export function decodeRpcBytesAsString(value: Bytes): string { @@ -221,7 +221,7 @@ const createPublicGetter = ( const [variant, argType] = publicGetter; const getter = parachainApi.createType('PublicGetter', { [variant]: parachainApi.createType(argType, params), - }) as unknown as PublicGetter; + }); return getter; }; @@ -257,7 +257,7 @@ export function createSignedTrustedGetterUserShieldingKey( signer, subject.toHuman() ); - return parachainApi.createType('Getter', { trusted: getterSigned }) as unknown as Getter; + return parachainApi.createType('Getter', { trusted: getterSigned }); } const sendRequestFromGetter = async ( @@ -332,7 +332,7 @@ export const getSidechainNonce = async ( log: WritableStream ): Promise => { const getterPublic = createPublicGetter(parachainApi, ['nonce', '(LitentryIdentity)'], subject.toHuman()); - const getter = parachainApi.createType('Getter', { public: getterPublic }) as unknown as Getter; + const getter = parachainApi.createType('Getter', { public: getterPublic }); const nonce = await sendRequestFromGetter(teeWorker, parachainApi, mrenclave, teeShieldingKey, getter, log); const nonceValue = decodeNonce(nonce.value.toHex()); return parachainApi.createType('Index', nonceValue) as Index; @@ -403,7 +403,7 @@ const createSignedTrustedCall = async ( call: call, index: nonce, signature: signature, - }) as unknown as TrustedCallSigned; + }); }; export const subscribeToEventsWithExtHash = async ( From bc471816c99050ee3188738c49ed5d5e13540c67 Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Thu, 22 Feb 2024 03:38:08 +0530 Subject: [PATCH 58/64] fix: merge request-vc commands (#2500) * fix: merge request-vc commands * refactor: use opposite flag and reduce redundancy * fix: remove request_vc_direct --------- Co-authored-by: Zhouhui Tian <125243011+zhouhuitian@users.noreply.github.com> --- .../trusted_base_cli/commands/litentry/mod.rs | 1 - .../commands/litentry/request_vc.rs | 14 +- .../commands/litentry/request_vc_direct.rs | 298 ------------------ tee-worker/cli/src/trusted_base_cli/mod.rs | 6 +- 4 files changed, 13 insertions(+), 306 deletions(-) delete mode 100644 tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/mod.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/mod.rs index d582982aee..40ecceefaf 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/mod.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/mod.rs @@ -20,5 +20,4 @@ pub mod id_graph_stats; pub mod link_identity; pub mod remove_identity; pub mod request_vc; -pub mod request_vc_direct; pub mod send_erroneous_parentchain_call; diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs index 097919fa0b..a8fcc74190 100644 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs +++ b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc.rs @@ -18,7 +18,7 @@ use crate::{ get_layer_two_nonce, trusted_cli::TrustedCli, trusted_command_utils::{get_identifiers, get_pair_from_str}, - trusted_operation::perform_trusted_operation, + trusted_operation::{perform_direct_operation, perform_trusted_operation}, Cli, CliResult, CliResultOk, }; use ita_stf::{trusted_call_result::RequestVCResult, Index, TrustedCall, TrustedCallSigning}; @@ -75,6 +75,9 @@ pub struct RequestVcCommand { /// subcommand to define the vc type requested #[clap(subcommand)] command: Command, + /// mode for the request-vc + #[clap(short = 's', long, default_value_t = false)] + stf: bool, } // see `assertion.rs` @@ -518,7 +521,13 @@ impl RequestVcCommand { .sign(&KeyPair::Sr25519(Box::new(alice)), nonce, &mrenclave, &shard) .into_trusted_operation(trusted_cli.direct); - match perform_trusted_operation::(cli, trusted_cli, &top) { + let maybe_vc = if self.stf { + perform_trusted_operation::(cli, trusted_cli, &top) + } else { + perform_direct_operation::(cli, trusted_cli, &top, key) + }; + + match maybe_vc { Ok(mut vc) => { let decrypted = aes_decrypt(&key, &mut vc.vc_payload).unwrap(); let credential_str = String::from_utf8(decrypted).expect("Found invalid UTF-8"); @@ -529,6 +538,7 @@ impl RequestVcCommand { println!("{:?}", e); }, } + Ok(CliResultOk::None) } diff --git a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs b/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs deleted file mode 100644 index 654dff59b3..0000000000 --- a/tee-worker/cli/src/trusted_base_cli/commands/litentry/request_vc_direct.rs +++ /dev/null @@ -1,298 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . - -use crate::{ - get_layer_two_nonce, - trusted_base_cli::commands::litentry::request_vc::*, - trusted_cli::TrustedCli, - trusted_command_utils::{get_identifiers, get_pair_from_str}, - trusted_operation::perform_direct_operation, - Cli, CliResult, CliResultOk, -}; -use ita_stf::{trusted_call_result::RequestVCResult, Index, TrustedCall, TrustedCallSigning}; -use itp_stf_primitives::types::KeyPair; -use litentry_hex_utils::decode_hex; -use litentry_primitives::{ - aes_decrypt, AchainableAmount, AchainableAmountHolding, AchainableAmountToken, - AchainableAmounts, AchainableBasic, AchainableBetweenPercents, AchainableClassOfYear, - AchainableDate, AchainableDateInterval, AchainableDatePercent, AchainableParams, - AchainableToken, Assertion, ContestType, EVMTokenType, GenericDiscordRoleType, Identity, - OneBlockCourseType, PlatformUserType, RequestAesKey, SoraQuizType, VIP3MembershipCardLevel, - Web3Network, Web3NftType, Web3TokenType, REQUEST_AES_KEY_LEN, -}; -use sp_core::Pair; - -// usage example (you can always use --help on subcommands to see more details) -// -// a8: -// ./bin/litentry-cli trusted -d request-vc-direct \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 a8 litentry,litmus -// -// OneBlock VC: -// ./bin/litentry-cli trusted -d request-vc-direct \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 one-block completion -// -// achainable VC: -// ./bin/litentry-cli trusted -d request-vc-direct \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 achainable amount-holding a litentry 1 2014-05-01 -// -// vip3 VC: -// ./bin/litentry-cli trusted -d request-vc-direct \ -// did:litentry:substrate:0x8eaf04151687736326c9fea17e25fc5287613693c912909cb226aa4794f26a48 vip3-membership-card gold - -#[derive(Parser)] -pub struct RequestVcDirectCommand { - /// did account to whom the vc will be issued - did: String, - /// subcommand to define the vc type requested - #[clap(subcommand)] - command: Command, -} - -impl RequestVcDirectCommand { - pub(crate) fn run(&self, cli: &Cli, trusted_cli: &TrustedCli) -> CliResult { - let alice = get_pair_from_str(trusted_cli, "//Alice", cli); - let id: Identity = Identity::from_did(self.did.as_str()).unwrap(); - - let (mrenclave, shard) = get_identifiers(trusted_cli, cli); - let nonce = get_layer_two_nonce!(alice, cli, trusted_cli); - - let assertion = match &self.command { - Command::A1 => Assertion::A1, - Command::A2(arg) => Assertion::A2(to_para_str(&arg.guild_id)), - Command::A3(arg) => Assertion::A3( - to_para_str(&arg.guild_id), - to_para_str(&arg.channel_id), - to_para_str(&arg.role_id), - ), - Command::A4(arg) => Assertion::A4(to_para_str(&arg.minimum_amount)), - Command::A6 => Assertion::A6, - Command::A7(arg) => Assertion::A7(to_para_str(&arg.minimum_amount)), - Command::A8(arg) => { - let networks: Vec = arg - .networks - .iter() - .map(|n| n.as_str().try_into().expect("cannot convert to Web3Network")) - .collect(); - Assertion::A8(networks.try_into().unwrap()) - }, - Command::A10(arg) => Assertion::A10(to_para_str(&arg.minimum_amount)), - Command::A11(arg) => Assertion::A11(to_para_str(&arg.minimum_amount)), - Command::A13(arg) => { - let raw: [u8; 32] = decode_hex(&arg.account).unwrap().try_into().unwrap(); - Assertion::A13(raw.into()) - }, - Command::A14 => Assertion::A14, - Command::A20 => Assertion::A20, - Command::BnbDomainHolding => Assertion::BnbDomainHolding, - Command::OneBlock(c) => match c { - OneblockCommand::Completion => - Assertion::OneBlock(OneBlockCourseType::CourseCompletion), - OneblockCommand::Outstanding => - Assertion::OneBlock(OneBlockCourseType::CourseOutstanding), - OneblockCommand::Participation => - Assertion::OneBlock(OneBlockCourseType::CourseParticipation), - }, - Command::Achainable(c) => match c { - AchainableCommand::AmountHolding(arg) => Assertion::Achainable( - AchainableParams::AmountHolding(AchainableAmountHolding { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - amount: to_para_str(&arg.amount), - date: to_para_str(&arg.date), - token: arg.token.as_ref().map(|s| to_para_str(s)), - }), - ), - AchainableCommand::AmountToken(arg) => - Assertion::Achainable(AchainableParams::AmountToken(AchainableAmountToken { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - amount: to_para_str(&arg.amount), - token: arg.token.as_ref().map(|s| to_para_str(s)), - })), - AchainableCommand::Amount(arg) => - Assertion::Achainable(AchainableParams::Amount(AchainableAmount { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - amount: to_para_str(&arg.amount), - })), - AchainableCommand::Amounts(arg) => - Assertion::Achainable(AchainableParams::Amounts(AchainableAmounts { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - amount1: to_para_str(&arg.amount1), - amount2: to_para_str(&arg.amount2), - })), - AchainableCommand::Basic(arg) => - Assertion::Achainable(AchainableParams::Basic(AchainableBasic { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - })), - AchainableCommand::BetweenPercents(arg) => Assertion::Achainable( - AchainableParams::BetweenPercents(AchainableBetweenPercents { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - greater_than_or_equal_to: to_para_str(&arg.greater_than_or_equal_to), - less_than_or_equal_to: to_para_str(&arg.less_than_or_equal_to), - }), - ), - AchainableCommand::ClassOfYear(arg) => - Assertion::Achainable(AchainableParams::ClassOfYear(AchainableClassOfYear { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - })), - AchainableCommand::DateInterval(arg) => - Assertion::Achainable(AchainableParams::DateInterval(AchainableDateInterval { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - start_date: to_para_str(&arg.start_date), - end_date: to_para_str(&arg.end_date), - })), - AchainableCommand::DatePercent(arg) => - Assertion::Achainable(AchainableParams::DatePercent(AchainableDatePercent { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - date: to_para_str(&arg.date), - percent: to_para_str(&arg.percent), - token: to_para_str(&arg.token), - })), - AchainableCommand::Date(arg) => - Assertion::Achainable(AchainableParams::Date(AchainableDate { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - date: to_para_str(&arg.date), - })), - AchainableCommand::Token(arg) => - Assertion::Achainable(AchainableParams::Token(AchainableToken { - name: to_para_str(&arg.name), - chain: to_chains(&arg.chain), - token: to_para_str(&arg.token), - })), - }, - Command::GenericDiscordRole(c) => match c { - GenericDiscordRoleCommand::Contest(s) => match s { - ContestCommand::Legend => Assertion::GenericDiscordRole( - GenericDiscordRoleType::Contest(ContestType::Legend), - ), - ContestCommand::Popularity => Assertion::GenericDiscordRole( - GenericDiscordRoleType::Contest(ContestType::Popularity), - ), - ContestCommand::Participant => Assertion::GenericDiscordRole( - GenericDiscordRoleType::Contest(ContestType::Participant), - ), - }, - GenericDiscordRoleCommand::SoraQuiz(s) => match s { - SoraQuizCommand::Attendee => Assertion::GenericDiscordRole( - GenericDiscordRoleType::SoraQuiz(SoraQuizType::Attendee), - ), - SoraQuizCommand::Master => Assertion::GenericDiscordRole( - GenericDiscordRoleType::SoraQuiz(SoraQuizType::Master), - ), - }, - }, - Command::VIP3MembershipCard(arg) => match arg { - VIP3MembershipCardLevelCommand::Gold => - Assertion::VIP3MembershipCard(VIP3MembershipCardLevel::Gold), - VIP3MembershipCardLevelCommand::Silver => - Assertion::VIP3MembershipCard(VIP3MembershipCardLevel::Silver), - }, - Command::WeirdoGhostGangHolder => Assertion::WeirdoGhostGangHolder, - Command::EVMAmountHolding(c) => match c { - EVMAmountHoldingCommand::Ton => Assertion::EVMAmountHolding(EVMTokenType::Ton), - EVMAmountHoldingCommand::Trx => Assertion::EVMAmountHolding(EVMTokenType::Trx), - }, - Command::CryptoSummary => Assertion::CryptoSummary, - Command::LITStaking => Assertion::LITStaking, - Command::BRC20AmountHolder => Assertion::BRC20AmountHolder, - Command::TokenHoldingAmount(arg) => match arg { - TokenHoldingAmountCommand::Bnb => Assertion::TokenHoldingAmount(Web3TokenType::Bnb), - TokenHoldingAmountCommand::Eth => Assertion::TokenHoldingAmount(Web3TokenType::Eth), - TokenHoldingAmountCommand::SpaceId => - Assertion::TokenHoldingAmount(Web3TokenType::SpaceId), - TokenHoldingAmountCommand::Lit => Assertion::TokenHoldingAmount(Web3TokenType::Lit), - TokenHoldingAmountCommand::Wbtc => - Assertion::TokenHoldingAmount(Web3TokenType::Wbtc), - TokenHoldingAmountCommand::Usdc => - Assertion::TokenHoldingAmount(Web3TokenType::Usdc), - TokenHoldingAmountCommand::Usdt => - Assertion::TokenHoldingAmount(Web3TokenType::Usdt), - TokenHoldingAmountCommand::Crv => Assertion::TokenHoldingAmount(Web3TokenType::Crv), - TokenHoldingAmountCommand::Matic => - Assertion::TokenHoldingAmount(Web3TokenType::Matic), - TokenHoldingAmountCommand::Dydx => - Assertion::TokenHoldingAmount(Web3TokenType::Dydx), - TokenHoldingAmountCommand::Amp => Assertion::TokenHoldingAmount(Web3TokenType::Amp), - TokenHoldingAmountCommand::Cvx => Assertion::TokenHoldingAmount(Web3TokenType::Cvx), - TokenHoldingAmountCommand::Tusd => - Assertion::TokenHoldingAmount(Web3TokenType::Tusd), - TokenHoldingAmountCommand::Usdd => - Assertion::TokenHoldingAmount(Web3TokenType::Usdd), - TokenHoldingAmountCommand::Gusd => - Assertion::TokenHoldingAmount(Web3TokenType::Gusd), - TokenHoldingAmountCommand::Link => - Assertion::TokenHoldingAmount(Web3TokenType::Link), - TokenHoldingAmountCommand::Grt => Assertion::TokenHoldingAmount(Web3TokenType::Grt), - TokenHoldingAmountCommand::Comp => - Assertion::TokenHoldingAmount(Web3TokenType::Comp), - TokenHoldingAmountCommand::People => - Assertion::TokenHoldingAmount(Web3TokenType::People), - TokenHoldingAmountCommand::Gtc => Assertion::TokenHoldingAmount(Web3TokenType::Gtc), - TokenHoldingAmountCommand::Ton => Assertion::TokenHoldingAmount(Web3TokenType::Ton), - TokenHoldingAmountCommand::Trx => Assertion::TokenHoldingAmount(Web3TokenType::Trx), - }, - Command::PlatformUser(arg) => match arg { - PlatformUserCommand::KaratDaoUser => - Assertion::PlatformUser(PlatformUserType::KaratDaoUser), - }, - Command::NftHolder(arg) => match arg { - NftHolderCommand::WeirdoGhostGang => - Assertion::NftHolder(Web3NftType::WeirdoGhostGang), - NftHolderCommand::Club3Sbt => Assertion::NftHolder(Web3NftType::Club3Sbt), - }, - }; - - let key: [u8; 32] = Self::random_aes_key(); - - let top = TrustedCall::request_vc( - alice.public().into(), - id, - assertion, - Some(key), - Default::default(), - ) - .sign(&KeyPair::Sr25519(Box::new(alice)), nonce, &mrenclave, &shard) - .into_trusted_operation(trusted_cli.direct); - - // This should contain the AES Key for AESRequest - match perform_direct_operation::(cli, trusted_cli, &top, key) { - Ok(mut vc) => { - let decrypted = aes_decrypt(&key, &mut vc.vc_payload).unwrap(); - let credential_str = String::from_utf8(decrypted).expect("Found invalid UTF-8"); - println!("----Generated VC-----"); - println!("{}", credential_str); - }, - Err(e) => { - println!("{:?}", e); - }, - } - Ok(CliResultOk::None) - } - - fn random_aes_key() -> RequestAesKey { - let random: Vec = (0..REQUEST_AES_KEY_LEN).map(|_| rand::random::()).collect(); - random[0..REQUEST_AES_KEY_LEN].try_into().unwrap() - } -} diff --git a/tee-worker/cli/src/trusted_base_cli/mod.rs b/tee-worker/cli/src/trusted_base_cli/mod.rs index 878d3e5f2f..cc221ec281 100644 --- a/tee-worker/cli/src/trusted_base_cli/mod.rs +++ b/tee-worker/cli/src/trusted_base_cli/mod.rs @@ -23,7 +23,7 @@ use crate::{ litentry::{ get_storage::GetStorageCommand, id_graph_stats::IDGraphStats, link_identity::LinkIdentityCommand, remove_identity::RemoveIdentityCommand, - request_vc::RequestVcCommand, request_vc_direct::RequestVcDirectCommand, + request_vc::RequestVcCommand, send_erroneous_parentchain_call::SendErroneousParentchainCallCommand, }, nonce::NonceCommand, @@ -93,9 +93,6 @@ pub enum TrustedBaseCommand { /// Request VC RequestVc(RequestVcCommand), - /// Request VC isolated from Block Production - RequestVcDirect(RequestVcDirectCommand), - /// Remove Identity from the prime identity RemoveIdentity(RemoveIdentityCommand), } @@ -119,7 +116,6 @@ impl TrustedBaseCommand { TrustedBaseCommand::LinkIdentity(cmd) => cmd.run(cli, trusted_cli), TrustedBaseCommand::IDGraph(cmd) => cmd.run(cli, trusted_cli), TrustedBaseCommand::RequestVc(cmd) => cmd.run(cli, trusted_cli), - TrustedBaseCommand::RequestVcDirect(cmd) => cmd.run(cli, trusted_cli), TrustedBaseCommand::RemoveIdentity(cmd) => cmd.run(cli, trusted_cli), } } From 78c79c949206acf1360b7dea7ea894c27decacbb Mon Sep 17 00:00:00 2001 From: "will.li" <120463031+higherordertech@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:18:14 +1100 Subject: [PATCH 59/64] fix: change nft holder credentials type name to 'Generic NFT Holder' (#2517) Co-authored-by: higherordertech --- tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs b/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs index b4b5e11d7b..2cd754c6a8 100644 --- a/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs +++ b/tee-worker/litentry/core/credentials-v2/src/nft_holder/mod.rs @@ -32,7 +32,7 @@ use lc_credentials::{ Credential, }; -const TYPE: &str = "NFT Holder"; +const TYPE: &str = "Generic NFT Holder"; const DESCRIPTION: &str = "You are a holder of a certain kind of NFT"; struct AssertionKeys { From e8578bfe33953ce3caa11e9573c4b9c0a9dca467 Mon Sep 17 00:00:00 2001 From: Kai <7630809+Kailai-Wang@users.noreply.github.com> Date: Thu, 22 Feb 2024 17:05:04 +0100 Subject: [PATCH 60/64] Fix typo add -> aad (#2518) --- tee-worker/ts-tests/integration-tests/common/di-utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee-worker/ts-tests/integration-tests/common/di-utils.ts b/tee-worker/ts-tests/integration-tests/common/di-utils.ts index 8ea4199eae..7fc65e134b 100644 --- a/tee-worker/ts-tests/integration-tests/common/di-utils.ts +++ b/tee-worker/ts-tests/integration-tests/common/di-utils.ts @@ -393,7 +393,7 @@ export const createAesRequest = async ( ciphertext: compactAddLength( hexToU8a(encryptWithAes(u8aToHex(aesKey), hexToU8a(keyNonce), Buffer.from(top))) ), - add: hexToU8a('0x'), + aad: hexToU8a('0x'), nonce: hexToU8a(keyNonce), }) .toU8a(), From 159570a18cde53ebf4e23ed539354a7154ae47e3 Mon Sep 17 00:00:00 2001 From: Jonathan Alvarez Date: Thu, 22 Feb 2024 22:58:02 -0500 Subject: [PATCH 61/64] feat(vc tests): use the schema validator package (#2502) --- .../common/utils/assertion.ts | 18 ++-- .../common/utils/vc-helper.ts | 82 ------------------- .../ts-tests/integration-tests/package.json | 2 +- tee-worker/ts-tests/pnpm-lock.yaml | 42 +++++++++- 4 files changed, 48 insertions(+), 96 deletions(-) diff --git a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts index cfafe52359..44c5e1f756 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/assertion.ts @@ -1,6 +1,5 @@ import { Event } from '@polkadot/types/interfaces'; import { hexToU8a, u8aToHex } from '@polkadot/util'; -import Ajv from 'ajv'; import { assert } from 'chai'; import * as ed from '@noble/ed25519'; import { parseIdGraph } from './identity-helper'; @@ -8,13 +7,13 @@ import { CorePrimitivesIdentity } from 'parachain-api'; import type { IntegrationTestContext } from '../common-types'; import { getIdGraphHash } from '../di-utils'; import type { HexString } from '@polkadot/util/types'; -import { jsonSchema } from './vc-helper'; import { aesKey } from '../call'; import colors from 'colors'; import { WorkerRpcReturnValue, StfError } from 'parachain-api'; import { Bytes } from '@polkadot/types-codec'; import { Signer, decryptWithAes } from './crypto'; import { blake2AsHex } from '@polkadot/util-crypto'; +import { validateVcSchema } from '@litentry/vc-schema-validator'; import { PalletIdentityManagementTeeIdentityContext } from 'sidechain-api'; import { KeyObject } from 'crypto'; import * as base58 from 'micro-base58'; @@ -152,7 +151,7 @@ export async function assertIdGraphMutationResult( return u8aToHex(decodedResult.id_graph_hash); } -/* +/* assert vc steps: 1. check vc status should be Active @@ -160,7 +159,7 @@ export async function assertIdGraphMutationResult( 3. check subject 4. compare vc index with vcPayload id 5. check vc signature - 6. compare vc wtih jsonSchema + 6. check vc schema TODO: This is incomplete; we still need to further check: https://github.com/litentry/litentry-parachain/issues/1873 */ @@ -195,8 +194,7 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri // step 4 // extrac proof and vc without proof json const vcPayloadJson = JSON.parse(decryptVcPayload); - console.log('credential: ', vcPayloadJson); - console.log('assertions: ', vcPayloadJson.credentialSubject.assertions); + console.log('credential: ', JSON.stringify(vcPayloadJson, null, 2)); const { proof, ...vcWithoutProof } = vcPayloadJson; // step 5 @@ -236,13 +234,13 @@ export async function assertVc(context: IntegrationTestContext, subject: CorePri // step 9 // validate VC aganist schema - const ajv = new Ajv(); - const validate = ajv.compile(jsonSchema); + const schemaResult = await validateVcSchema(vcPayloadJson); - const isValid = validate(vcPayloadJson); + if (schemaResult.errors) console.log('Schema Validation errors: ', schemaResult.errors); + + assert.isTrue(schemaResult.isValid, 'Check Vc payload error: vcPayload should be valid'); - assert.isTrue(isValid, 'Check Vc payload error: vcPayload should be valid'); assert.equal( vcWithoutProof.type[0], 'VerifiableCredential', diff --git a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts index 90d1fff0aa..e67e8cfa33 100644 --- a/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts +++ b/tee-worker/ts-tests/integration-tests/common/utils/vc-helper.ts @@ -166,85 +166,3 @@ export const unconfiguredAssertions = [ }, }, ]; -export const jsonSchema = { - type: 'object', - properties: { - id: { - type: 'string', - }, - type: { - type: 'array', - }, - issuer: { - type: 'object', - properties: { - id: { - type: 'string', - }, - name: { - type: 'string', - }, - shard: { - type: 'string', - }, - }, - }, - issuanceDate: { - type: 'string', - }, - credentialSubject: { - type: 'object', - properties: { - id: { - type: 'string', - }, - description: { - type: 'string', - }, - type: { - type: 'string', - }, - tag: { - type: 'array', - }, - assertions: { - type: 'array', - items: { - type: 'object', - }, - }, - values: { - type: 'array', - items: { - type: 'boolean', - }, - }, - endpoint: { - type: 'string', - }, - }, - required: ['id', 'description', 'type', 'assertions', 'values', 'endpoint'], - }, - proof: { - type: 'object', - properties: { - created: { - type: 'string', - }, - type: { - enum: ['Ed25519Signature2020'], - }, - proofPurpose: { - enum: ['assertionMethod'], - }, - proofValue: { - type: 'string', - }, - verificationMethod: { - type: 'string', - }, - }, - }, - }, - required: ['id', 'type', 'credentialSubject', 'issuer', 'issuanceDate', 'proof'], -}; diff --git a/tee-worker/ts-tests/integration-tests/package.json b/tee-worker/ts-tests/integration-tests/package.json index 0bf78b86df..60d8500abf 100644 --- a/tee-worker/ts-tests/integration-tests/package.json +++ b/tee-worker/ts-tests/integration-tests/package.json @@ -9,6 +9,7 @@ "test": "mocha --exit --sort -r ts-node/register --loader=ts-node/esm" }, "dependencies": { + "@litentry/vc-schema-validator": "^0.0.1", "@noble/ed25519": "^1.7.3", "@polkadot/api": "^10.9.1", "@polkadot/api-augment": "^10.9.1", @@ -25,7 +26,6 @@ "@polkadot/util": "^12.5.1", "@polkadot/util-crypto": "^12.5.1", "add": "^2.0.6", - "ajv": "^8.12.0", "bitcore-lib": "^10.0.21", "chai": "^4.3.6", "colors": "^1.4.0", diff --git a/tee-worker/ts-tests/pnpm-lock.yaml b/tee-worker/ts-tests/pnpm-lock.yaml index edaf43607f..878d19c8f7 100644 --- a/tee-worker/ts-tests/pnpm-lock.yaml +++ b/tee-worker/ts-tests/pnpm-lock.yaml @@ -10,6 +10,9 @@ importers: integration-tests: dependencies: + '@litentry/vc-schema-validator': + specifier: ^0.0.1 + version: 0.0.1(ajv-formats@2.1.1)(ajv@8.12.0)(fast-glob@3.3.2)(tslib@2.6.2) '@noble/ed25519': specifier: ^1.7.3 version: 1.7.3 @@ -58,9 +61,6 @@ importers: add: specifier: ^2.0.6 version: 2.0.6 - ajv: - specifier: ^8.12.0 - version: 8.12.0 bitcore-lib: specifier: ^10.0.21 version: 10.0.21 @@ -779,6 +779,20 @@ packages: '@jridgewell/sourcemap-codec': 1.4.15 dev: true + /@litentry/vc-schema-validator@0.0.1(ajv-formats@2.1.1)(ajv@8.12.0)(fast-glob@3.3.2)(tslib@2.6.2): + resolution: {integrity: sha512-Utnu2m/IPcGbqpopNsEdB1AvRimoL300dGpk/9g/8P16OemCHYxHE8j+n2JrHS+BUvH9upDiNMy5sbZPmDAAfQ==} + peerDependencies: + ajv: ^8.12.0 + ajv-formats: ^2.1.1 + fast-glob: ^3.3.2 + tslib: ^2.3.0 + dependencies: + ajv: 8.12.0 + ajv-formats: 2.1.1(ajv@8.12.0) + fast-glob: 3.3.2 + tslib: 2.6.2 + dev: false + /@noble/curves@1.2.0: resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} dependencies: @@ -1496,6 +1510,17 @@ packages: resolution: {integrity: sha512-H7wUZRn8WpTq9jocdxQ2c8x2sKo9ZVmzfRE13GiNJXfp7NcKYEdvl3vspKjXox6RIG2VtaRe4JFvxG4rqp2Zuw==} dev: true + /ajv-formats@2.1.1(ajv@8.12.0): + resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + dependencies: + ajv: 8.12.0 + dev: false + /ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} dependencies: @@ -2134,6 +2159,17 @@ packages: merge2: 1.4.1 micromatch: 4.0.5 + /fast-glob@3.3.2: + resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} + engines: {node: '>=8.6.0'} + dependencies: + '@nodelib/fs.stat': 2.0.5 + '@nodelib/fs.walk': 1.2.8 + glob-parent: 5.1.2 + merge2: 1.4.1 + micromatch: 4.0.5 + dev: false + /fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} dev: true From 9099ea61f56290812d15b21cf0158e22044c15ad Mon Sep 17 00:00:00 2001 From: Faisal Ahmed <42486737+felixfaisal@users.noreply.github.com> Date: Fri, 23 Feb 2024 14:44:43 +0530 Subject: [PATCH 62/64] fix: add metrics for isolated request-vc (#2516) * fix: add metrics for isolated request-vc * refactor: fix clippy * refactor: adjust based on comments --- .../enclave-metrics/src/lib.rs | 3 ++ .../lc-vc-task-receiver/src/lib.rs | 30 ++++++++++++++++--- tee-worker/service/src/prometheus_metrics.rs | 23 ++++++++++++-- 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/tee-worker/core-primitives/enclave-metrics/src/lib.rs b/tee-worker/core-primitives/enclave-metrics/src/lib.rs index c2501acab5..aab5638308 100644 --- a/tee-worker/core-primitives/enclave-metrics/src/lib.rs +++ b/tee-worker/core-primitives/enclave-metrics/src/lib.rs @@ -44,4 +44,7 @@ pub enum EnclaveMetric { SidechainSlotStfExecutionTime(Duration), SidechainSlotBlockCompositionTime(Duration), SidechainBlockBroadcastingTime(Duration), + VCBuildTime(String, Duration), + SuccessfullVCIssuance, + FailedVCIssuance, } diff --git a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs index 395ca2f702..91901c4e91 100644 --- a/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs +++ b/tee-worker/litentry/core/vc-issuance/lc-vc-task-receiver/src/lib.rs @@ -25,6 +25,7 @@ use ita_stf::{ aes_encrypt_default, helpers::ensure_self, trusted_call_result::RequestVCResult, Getter, OpaqueCall, TrustedCall, TrustedCallSigned, TrustedCallVerification, TrustedOperation, H256, }; +use itp_enclave_metrics::EnclaveMetric; use itp_extrinsics_factory::CreateExtrinsics; use itp_node_api::metadata::{ pallet_vcmp::VCMPCallIndexes, provider::AccessNodeMetadata, NodeMetadataTrait, @@ -58,6 +59,7 @@ use std::{ Arc, }, thread, + time::Instant, vec::Vec, }; use threadpool::ThreadPool; @@ -104,15 +106,27 @@ pub fn run_vc_handler_runner( let sender_pool = sender.clone(); pool.execute(move || { - if let Err(e) = req.sender.send(handle_request( + let response = handle_request( &mut req.request, - context_pool, + context_pool.clone(), extrinsic_factory_pool, node_metadata_repo_pool, sender_pool, - )) { + ); + if let Err(e) = req.sender.send(response.clone()) { warn!("Unable to submit response back to the handler: {:?}", e); } + if response.is_err() { + if let Err(e) = + context_pool.ocall_api.update_metric(EnclaveMetric::FailedVCIssuance) + { + warn!("Failed to update metric for VC Issuance: {:?}", e); + } + } else if let Err(e) = + context_pool.ocall_api.update_metric(EnclaveMetric::SuccessfullVCIssuance) + { + warn!("Failed to update metric for VC Issuance: {:?}", e); + } }); } @@ -140,6 +154,7 @@ where N: AccessNodeMetadata + Send + Sync + 'static, N::MetadataType: NodeMetadataTrait, { + let start_time = Instant::now(); let enclave_shielding_key = context .shielding_key .retrieve_key() @@ -288,7 +303,7 @@ where let call = OpaqueCall::from_tuple(&( call_index, who.clone(), - assertion, + assertion.clone(), id_graph_hash, req_ext_hash, )); @@ -321,6 +336,13 @@ where .send_to_parentchain(xt, &ParentchainId::Litentry, false) .map_err(|e| format!("Unable to send extrinsic to parentchain: {:?}", e))?; + if let Err(e) = context.ocall_api.update_metric(EnclaveMetric::VCBuildTime( + format!("{:?}", assertion), + start_time.elapsed(), + )) { + warn!("Failed to update metric for vc build time: {:?}", e); + } + Ok(res.encode()) } else { Err("Expect request_vc trusted call".to_string()) diff --git a/tee-worker/service/src/prometheus_metrics.rs b/tee-worker/service/src/prometheus_metrics.rs index f8825dca9b..4e99f4e7d4 100644 --- a/tee-worker/service/src/prometheus_metrics.rs +++ b/tee-worker/service/src/prometheus_metrics.rs @@ -38,9 +38,9 @@ use lc_stf_task_sender::RequestType; use litentry_primitives::{Assertion, Identity}; use log::*; use prometheus::{ - proto::MetricFamily, register_counter_vec, register_histogram, register_histogram_vec, - register_int_gauge, register_int_gauge_vec, CounterVec, Histogram, HistogramVec, IntGauge, - IntGaugeVec, + proto::MetricFamily, register_counter, register_counter_vec, register_histogram, + register_histogram_vec, register_int_gauge, register_int_gauge_vec, Counter, CounterVec, + Histogram, HistogramVec, IntGauge, IntGaugeVec, }; use serde::{Deserialize, Serialize}; use std::{net::SocketAddr, sync::Arc}; @@ -88,6 +88,15 @@ lazy_static! { static ref ENCLAVE_SIDECHAIN_BLOCK_BROADCASTING_TIME: Histogram = register_histogram!("litentry_worker_enclave_sidechain_block_broadcasting_time", "Time taken to broadcast sidechain block") .unwrap(); + static ref VC_BUILD_TIME: HistogramVec = + register_histogram_vec!("litentry_worker_vc_build_time", "Time taken to build a vc", &["variant"]) + .unwrap(); + static ref SUCCESSFULL_VC_ISSUANCE_TASKS: Counter = + register_counter!("litentry_worker_vc_successfull_issuances_tasks", "Succesfull VC Issuance tasks") + .unwrap(); + static ref FAILED_VC_ISSUANCE_TASKS: Counter = + register_counter!("litentry_worker_vc_failed_issuances_tasks", "Failed VC Issuance tasks") + .unwrap(); } @@ -226,6 +235,14 @@ impl ReceiveEnclaveMetrics for EnclaveMetricsReceiver { ENCLAVE_SIDECHAIN_SLOT_BLOCK_COMPOSITION_TIME.observe(time.as_secs_f64()), EnclaveMetric::SidechainBlockBroadcastingTime(time) => ENCLAVE_SIDECHAIN_BLOCK_BROADCASTING_TIME.observe(time.as_secs_f64()), + EnclaveMetric::VCBuildTime(assertion, time) => + VC_BUILD_TIME.with_label_values(&[&assertion]).observe(time.as_secs_f64()), + EnclaveMetric::SuccessfullVCIssuance => { + SUCCESSFULL_VC_ISSUANCE_TASKS.inc(); + }, + EnclaveMetric::FailedVCIssuance => { + FAILED_VC_ISSUANCE_TASKS.inc(); + }, } Ok(()) } From 717e39f58b4b1ec770f791b9425daf8f552b3b21 Mon Sep 17 00:00:00 2001 From: "will.li" <120463031+higherordertech@users.noreply.github.com> Date: Fri, 23 Feb 2024 21:20:26 +1100 Subject: [PATCH 63/64] fix: P-507 rename config name discord_litentry_url to litentry_discord_microservice_url, added prod api endpoint for it, and remove twitter_litentry mock and config (#2519) Co-authored-by: higherordertech --- bitacross-worker/docker/docker-compose.yml | 3 +- .../docker/multiworker-docker-compose.yml | 9 ++--- .../scripts/litentry/release/config.json.eg | 3 +- tee-worker/docker/docker-compose.yml | 3 +- .../docker/multiworker-docker-compose.yml | 9 ++--- .../litentry/core/assertion-build/src/a2.rs | 8 ++-- .../litentry/core/assertion-build/src/a3.rs | 6 ++- .../src/generic_discord_role.rs | 5 ++- .../data-providers/src/discord_litentry.rs | 16 ++++---- .../litentry/core/data-providers/src/lib.rs | 24 ++++-------- .../litentry/core/mock-server/src/lib.rs | 2 - .../core/mock-server/src/twitter_litentry.rs | 38 ------------------- tee-worker/local-setup/.env.dev | 3 +- 13 files changed, 39 insertions(+), 90 deletions(-) delete mode 100644 tee-worker/litentry/core/mock-server/src/twitter_litentry.rs diff --git a/bitacross-worker/docker/docker-compose.yml b/bitacross-worker/docker/docker-compose.yml index fedb1e549f..95116de23b 100644 --- a/bitacross-worker/docker/docker-compose.yml +++ b/bitacross-worker/docker/docker-compose.yml @@ -118,10 +118,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= diff --git a/bitacross-worker/docker/multiworker-docker-compose.yml b/bitacross-worker/docker/multiworker-docker-compose.yml index 081077de84..a3b2c2aa90 100644 --- a/bitacross-worker/docker/multiworker-docker-compose.yml +++ b/bitacross-worker/docker/multiworker-docker-compose.yml @@ -119,10 +119,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= @@ -169,10 +168,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= @@ -219,10 +217,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= diff --git a/bitacross-worker/scripts/litentry/release/config.json.eg b/bitacross-worker/scripts/litentry/release/config.json.eg index acfdbc872a..c655446160 100644 --- a/bitacross-worker/scripts/litentry/release/config.json.eg +++ b/bitacross-worker/scripts/litentry/release/config.json.eg @@ -1,9 +1,8 @@ { "twitter_official_url": "https://api.twitter.com", - "twitter_litentry_url": "", "twitter_auth_token_v2": "abcdefghijklmnopqrstuvwxyz", "discord_official_url": "https://discordapp.com", - "discord_litentry_url": "", + "litentry_discord_microservice_url": "", "discord_auth_token": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", "achainable_url": "https://graph.tdf-labs.io/", "achainable_auth_key": "88888888-4444-4444-4444-1234567890ab", diff --git a/tee-worker/docker/docker-compose.yml b/tee-worker/docker/docker-compose.yml index c9436092d0..938565778b 100644 --- a/tee-worker/docker/docker-compose.yml +++ b/tee-worker/docker/docker-compose.yml @@ -118,10 +118,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= diff --git a/tee-worker/docker/multiworker-docker-compose.yml b/tee-worker/docker/multiworker-docker-compose.yml index ffc6c7d27c..ce3688afe1 100644 --- a/tee-worker/docker/multiworker-docker-compose.yml +++ b/tee-worker/docker/multiworker-docker-compose.yml @@ -119,10 +119,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= @@ -171,10 +170,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= @@ -223,10 +221,9 @@ services: environment: - RUST_LOG=info,litentry_worker=debug,ws=warn,sp_io=error,substrate_api_client=warn,itc_parentchain_light_client=info,jsonrpsee_ws_client=warn,jsonrpsee_ws_server=warn,enclave_runtime=debug,ita_stf=debug,its_rpc_handler=warn,itc_rpc_client=warn,its_consensus_common=debug,its_state=warn,its_consensus_aura=warn,aura*=warn,its_consensus_slots=warn,itp_attestation_handler=debug,http_req=debug,lc_mock_server=warn,itc_rest_client=debug,lc_credentials=debug,lc_identity_verification=debug,lc_stf_task_receiver=debug,lc_stf_task_sender=debug,lc_data_providers=debug,itp_top_pool=debug,itc_parentchain_indirect_calls_executor=debug, - TWITTER_OFFICIAL_URL=http://localhost:19527 - - TWITTER_LITENTRY_URL=http://localhost:19527 - TWITTER_AUTH_TOKEN_V2= - DISCORD_OFFICIAL_URL=http://localhost:19527 - - DISCORD_LITENTRY_URL=http://localhost:19527 + - LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 - DISCORD_AUTH_TOKEN= - ACHAINABLE_URL=http://localhost:19527 - ACHAINABLE_AUTH_KEY= diff --git a/tee-worker/litentry/core/assertion-build/src/a2.rs b/tee-worker/litentry/core/assertion-build/src/a2.rs index 191777cb49..4307645078 100644 --- a/tee-worker/litentry/core/assertion-build/src/a2.rs +++ b/tee-worker/litentry/core/assertion-build/src/a2.rs @@ -25,7 +25,7 @@ use lc_data_providers::{ discord_litentry::DiscordLitentryClient, vec_to_string, DataProviderConfig, }; -const VC_A2_SUBJECT_DESCRIPTION: &str = "The user is a member of Litentry Discord. +const VC_A2_SUBJECT_DESCRIPTION: &str = "The user is a member of Litentry Discord. Server link: https://discord.gg/phBSa3eMX9 Guild ID: 807161594245152800."; const VC_A2_SUBJECT_TYPE: &str = "Litentry Discord Member"; @@ -44,7 +44,8 @@ pub fn build( Error::RequestVCFailed(Assertion::A2(guild_id.clone()), ErrorDetail::ParseError) })?; - let mut client = DiscordLitentryClient::new(&data_provider_config.discord_litentry_url); + let mut client = + DiscordLitentryClient::new(&data_provider_config.litentry_discord_microservice_url); for identity in &req.identities { if let Identity::Discord(address) = &identity.0 { discord_cnt += 1; @@ -94,7 +95,8 @@ mod tests { #[test] fn build_a2_works() { let mut data_provider_config = DataProviderConfig::new(); - data_provider_config.set_discord_litentry_url("http://localhost:19527".to_string()); + data_provider_config + .set_litentry_discord_microservice_url("http://localhost:19527".to_string()); let guild_id_u: u64 = 919848390156767232; let guild_id_vec: Vec = format!("{}", guild_id_u).as_bytes().to_vec(); diff --git a/tee-worker/litentry/core/assertion-build/src/a3.rs b/tee-worker/litentry/core/assertion-build/src/a3.rs index 9e43923fba..0f2bad9a2f 100644 --- a/tee-worker/litentry/core/assertion-build/src/a3.rs +++ b/tee-worker/litentry/core/assertion-build/src/a3.rs @@ -59,7 +59,8 @@ pub fn build( ) })?; - let mut client = DiscordLitentryClient::new(&data_provider_config.discord_litentry_url); + let mut client = + DiscordLitentryClient::new(&data_provider_config.litentry_discord_microservice_url); for identity in &req.identities { if let Identity::Discord(address) = &identity.0 { let resp = client @@ -117,7 +118,8 @@ mod tests { #[test] fn build_a3_works() { let mut data_provider_config = DataProviderConfig::new(); - data_provider_config.set_discord_litentry_url("http://localhost:19527".to_string()); + data_provider_config + .set_litentry_discord_microservice_url("http://localhost:19527".to_string()); let guild_id_u: u64 = 919848390156767232; let channel_id_u: u64 = 919848392035794945; let role_id_u: u64 = 1034083718425493544; diff --git a/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs b/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs index cadb2ef9e2..e460b7a324 100644 --- a/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs +++ b/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs @@ -38,7 +38,8 @@ pub fn build( })?; let mut has_role_value = false; - let mut client = DiscordLitentryClient::new(&data_provider_config.discord_litentry_url); + let mut client = + DiscordLitentryClient::new(&data_provider_config.litentry_discord_microservice_url); for identity in &req.identities { if let Identity::Discord(address) = &identity.0 { let resp = @@ -110,7 +111,7 @@ mod tests { let url = run(0).unwrap(); let mut data_provider_conifg = DataProviderConfig::new(); - data_provider_conifg.set_discord_litentry_url(url); + data_provider_conifg.set_litentry_discord_microservice_url(url); data_provider_conifg.set_contest_legend_discord_role_id("1034083718425493544".to_string()); data_provider_conifg.set_sora_quiz_attendee_id("1034083718425493544".to_string()); data_provider_conifg diff --git a/tee-worker/litentry/core/data-providers/src/discord_litentry.rs b/tee-worker/litentry/core/data-providers/src/discord_litentry.rs index 129f5ce55e..302013aaa7 100644 --- a/tee-worker/litentry/core/data-providers/src/discord_litentry.rs +++ b/tee-worker/litentry/core/data-providers/src/discord_litentry.rs @@ -17,11 +17,11 @@ #[cfg(all(not(feature = "std"), feature = "sgx"))] use crate::sgx_reexport_prelude::*; -use crate::{build_client, vec_to_string, Error, HttpError}; +use crate::{build_client_with_cert, vec_to_string, Error, HttpError}; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::RestClient, RestGet, RestPath, }; @@ -50,14 +50,14 @@ impl RestPath for DiscordResponse { } pub struct DiscordLitentryClient { - client: RestClient>, + client: RestClient>, } impl DiscordLitentryClient { pub fn new(url: &str) -> Self { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let client = build_client(url, headers); + let client = build_client_with_cert(url, headers); DiscordLitentryClient { client } } @@ -162,7 +162,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); let mut data_provider_config = DataProviderConfig::new(); - data_provider_config.set_discord_litentry_url(url); + data_provider_config.set_litentry_discord_microservice_url(url); data_provider_config } @@ -171,7 +171,8 @@ mod tests { let data_provider_config = init(); let guild_id = "919848390156767232".as_bytes().to_vec(); let handler = "againstwar".as_bytes().to_vec(); - let mut client = DiscordLitentryClient::new(&data_provider_config.discord_litentry_url); + let mut client = + DiscordLitentryClient::new(&data_provider_config.litentry_discord_microservice_url); let response = client.check_join(guild_id, handler); assert!(response.is_ok(), "check join discord error: {:?}", response); } @@ -183,7 +184,8 @@ mod tests { let channel_id = "919848392035794945".as_bytes().to_vec(); let role_id = "1034083718425493544".as_bytes().to_vec(); let handler = "ericzhang.eth".as_bytes().to_vec(); - let mut client = DiscordLitentryClient::new(&data_provider_config.discord_litentry_url); + let mut client = + DiscordLitentryClient::new(&data_provider_config.litentry_discord_microservice_url); let response = client.check_id_hubber(guild_id, channel_id, role_id, handler); assert!(response.is_ok(), "check discord id hubber error: {:?}", response); } diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index e6e4050773..6d9cbebcb9 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -169,10 +169,9 @@ impl TokenFromString for ETokenAddress { #[derive(PartialEq, Eq, Clone, Encode, Decode, Serialize, Deserialize, Debug)] pub struct DataProviderConfig { pub twitter_official_url: String, - pub twitter_litentry_url: String, pub twitter_auth_token_v2: String, pub discord_official_url: String, - pub discord_litentry_url: String, + pub litentry_discord_microservice_url: String, pub discord_auth_token: String, pub achainable_url: String, pub achainable_auth_key: String, @@ -215,10 +214,10 @@ impl DataProviderConfig { // default prod config let mut config = DataProviderConfig { twitter_official_url: "https://api.twitter.com".to_string(), - twitter_litentry_url: "http://127.0.0.1:9527”".to_string(), twitter_auth_token_v2: "".to_string(), discord_official_url: "https://discordapp.com".to_string(), - discord_litentry_url: "http://127.0.0.1:9527”".to_string(), + litentry_discord_microservice_url: "https://tee-microservice.litentry.io:9528" + .to_string(), discord_auth_token: "".to_string(), achainable_url: "https://label-production.graph.tdf-labs.io/".to_string(), achainable_auth_key: "".to_string(), @@ -255,14 +254,11 @@ impl DataProviderConfig { if let Ok(v) = env::var("TWITTER_OFFICIAL_URL") { config.set_twitter_official_url(v); } - if let Ok(v) = env::var("TWITTER_LITENTRY_URL") { - config.set_twitter_litentry_url(v); - } if let Ok(v) = env::var("DISCORD_OFFICIAL_URL") { config.set_discord_official_url(v); } - if let Ok(v) = env::var("DISCORD_LITENTRY_URL") { - config.set_discord_litentry_url(v); + if let Ok(v) = env::var("LITENTRY_DISCORD_MICROSERVICE_URL") { + config.set_litentry_discord_microservice_url(v); } if let Ok(v) = env::var("ACHAINABLE_URL") { config.set_achainable_url(v); @@ -356,10 +352,6 @@ impl DataProviderConfig { debug!("set_twitter_official_url: {:?}", v); self.twitter_official_url = v; } - pub fn set_twitter_litentry_url(&mut self, v: String) { - debug!("set_twitter_litentry_url: {:?}", v); - self.twitter_litentry_url = v; - } pub fn set_twitter_auth_token_v2(&mut self, v: String) { debug!("set_twitter_auth_token_v2: {:?}", v); self.twitter_auth_token_v2 = v; @@ -368,9 +360,9 @@ impl DataProviderConfig { debug!("set_discord_official_url: {:?}", v); self.discord_official_url = v; } - pub fn set_discord_litentry_url(&mut self, v: String) { - debug!("set_discord_litentry_url: {:?}", v); - self.discord_litentry_url = v; + pub fn set_litentry_discord_microservice_url(&mut self, v: String) { + debug!("set_litentry_discord_microservice_url: {:?}", v); + self.litentry_discord_microservice_url = v; } pub fn set_discord_auth_token(&mut self, v: String) { debug!("set_discord_auth_token: {:?}", v); diff --git a/tee-worker/litentry/core/mock-server/src/lib.rs b/tee-worker/litentry/core/mock-server/src/lib.rs index 7d50adcc4e..d725b79ed5 100644 --- a/tee-worker/litentry/core/mock-server/src/lib.rs +++ b/tee-worker/litentry/core/mock-server/src/lib.rs @@ -28,7 +28,6 @@ pub mod karat_dao; pub mod litentry_archive; pub mod moralis; pub mod nodereal_jsonrpc; -pub mod twitter_litentry; pub mod twitter_official; pub mod vip3; @@ -60,7 +59,6 @@ pub fn run(port: u16) -> Result { .or(twitter_official::query_retweeted_by()) .or(twitter_official::query_user_by_name()) .or(twitter_official::query_user_by_id()) - .or(twitter_litentry::check_follow()) .or(discord_official::query_message()) .or(discord_litentry::check_id_hubber()) .or(discord_litentry::check_join()) diff --git a/tee-worker/litentry/core/mock-server/src/twitter_litentry.rs b/tee-worker/litentry/core/mock-server/src/twitter_litentry.rs deleted file mode 100644 index 00443af0eb..0000000000 --- a/tee-worker/litentry/core/mock-server/src/twitter_litentry.rs +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright 2020-2024 Trust Computing GmbH. -// This file is part of Litentry. -// -// Litentry is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// Litentry is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with Litentry. If not, see . -#![allow(opaque_hidden_inferred_bound)] - -use std::collections::HashMap; -use warp::{http::Response, Filter}; - -pub(crate) fn check_follow( -) -> impl Filter + Clone { - warp::get() - .and(warp::path!("twitter" / "followers" / "verification")) - .and(warp::query::>()) - .map(move |p: HashMap| { - let default = String::default(); - let handler1 = p.get("handler1").unwrap_or(&default); - let handler2 = p.get("handler2").unwrap_or(&default); - - let body = r#"{ "data": false }"#; - if handler1.as_str() == "litentry" && handler2 == "paritytech" { - Response::builder().body(body.to_string()) - } else { - Response::builder().status(400).body(String::from("Error query")) - } - }) -} diff --git a/tee-worker/local-setup/.env.dev b/tee-worker/local-setup/.env.dev index 2e82cc537b..5f9c5dbec7 100644 --- a/tee-worker/local-setup/.env.dev +++ b/tee-worker/local-setup/.env.dev @@ -36,9 +36,8 @@ MORALIS_API_KEY= # The followings are default value. # Can be skipped; or overwrite within non-production mode. TWITTER_OFFICIAL_URL=http://localhost:19527 -TWITTER_LITENTRY_URL=http://localhost:19527 DISCORD_OFFICIAL_URL=http://localhost:19527 -DISCORD_LITENTRY_URL=http://localhost:19527 +LITENTRY_DISCORD_MICROSERVICE_URL=http://localhost:19527 ACHAINABLE_URL=http://localhost:19527 CREDENTIAL_ENDPOINT=http://localhost:9933 ONEBLOCK_NOTION_URL=https://abc.com From 655fc19038fd02ac95823c7142c14ddf4b50c2b8 Mon Sep 17 00:00:00 2001 From: BillyWooo Date: Fri, 23 Feb 2024 15:57:06 +0100 Subject: [PATCH 64/64] P 491 - url checking (#2511) * this should be a warn message. * keep consistency with tee-worker * fix the typo error, which causes crash * remove `build_client`; all use `build_client_with_cert`. * remove DataProviderConfig::default() * add url sanity check at initialization * add missing return value --------- Co-authored-by: Yang --- .../core-primitives/settings/src/lib.rs | 2 +- .../enclave-runtime/src/initialization/mod.rs | 7 +- .../assertion-build-v2/src/nft_holder/mod.rs | 2 +- .../src/platform_user/mod.rs | 2 +- .../src/token_holding_amount/mod.rs | 2 +- .../litentry/core/assertion-build/src/a14.rs | 8 +- .../litentry/core/assertion-build/src/a2.rs | 2 +- .../litentry/core/assertion-build/src/a20.rs | 4 +- .../litentry/core/assertion-build/src/a3.rs | 2 +- .../src/generic_discord_role.rs | 2 +- .../core/assertion-build/src/holding_time.rs | 2 +- .../core/assertion-build/src/lit_staking.rs | 8 +- .../amount_holding/evm_amount_holding.rs | 2 +- .../nft_holder/weirdo_ghost_gang_holder.rs | 2 +- .../core/assertion-build/src/oneblock/mod.rs | 4 +- .../core/data-providers/src/achainable.rs | 2 +- .../data-providers/src/discord_litentry.rs | 2 +- .../data-providers/src/discord_official.rs | 2 +- .../core/data-providers/src/karat_dao.rs | 11 ++- .../litentry/core/data-providers/src/lib.rs | 99 +++++++++++-------- .../core/data-providers/src/moralis.rs | 11 ++- .../data-providers/src/nodereal_jsonrpc.rs | 12 +-- .../data-providers/src/twitter_official.rs | 2 +- .../core/stf-task/receiver/src/test.rs | 2 +- .../sidechain/consensus/slots/src/lib.rs | 2 +- 25 files changed, 109 insertions(+), 87 deletions(-) diff --git a/bitacross-worker/core-primitives/settings/src/lib.rs b/bitacross-worker/core-primitives/settings/src/lib.rs index fb3a84fd7a..74e9838a75 100644 --- a/bitacross-worker/core-primitives/settings/src/lib.rs +++ b/bitacross-worker/core-primitives/settings/src/lib.rs @@ -28,7 +28,7 @@ pub mod files { // used by enclave /// Path to the light-client db for the Integritee parentchain. - pub const LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "integritee_lcdb"; + pub const LITENTRY_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "litentry_lcdb"; /// Path to the light-client db for the Target A parentchain. pub const TARGET_A_PARENTCHAIN_LIGHT_CLIENT_DB_PATH: &str = "target_a_lcdb"; diff --git a/tee-worker/enclave-runtime/src/initialization/mod.rs b/tee-worker/enclave-runtime/src/initialization/mod.rs index 5f50cdb42d..a0edccab0b 100644 --- a/tee-worker/enclave-runtime/src/initialization/mod.rs +++ b/tee-worker/enclave-runtime/src/initialization/mod.rs @@ -216,8 +216,11 @@ pub(crate) fn init_enclave( Arc::new(IntelAttestationHandler::new(ocall_api, signing_key_repository)); GLOBAL_ATTESTATION_HANDLER_COMPONENT.initialize(attestation_handler); - let data_provider_config = DataProviderConfig::new(); - GLOBAL_DATA_PROVIDER_CONFIG.initialize(data_provider_config.into()); + if let Ok(data_provider_config) = DataProviderConfig::new() { + GLOBAL_DATA_PROVIDER_CONFIG.initialize(data_provider_config.into()); + } else { + return Err(Error::Other("data provider initialize error".into())) + } Ok(()) } diff --git a/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs index 677f8f1b82..bb4e1ed443 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/nft_holder/mod.rs @@ -189,7 +189,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::default(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".into()); data_provider_config.set_nodereal_api_chain_network_url(url.clone() + "/nodereal_jsonrpc/"); diff --git a/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs index 5ba68f7f9b..5c261f8f92 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/platform_user/mod.rs @@ -88,7 +88,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/karat_dao/"; - let mut config = DataProviderConfig::new(); + let mut config = DataProviderConfig::new().unwrap(); config.set_karat_dao_api_url(url); config } diff --git a/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs b/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs index a39660141b..50ca6477e1 100644 --- a/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs +++ b/tee-worker/litentry/core/assertion-build-v2/src/token_holding_amount/mod.rs @@ -287,7 +287,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::default(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".into()); data_provider_config.set_nodereal_api_chain_network_url(url.clone() + "/nodereal_jsonrpc/"); diff --git a/tee-worker/litentry/core/assertion-build/src/a14.rs b/tee-worker/litentry/core/assertion-build/src/a14.rs index bde9bef807..91de2a1a3c 100644 --- a/tee-worker/litentry/core/assertion-build/src/a14.rs +++ b/tee-worker/litentry/core/assertion-build/src/a14.rs @@ -28,11 +28,11 @@ use http::header::{AUTHORIZATION, CONNECTION}; use http_req::response::Headers; use itc_rest_client::{ error::Error as RestClientError, - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::RestClient, RestPath, RestPost, }; -use lc_data_providers::{build_client, DataProviderConfig}; +use lc_data_providers::{build_client_with_cert, DataProviderConfig}; use serde::{Deserialize, Serialize}; const VC_A14_SUBJECT_DESCRIPTION: &str = @@ -67,7 +67,7 @@ pub struct A14Response { // TODO: merge it to new achainable API client once the migration is done pub struct A14Client { - client: RestClient>, + client: RestClient>, } impl A14Client { @@ -79,7 +79,7 @@ impl A14Client { data_provider_config.achainable_auth_key.clone().as_str(), ); let client = - build_client("https://label-production.graph.tdf-labs.io/v1/run/labels/a719e99c-1f9b-432e-8f1d-cb3de0f14dde", headers); + build_client_with_cert("https://label-production.graph.tdf-labs.io/v1/run/labels/a719e99c-1f9b-432e-8f1d-cb3de0f14dde", headers); A14Client { client } } diff --git a/tee-worker/litentry/core/assertion-build/src/a2.rs b/tee-worker/litentry/core/assertion-build/src/a2.rs index 4307645078..b351f1aea1 100644 --- a/tee-worker/litentry/core/assertion-build/src/a2.rs +++ b/tee-worker/litentry/core/assertion-build/src/a2.rs @@ -94,7 +94,7 @@ mod tests { #[test] fn build_a2_works() { - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config .set_litentry_discord_microservice_url("http://localhost:19527".to_string()); let guild_id_u: u64 = 919848390156767232; diff --git a/tee-worker/litentry/core/assertion-build/src/a20.rs b/tee-worker/litentry/core/assertion-build/src/a20.rs index 859f4358d9..822bcebabb 100644 --- a/tee-worker/litentry/core/assertion-build/src/a20.rs +++ b/tee-worker/litentry/core/assertion-build/src/a20.rs @@ -23,7 +23,7 @@ extern crate sgx_tstd as std; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{error::Error as RestClientError, RestGet, RestPath}; -use lc_data_providers::{build_client, DataProviderConfig}; +use lc_data_providers::{build_client_with_cert, DataProviderConfig}; use serde::{Deserialize, Serialize}; #[cfg(all(not(feature = "std"), feature = "sgx"))] @@ -65,7 +65,7 @@ pub fn build( let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let mut client = build_client(&data_provider_config.litentry_archive_url, headers); + let mut client = build_client_with_cert(&data_provider_config.litentry_archive_url, headers); let query = vec![("account", who.as_str())]; let value = client .get_with::("".to_string(), query.as_slice()) diff --git a/tee-worker/litentry/core/assertion-build/src/a3.rs b/tee-worker/litentry/core/assertion-build/src/a3.rs index 0f2bad9a2f..c0dc6f5a5d 100644 --- a/tee-worker/litentry/core/assertion-build/src/a3.rs +++ b/tee-worker/litentry/core/assertion-build/src/a3.rs @@ -117,7 +117,7 @@ mod tests { #[test] fn build_a3_works() { - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config .set_litentry_discord_microservice_url("http://localhost:19527".to_string()); let guild_id_u: u64 = 919848390156767232; diff --git a/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs b/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs index e460b7a324..9f82b9ba19 100644 --- a/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs +++ b/tee-worker/litentry/core/assertion-build/src/generic_discord_role.rs @@ -109,7 +109,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_conifg = DataProviderConfig::new(); + let mut data_provider_conifg = DataProviderConfig::new().unwrap(); data_provider_conifg.set_litentry_discord_microservice_url(url); data_provider_conifg.set_contest_legend_discord_role_id("1034083718425493544".to_string()); diff --git a/tee-worker/litentry/core/assertion-build/src/holding_time.rs b/tee-worker/litentry/core/assertion-build/src/holding_time.rs index a6744d6eb0..c46fceafa4 100644 --- a/tee-worker/litentry/core/assertion-build/src/holding_time.rs +++ b/tee-worker/litentry/core/assertion-build/src/holding_time.rs @@ -345,7 +345,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_achainable_url(url); data_provider_config } diff --git a/tee-worker/litentry/core/assertion-build/src/lit_staking.rs b/tee-worker/litentry/core/assertion-build/src/lit_staking.rs index c24ef05256..2b1c1c5670 100644 --- a/tee-worker/litentry/core/assertion-build/src/lit_staking.rs +++ b/tee-worker/litentry/core/assertion-build/src/lit_staking.rs @@ -20,14 +20,14 @@ use frame_support::{StorageHasher, Twox64Concat}; use http::header::CONNECTION; use itc_rest_client::{ error::Error as RestClientError, - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::{Headers, RestClient}, RestPath, RestPost, }; use itp_stf_primitives::types::AccountId; use itp_utils::hex_display::AsBytesRef; use lc_credentials::litentry_profile::lit_staking::UpdateLITStakingAmountCredential; -use lc_data_providers::build_client; +use lc_data_providers::build_client_with_cert; use litentry_primitives::ParentchainBalance; use pallet_parachain_staking::types::Delegator; use serde::{Deserialize, Serialize}; @@ -70,7 +70,7 @@ pub struct JsonRPCResponse { } pub struct LitentryStakingClient { - client: RestClient>, + client: RestClient>, } impl Default for LitentryStakingClient { @@ -83,7 +83,7 @@ impl LitentryStakingClient { pub fn new() -> Self { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let client = build_client("https://litentry-rpc.dwellir.com:443", headers); + let client = build_client_with_cert("https://litentry-rpc.dwellir.com:443", headers); LitentryStakingClient { client } } diff --git a/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs b/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs index cebb4616b7..a547680733 100644 --- a/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs +++ b/tee-worker/litentry/core/assertion-build/src/nodereal/amount_holding/evm_amount_holding.rs @@ -169,7 +169,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/nodereal_jsonrpc/"; - let mut data_provider_config = DataProviderConfig::default(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".into()); data_provider_config.set_nodereal_api_chain_network_url(url); diff --git a/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs b/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs index e768d5a420..df358ef0a6 100644 --- a/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs +++ b/tee-worker/litentry/core/assertion-build/src/nodereal/nft_holder/weirdo_ghost_gang_holder.rs @@ -120,7 +120,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/nodereal_jsonrpc/"; - let mut config = DataProviderConfig::new(); + let mut config = DataProviderConfig::new().unwrap(); config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".to_string()); config.set_nodereal_api_chain_network_url(url); diff --git a/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs b/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs index 438f6fd183..24e514cb28 100644 --- a/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs +++ b/tee-worker/litentry/core/assertion-build/src/oneblock/mod.rs @@ -26,7 +26,7 @@ use crate::*; use http::header::{AUTHORIZATION, CONNECTION}; use http_req::response::Headers; use itc_rest_client::{error::Error as RestClientError, RestGet, RestPath}; -use lc_data_providers::{build_client, DataProviderConfig}; +use lc_data_providers::{build_client_with_cert, DataProviderConfig}; use serde::{Deserialize, Serialize}; use std::string::ToString; @@ -55,7 +55,7 @@ fn fetch_data_from_notion( headers.insert("Notion-Version", "2022-06-28"); headers.insert(AUTHORIZATION.as_str(), oneblock_notion_key.as_str()); - let mut client = build_client(oneblock_notion_url.as_str(), headers); + let mut client = build_client_with_cert(oneblock_notion_url.as_str(), headers); client.get::(String::default()).map_err(|e| { Error::RequestVCFailed( diff --git a/tee-worker/litentry/core/data-providers/src/achainable.rs b/tee-worker/litentry/core/data-providers/src/achainable.rs index 41668ecfd7..733b2261c7 100644 --- a/tee-worker/litentry/core/data-providers/src/achainable.rs +++ b/tee-worker/litentry/core/data-providers/src/achainable.rs @@ -1441,7 +1441,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_achainable_url(url); AchainableClient::new(&data_provider_config) } diff --git a/tee-worker/litentry/core/data-providers/src/discord_litentry.rs b/tee-worker/litentry/core/data-providers/src/discord_litentry.rs index 302013aaa7..7ae93687de 100644 --- a/tee-worker/litentry/core/data-providers/src/discord_litentry.rs +++ b/tee-worker/litentry/core/data-providers/src/discord_litentry.rs @@ -161,7 +161,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_litentry_discord_microservice_url(url); data_provider_config } diff --git a/tee-worker/litentry/core/data-providers/src/discord_official.rs b/tee-worker/litentry/core/data-providers/src/discord_official.rs index 938b996133..7ee9d08617 100644 --- a/tee-worker/litentry/core/data-providers/src/discord_official.rs +++ b/tee-worker/litentry/core/data-providers/src/discord_official.rs @@ -125,7 +125,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_discord_official_url(url); data_provider_config } diff --git a/tee-worker/litentry/core/data-providers/src/karat_dao.rs b/tee-worker/litentry/core/data-providers/src/karat_dao.rs index 718fbfbd79..169ed48909 100644 --- a/tee-worker/litentry/core/data-providers/src/karat_dao.rs +++ b/tee-worker/litentry/core/data-providers/src/karat_dao.rs @@ -21,12 +21,13 @@ use crate::sgx_reexport_prelude::*; extern crate sgx_tstd as std; use crate::{ - build_client, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, RetryableRestGet, + build_client_with_cert, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, + RetryableRestGet, }; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::RestClient, RestPath, }; @@ -41,7 +42,7 @@ use std::{ pub struct KaratDaoClient { retry_option: RetryOption, - client: RestClient>, + client: RestClient>, } #[derive(Debug)] @@ -60,7 +61,7 @@ impl KaratDaoClient { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let client = build_client(api_url.as_str(), headers); + let client = build_client_with_cert(api_url.as_str(), headers); KaratDaoClient { retry_option, client } } @@ -146,7 +147,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/karat_dao/"; - let mut config = DataProviderConfig::new(); + let mut config = DataProviderConfig::new().unwrap(); config.set_karat_dao_api_url(url); config } diff --git a/tee-worker/litentry/core/data-providers/src/lib.rs b/tee-worker/litentry/core/data-providers/src/lib.rs index 6d9cbebcb9..908019f57d 100644 --- a/tee-worker/litentry/core/data-providers/src/lib.rs +++ b/tee-worker/litentry/core/data-providers/src/lib.rs @@ -41,7 +41,7 @@ use core::time::Duration; use http_req::response::Headers; use itc_rest_client::{ error::Error as HttpError, - http_client::{DefaultSend, HttpClient, Send}, + http_client::{HttpClient, Send, SendWithCertificateVerification}, rest_client::RestClient, Query, RestGet, RestPath, RestPost, }; @@ -50,7 +50,6 @@ use log::debug; use serde::{Deserialize, Serialize}; use std::{thread, vec}; -use itc_rest_client::http_client::SendWithCertificateVerification; use litentry_primitives::{ AchainableParams, Assertion, ErrorDetail, ErrorString, IntoErrorDetail, ParameterString, VCMPError, @@ -201,14 +200,8 @@ pub struct DataProviderConfig { pub moralis_api_key: String, } -impl Default for DataProviderConfig { - fn default() -> Self { - Self::new() - } -} - impl DataProviderConfig { - pub fn new() -> Self { + pub fn new() -> Result { log::debug!("Initializing data providers config"); // default prod config @@ -252,22 +245,22 @@ impl DataProviderConfig { // we allow to override following config properties for non prod dev if_not_production!({ if let Ok(v) = env::var("TWITTER_OFFICIAL_URL") { - config.set_twitter_official_url(v); + config.set_twitter_official_url(v)?; } if let Ok(v) = env::var("DISCORD_OFFICIAL_URL") { - config.set_discord_official_url(v); + config.set_discord_official_url(v)?; } if let Ok(v) = env::var("LITENTRY_DISCORD_MICROSERVICE_URL") { - config.set_litentry_discord_microservice_url(v); + config.set_litentry_discord_microservice_url(v)?; } if let Ok(v) = env::var("ACHAINABLE_URL") { - config.set_achainable_url(v); + config.set_achainable_url(v)?; } if let Ok(v) = env::var("CREDENTIAL_ENDPOINT") { config.set_credential_endpoint(v); } if let Ok(v) = env::var("ONEBLOCK_NOTION_URL") { - config.set_oneblock_notion_url(v); + config.set_oneblock_notion_url(v)?; } if let Ok(v) = env::var("SORA_QUIZ_MASTER_ID") { config.set_sora_quiz_master_id(v); @@ -276,7 +269,7 @@ impl DataProviderConfig { config.set_sora_quiz_attendee_id(v); } if let Ok(v) = env::var("NODEREAL_API_URL") { - config.set_nodereal_api_url(v); + config.set_nodereal_api_url(v)?; } if let Ok(v) = env::var("NODEREAL_API_RETRY_DELAY") { config.set_nodereal_api_retry_delay(v.parse::().unwrap()); @@ -285,7 +278,7 @@ impl DataProviderConfig { config.set_nodereal_api_retry_times(v.parse::().unwrap()); } if let Ok(v) = env::var("NODEREAL_API_CHAIN_NETWORK_URL") { - config.set_nodereal_api_chain_network_url(v); + config.set_nodereal_api_chain_network_url(v)?; } if let Ok(v) = env::var("CONTEST_LEGEND_DISCORD_ROLE_ID") { config.set_contest_legend_discord_role_id(v); @@ -297,13 +290,13 @@ impl DataProviderConfig { config.set_contest_participant_discord_role_id(v); } if let Ok(v) = env::var("VIP3_URL") { - config.set_vip3_url(v); + config.set_vip3_url(v)?; } if let Ok(v) = env::var("GENIIDATA_URL") { - config.set_geniidata_url(v); + config.set_geniidata_url(v)?; } if let Ok(v) = env::var("LITENTRY_ARCHIVE_URL") { - config.set_litentry_archive_url(v); + config.set_litentry_archive_url(v)?; } if let Ok(v) = env::var("KARAT_DAO_API_RETRY_DELAY") { config.set_karat_dao_api_retry_delay(v.parse::().unwrap()); @@ -312,10 +305,10 @@ impl DataProviderConfig { config.set_karat_dao_api_retry_times(v.parse::().unwrap()); } if let Ok(v) = env::var("KARAT_DAO_API_URL") { - config.set_karat_dao_api_url(v); + config.set_karat_dao_api_url(v)?; } if let Ok(v) = env::var("MORALIS_API_URL") { - config.set_moralis_api_url(v); + config.set_moralis_api_url(v)?; } if let Ok(v) = env::var("MORALIS_API_RETRY_DELAY") { config.set_moralis_api_retry_delay(v.parse::().unwrap()); @@ -346,31 +339,39 @@ impl DataProviderConfig { if let Ok(v) = env::var("MORALIS_API_KEY") { config.set_moralis_api_key(v); } - config + Ok(config) } - pub fn set_twitter_official_url(&mut self, v: String) { + pub fn set_twitter_official_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_twitter_official_url: {:?}", v); self.twitter_official_url = v; + Ok(()) } pub fn set_twitter_auth_token_v2(&mut self, v: String) { debug!("set_twitter_auth_token_v2: {:?}", v); self.twitter_auth_token_v2 = v; } - pub fn set_discord_official_url(&mut self, v: String) { + pub fn set_discord_official_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_discord_official_url: {:?}", v); self.discord_official_url = v; + Ok(()) } - pub fn set_litentry_discord_microservice_url(&mut self, v: String) { + pub fn set_litentry_discord_microservice_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_litentry_discord_microservice_url: {:?}", v); self.litentry_discord_microservice_url = v; + Ok(()) } pub fn set_discord_auth_token(&mut self, v: String) { debug!("set_discord_auth_token: {:?}", v); self.discord_auth_token = v; } - pub fn set_achainable_url(&mut self, v: String) { + pub fn set_achainable_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_achainable_url: {:?}", v); self.achainable_url = v; + Ok(()) } pub fn set_achainable_auth_key(&mut self, v: String) { debug!("set_achainable_auth_key: {:?}", v); @@ -384,9 +385,11 @@ impl DataProviderConfig { debug!("set_oneblock_notion_key: {:?}", v); self.oneblock_notion_key = v; } - pub fn set_oneblock_notion_url(&mut self, v: String) { + pub fn set_oneblock_notion_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_oneblock_notion_url: {:?}", v); self.oneblock_notion_url = v; + Ok(()) } pub fn set_sora_quiz_master_id(&mut self, v: String) { debug!("set_sora_quiz_master_id: {:?}", v); @@ -408,13 +411,17 @@ impl DataProviderConfig { debug!("set_nodereal_api_retry_times: {:?}", v); self.nodereal_api_retry_times = v; } - pub fn set_nodereal_api_url(&mut self, v: String) { + pub fn set_nodereal_api_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_nodereal_api_url: {:?}", v); self.nodereal_api_url = v; + Ok(()) } - pub fn set_nodereal_api_chain_network_url(&mut self, v: String) { + pub fn set_nodereal_api_chain_network_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_nodereal_api_chain_network_url: {:?}", v); self.nodereal_api_chain_network_url = v; + Ok(()) } pub fn set_contest_legend_discord_role_id(&mut self, v: String) { debug!("set_contest_legend_discord_role_id: {:?}", v); @@ -428,21 +435,27 @@ impl DataProviderConfig { debug!("set_contest_participant_discord_role_id: {:?}", v); self.contest_participant_discord_role_id = v; } - pub fn set_vip3_url(&mut self, v: String) { + pub fn set_vip3_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_vip3_url: {:?}", v); self.vip3_url = v; + Ok(()) } - pub fn set_geniidata_url(&mut self, v: String) { + pub fn set_geniidata_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_geniidata_url: {:?}", v); self.geniidata_url = v; + Ok(()) } pub fn set_geniidata_api_key(&mut self, v: String) { debug!("set_geniidata_api_key: {:?}", v); self.geniidata_api_key = v; } - pub fn set_litentry_archive_url(&mut self, v: String) { + pub fn set_litentry_archive_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_litentry_archive_url: {:?}", v); self.litentry_archive_url = v; + Ok(()) } pub fn set_karat_dao_api_retry_delay(&mut self, v: u64) { debug!("set_karat_dao_api_retry_delay: {:?}", v); @@ -452,9 +465,11 @@ impl DataProviderConfig { debug!("set_karat_dao_api_retry_times: {:?}", v); self.karat_dao_api_retry_times = v; } - pub fn set_karat_dao_api_url(&mut self, v: String) { + pub fn set_karat_dao_api_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_karat_dao_api_url: {:?}", v); self.karat_dao_api_url = v; + Ok(()) } pub fn set_moralis_api_key(&mut self, v: String) { debug!("set_moralis_api_key: {:?}", v); @@ -468,9 +483,18 @@ impl DataProviderConfig { debug!("set_moralis_api_retry_times: {:?}", v); self.moralis_api_retry_times = v; } - pub fn set_moralis_api_url(&mut self, v: String) { + pub fn set_moralis_api_url(&mut self, v: String) -> Result<(), Error> { + check_url(&v)?; debug!("set_moralis_api_url: {:?}", v); self.moralis_api_url = v; + Ok(()) + } +} + +fn check_url(v: &String) -> Result<(), Error> { + match Url::parse(v) { + Ok(_) => Ok(()), + Err(err) => Err(Error::Utf8Error(format!("Input url: {:?}, parse error: {:?}", v, err))), } } @@ -516,13 +540,6 @@ pub fn vec_to_string(vec: Vec) -> Result { Ok(tmp.to_string()) } -pub fn build_client(base_url: &str, headers: Headers) -> RestClient> { - debug!("base_url: {}", base_url); - let base_url = Url::parse(base_url).unwrap(); - let http_client = HttpClient::new(DefaultSend {}, true, Some(TIMEOUT), Some(headers), None); - RestClient::new(http_client, base_url) -} - pub fn build_client_with_cert( base_url: &str, headers: Headers, diff --git a/tee-worker/litentry/core/data-providers/src/moralis.rs b/tee-worker/litentry/core/data-providers/src/moralis.rs index 4a606dcb44..8adefdad13 100644 --- a/tee-worker/litentry/core/data-providers/src/moralis.rs +++ b/tee-worker/litentry/core/data-providers/src/moralis.rs @@ -18,12 +18,13 @@ use crate::sgx_reexport_prelude::*; use crate::{ - build_client, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, RetryableRestGet, + build_client_with_cert, DataProviderConfig, Error, HttpError, ReqPath, RetryOption, + RetryableRestGet, }; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::RestClient, RestPath, }; @@ -45,7 +46,7 @@ pub struct MoralisRequest { pub struct MoralisClient { retry_option: RetryOption, - client: RestClient>, + client: RestClient>, } impl MoralisClient { @@ -60,7 +61,7 @@ impl MoralisClient { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); headers.insert("X-API-Key", api_key.as_str()); - let client = build_client(api_url.as_str(), headers); + let client = build_client_with_cert(api_url.as_str(), headers); MoralisClient { retry_option, client } } @@ -206,7 +207,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/moralis/"; - let mut config = DataProviderConfig::new(); + let mut config = DataProviderConfig::new().unwrap(); config.set_moralis_api_key("d416f55179dbd0e45b1a8ed030e3".to_string()); config.set_moralis_api_url(url); config diff --git a/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs b/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs index 905c473591..713535d2bc 100644 --- a/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs +++ b/tee-worker/litentry/core/data-providers/src/nodereal_jsonrpc.rs @@ -18,13 +18,13 @@ use crate::sgx_reexport_prelude::*; use crate::{ - build_client, convert_balance_hex_json_value_to_u128, DataProviderConfig, Error, HttpError, - ReqPath, RetryOption, RetryableRestPost, + build_client_with_cert, convert_balance_hex_json_value_to_u128, DataProviderConfig, Error, + HttpError, ReqPath, RetryOption, RetryableRestPost, }; use http::header::CONNECTION; use http_req::response::Headers; use itc_rest_client::{ - http_client::{DefaultSend, HttpClient}, + http_client::{HttpClient, SendWithCertificateVerification}, rest_client::RestClient, RestPath, }; @@ -111,7 +111,7 @@ pub struct RpcResponse { pub struct NoderealJsonrpcClient { path: String, retry_option: RetryOption, - client: RestClient>, + client: RestClient>, } impl NoderealJsonrpcClient { @@ -127,7 +127,7 @@ impl NoderealJsonrpcClient { let mut headers = Headers::new(); headers.insert(CONNECTION.as_str(), "close"); - let client = build_client(base_url.as_str(), headers); + let client = build_client_with_cert(base_url.as_str(), headers); NoderealJsonrpcClient { path, retry_option, client } } @@ -529,7 +529,7 @@ mod tests { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap() + "/nodereal_jsonrpc/"; - let mut config = DataProviderConfig::new(); + let mut config = DataProviderConfig::new().unwrap(); config.set_nodereal_api_key("d416f55179dbd0e45b1a8ed030e3".to_string()); config.set_nodereal_api_chain_network_url(url); config diff --git a/tee-worker/litentry/core/data-providers/src/twitter_official.rs b/tee-worker/litentry/core/data-providers/src/twitter_official.rs index 09404f862a..43d6b7d618 100644 --- a/tee-worker/litentry/core/data-providers/src/twitter_official.rs +++ b/tee-worker/litentry/core/data-providers/src/twitter_official.rs @@ -255,7 +255,7 @@ mod tests { fn init() -> DataProviderConfig { let _ = env_logger::builder().is_test(true).try_init(); let url = run(0).unwrap(); - let mut data_provider_config = DataProviderConfig::new(); + let mut data_provider_config = DataProviderConfig::new().unwrap(); data_provider_config.set_twitter_official_url(url); data_provider_config } diff --git a/tee-worker/litentry/core/stf-task/receiver/src/test.rs b/tee-worker/litentry/core/stf-task/receiver/src/test.rs index 61a9048590..1e55e37c90 100644 --- a/tee-worker/litentry/core/stf-task/receiver/src/test.rs +++ b/tee-worker/litentry/core/stf-task/receiver/src/test.rs @@ -21,7 +21,7 @@ fn test_threadpool_behaviour() { let stf_enclave_signer_mock = StfEnclaveSignerMock::default(); let handle_state_mock = HandleStateMock::default(); let onchain_mock = OnchainMock::default(); - let data_provider_conifg = DataProviderConfig::new(); + let data_provider_conifg = DataProviderConfig::new().unwrap(); let context = StfTaskContext::new( Arc::new(shielding_key_repository_mock), author_mock.into(), diff --git a/tee-worker/sidechain/consensus/slots/src/lib.rs b/tee-worker/sidechain/consensus/slots/src/lib.rs index c27f9d2450..b2201cfb08 100644 --- a/tee-worker/sidechain/consensus/slots/src/lib.rs +++ b/tee-worker/sidechain/consensus/slots/src/lib.rs @@ -466,7 +466,7 @@ pub trait SimpleSlotWorker { }; if is_single_worker { - error!("Running as single worker, skipping timestamp within slot check") + warn!("Running as single worker, skipping timestamp within slot check") } else if !timestamp_within_slot(&slot_info, &proposing.block) { warn!( "⌛️ Discarding proposal for slot {}, block number {}; block production took too long",

    0!QfkZ{$rnf@G8(VU9V?c|7s?i{ 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�#!$ys^60y!9+@vl7D+d5C1K`NwN!`VGsax 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�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 z%0A?KB|?uXF>w(_f1C(|41oWB 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