diff --git a/CHANGELOG.md b/CHANGELOG.md index 49662da79..762225070 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ At the moment this project **does not** adhere to - Signing flow with derived accounts ([#990](https://github.com/entropyxyz/entropy-core/pull/990)) - TSS attestation endpoint ([#1001](https://github.com/entropyxyz/entropy-core/pull/1001)) - Add `network-jumpstart` command to `entropy-test-cli` ([#1004](https://github.com/entropyxyz/entropy-core/pull/1004)) +- Attestation pallet ([#1003](https://github.com/entropyxyz/entropy-core/pull/1003)) - Update test CLI for new registration and signing flows ([#1008](https://github.com/entropyxyz/entropy-core/pull/1008)) ### Changed diff --git a/Cargo.lock b/Cargo.lock index 964cad4ab..8a1cdd57e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2619,6 +2619,7 @@ dependencies = [ "frame-try-runtime 0.35.0", "hex-literal", "log", + "pallet-attestation", "pallet-authority-discovery", "pallet-authorship", "pallet-babe", @@ -6671,6 +6672,36 @@ dependencies = [ "primeorder", ] +[[package]] +name = "pallet-attestation" +version = "0.2.0" +dependencies = [ + "entropy-shared", + "frame-benchmarking", + "frame-election-provider-support", + "frame-support 29.0.2", + "frame-system", + "log", + "pallet-bags-list", + "pallet-balances", + "pallet-parameters", + "pallet-session", + "pallet-staking", + "pallet-staking-extension", + "pallet-staking-reward-curve", + "pallet-timestamp", + "parity-scale-codec", + "rand_core 0.6.4", + "scale-info", + "sp-core 29.0.0", + "sp-io 31.0.0", + "sp-npos-elections", + "sp-runtime 32.0.0", + "sp-staking 27.0.0", + "sp-std 14.0.0", + "tdx-quote", +] + [[package]] name = "pallet-authority-discovery" version = "29.0.1" @@ -7162,6 +7193,7 @@ dependencies = [ "frame-support 29.0.2", "frame-system", "log", + "pallet-attestation", "pallet-authorship", "pallet-babe", "pallet-bags-list", diff --git a/crates/client/entropy_metadata.scale b/crates/client/entropy_metadata.scale index 421e53253..c0c5982a0 100644 Binary files a/crates/client/entropy_metadata.scale and b/crates/client/entropy_metadata.scale differ diff --git a/crates/shared/src/types.rs b/crates/shared/src/types.rs index 8e42d8960..49b558186 100644 --- a/crates/shared/src/types.rs +++ b/crates/shared/src/types.rs @@ -82,6 +82,15 @@ pub struct OcwMessageProactiveRefresh { pub proactive_refresh_keys: Vec>, } +/// Offchain worker message for requesting a TDX attestation +#[cfg(not(feature = "wasm"))] +#[cfg_attr(feature = "std", derive(Serialize, Deserialize))] +#[derive(Clone, Encode, Decode, Debug, Eq, PartialEq, TypeInfo)] +pub struct OcwMessageAttestationRequest { + /// The account ids of all TSS servers who must submit an attestation this block + pub tss_account_ids: Vec<[u8; 32]>, +} + /// 256-bit hashing algorithms for deriving the point to be signed. #[cfg_attr(any(feature = "wasm", feature = "std"), derive(Serialize, Deserialize))] #[cfg_attr(feature = "std", derive(EnumIter))] @@ -105,14 +114,14 @@ pub type EncodedVerifyingKey = [u8; VERIFICATION_KEY_LENGTH as usize]; pub struct QuoteInputData(pub [u8; 64]); impl QuoteInputData { - pub fn new( - tss_account_id: [u8; 32], + pub fn new( + tss_account_id: T, x25519_public_key: X25519PublicKey, nonce: [u8; 32], block_number: u32, ) -> Self { let mut hasher = Blake2b512::new(); - hasher.update(tss_account_id); + hasher.update(tss_account_id.encode()); hasher.update(x25519_public_key); hasher.update(nonce); hasher.update(block_number.to_be_bytes()); diff --git a/crates/threshold-signature-server/src/attestation/api.rs b/crates/threshold-signature-server/src/attestation/api.rs index 70971d50d..ae97b2fab 100644 --- a/crates/threshold-signature-server/src/attestation/api.rs +++ b/crates/threshold-signature-server/src/attestation/api.rs @@ -13,57 +13,98 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::{attestation::errors::AttestationErr, AppState}; +use crate::{ + attestation::errors::AttestationErr, + chain_api::{entropy, get_api, get_rpc, EntropyConfig}, + get_signer_and_x25519_secret, + helpers::substrate::{query_chain, submit_transaction}, + AppState, +}; use axum::{body::Bytes, extract::State, http::StatusCode}; +use entropy_shared::OcwMessageAttestationRequest; +use parity_scale_codec::Decode; +use sp_core::Pair; +use subxt::tx::PairSigner; +use x25519_dalek::StaticSecret; /// HTTP POST endpoint to initiate a TDX attestation. -/// Not yet implemented. -#[cfg(not(any(test, feature = "unsafe")))] -pub async fn attest( - State(_app_state): State, - _input: Bytes, -) -> Result { - // Non-mock attestation (the real thing) will go here - Err(AttestationErr::NotImplemented) -} - -/// HTTP POST endpoint to initiate a mock TDX attestation for testing on non-TDX hardware. /// The body of the request should be a 32 byte random nonce used to show 'freshness' of the /// quote. /// The response body contains a mock TDX v4 quote serialized as described in the /// [Index TDX DCAP Quoting Library API](https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf). -#[cfg(any(test, feature = "unsafe"))] pub async fn attest( State(app_state): State, input: Bytes, -) -> Result<(StatusCode, Bytes), AttestationErr> { - use crate::{chain_api::get_rpc, get_signer_and_x25519_secret}; - use rand_core::OsRng; - use sp_core::Pair; +) -> Result { + let (signer, x25519_secret) = get_signer_and_x25519_secret(&app_state.kv_store).await?; + let attestation_requests = OcwMessageAttestationRequest::decode(&mut input.as_ref())?; - // TODO (#982) confirm with the chain that an attestation should be happenning - let nonce = input.as_ref().try_into()?; + // Check whether there is an attestion request for us + if !attestation_requests.tss_account_ids.contains(&signer.signer().public().0) { + return Ok(StatusCode::OK); + } + let api = get_api(&app_state.configuration.endpoint).await?; let rpc = get_rpc(&app_state.configuration.endpoint).await?; + // Get the input nonce for this attestation + let nonce = { + let pending_attestation_query = + entropy::storage().attestation().pending_attestations(signer.account_id()); + query_chain(&api, &rpc, pending_attestation_query, None) + .await? + .ok_or_else(|| AttestationErr::Unexpected)? + }; + + // We also need the current block number as input let block_number = rpc.chain_get_header(None).await?.ok_or_else(|| AttestationErr::BlockNumber)?.number; + // We add 1 to the block number as this will be processed in the next block + let quote = create_quote(block_number + 1, nonce, &signer, &x25519_secret).await?; + + // Submit the quote + let attest_tx = entropy::tx().attestation().attest(quote.clone()); + submit_transaction(&api, &rpc, &signer, &attest_tx, None).await?; + + Ok(StatusCode::OK) +} + +/// Create a mock quote for testing on non-TDX hardware +#[cfg(any(test, feature = "unsafe"))] +pub async fn create_quote( + block_number: u32, + nonce: [u8; 32], + signer: &PairSigner, + x25519_secret: &StaticSecret, +) -> Result, AttestationErr> { + use rand_core::OsRng; + use sp_core::Pair; + // In the real thing this is the hardware key used in the quoting enclave let signing_key = tdx_quote::SigningKey::random(&mut OsRng); - let (signer, x25519_secret) = get_signer_and_x25519_secret(&app_state.kv_store).await?; - let public_key = x25519_dalek::PublicKey::from(&x25519_secret); + let public_key = x25519_dalek::PublicKey::from(x25519_secret); let input_data = entropy_shared::QuoteInputData::new( - signer.signer().public().into(), + signer.signer().public(), *public_key.as_bytes(), nonce, block_number, ); - let quote = tdx_quote::Quote::mock(signing_key.clone(), input_data.0); - // Here we would submit an attest extrinsic to the chain - but for now we just include it in the - // response - Ok((StatusCode::OK, Bytes::from(quote.as_bytes().to_vec()))) + let quote = tdx_quote::Quote::mock(signing_key.clone(), input_data.0).as_bytes().to_vec(); + Ok(quote) +} + +/// Once implemented, this will create a TDX quote in production +#[cfg(not(any(test, feature = "unsafe")))] +pub async fn create_quote( + _block_number: u32, + _nonce: [u8; 32], + _signer: &PairSigner, + _x25519_secret: &StaticSecret, +) -> Result, AttestationErr> { + // Non-mock attestation (the real thing) will go here + Err(AttestationErr::NotImplemented) } diff --git a/crates/threshold-signature-server/src/attestation/errors.rs b/crates/threshold-signature-server/src/attestation/errors.rs index 21c351cb2..0a6d52098 100644 --- a/crates/threshold-signature-server/src/attestation/errors.rs +++ b/crates/threshold-signature-server/src/attestation/errors.rs @@ -32,9 +32,14 @@ pub enum AttestationErr { NotImplemented, #[error("Input must be 32 bytes: {0}")] TryFromSlice(#[from] TryFromSliceError), - #[cfg(any(test, feature = "unsafe"))] #[error("Could not get block number")] BlockNumber, + #[error("Substrate: {0}")] + SubstrateClient(#[from] entropy_client::substrate::SubstrateError), + #[error("Got an attestation request but there is no pending attestation request on chain")] + Unexpected, + #[error("Could not decode message: {0}")] + Codec(#[from] parity_scale_codec::Error), } impl IntoResponse for AttestationErr { diff --git a/crates/threshold-signature-server/src/attestation/tests.rs b/crates/threshold-signature-server/src/attestation/tests.rs index 4966c3ad5..fb31117b9 100644 --- a/crates/threshold-signature-server/src/attestation/tests.rs +++ b/crates/threshold-signature-server/src/attestation/tests.rs @@ -12,12 +12,16 @@ // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -use crate::helpers::tests::{initialize_test_logger, spawn_testing_validators}; +use crate::{ + chain_api::{entropy, get_api, get_rpc}, + helpers::{ + substrate::query_chain, + tests::{initialize_test_logger, run_to_block, spawn_testing_validators}, + }, +}; use entropy_kvdb::clean_tests; -use entropy_shared::QuoteInputData; use entropy_testing_utils::{ - constants::{TSS_ACCOUNTS, X25519_PUBLIC_KEYS}, - substrate_context::test_node_process_testing_state, + constants::TSS_ACCOUNTS, substrate_context::test_node_process_stationary, }; use serial_test::serial; @@ -27,25 +31,34 @@ async fn test_attest() { initialize_test_logger().await; clean_tests(); - let _cxt = test_node_process_testing_state(false).await; + let cxt = test_node_process_stationary().await; let (_validator_ips, _validator_ids) = spawn_testing_validators(false).await; + let api = get_api(&cxt.ws_url).await.unwrap(); + let rpc = get_rpc(&cxt.ws_url).await.unwrap(); + + // Check that there is an attestation request at block 3 from the genesis config + let attestation_requests_query = entropy::storage().attestation().attestation_requests(3); + query_chain(&api, &rpc, attestation_requests_query, None).await.unwrap().unwrap(); - let nonce = [0; 32]; - let client = reqwest::Client::new(); - let res = client - .post(format!("http://127.0.0.1:3001/attest")) - .body(nonce.to_vec()) - .send() - .await - .unwrap(); - assert_eq!(res.status(), 200); - let quote = res.bytes().await.unwrap(); + // Get the nonce from the pending attestation from the genesis config + let nonce = { + let pending_attestation_query = + entropy::storage().attestation().pending_attestations(&TSS_ACCOUNTS[0]); + query_chain(&api, &rpc, pending_attestation_query, None).await.unwrap().unwrap() + }; + assert_eq!(nonce, [0; 32]); - // This internally verifies the signature in the quote - let quote = tdx_quote::Quote::from_bytes("e).unwrap(); + // Wait for the attestation to be handled + for _ in 0..10 { + let block_number = rpc.chain_get_header(None).await.unwrap().unwrap().number; + run_to_block(&rpc, block_number + 1).await; - // Check the input data of the quote - let expected_input_data = - QuoteInputData::new(TSS_ACCOUNTS[0].0, X25519_PUBLIC_KEYS[0], nonce, 0); - assert_eq!(quote.report_input_data(), expected_input_data.0); + // There should be no more pending attestation as the attestation has been handled + let pending_attestation_query = + entropy::storage().attestation().pending_attestations(&TSS_ACCOUNTS[0]); + if query_chain(&api, &rpc, pending_attestation_query, None).await.unwrap().is_none() { + return; + } + } + panic!("Waited 10 blocks and attestation is still pending"); } diff --git a/node/cli/src/chain_spec/dev.rs b/node/cli/src/chain_spec/dev.rs index 3b35079e9..44e29e38b 100644 --- a/node/cli/src/chain_spec/dev.rs +++ b/node/cli/src/chain_spec/dev.rs @@ -17,10 +17,10 @@ use crate::chain_spec::{get_account_id_from_seed, ChainSpec}; use crate::endowed_accounts::endowed_accounts_dev; use entropy_runtime::{ - constants::currency::*, wasm_binary_unwrap, AuthorityDiscoveryConfig, BabeConfig, - BalancesConfig, ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, MaxNominations, - ParametersConfig, ProgramsConfig, RegistryConfig, SessionConfig, StakerStatus, StakingConfig, - StakingExtensionConfig, SudoConfig, TechnicalCommitteeConfig, + constants::currency::*, wasm_binary_unwrap, AttestationConfig, AuthorityDiscoveryConfig, + BabeConfig, BalancesConfig, ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, + MaxNominations, ParametersConfig, ProgramsConfig, RegistryConfig, SessionConfig, StakerStatus, + StakingConfig, StakingExtensionConfig, SudoConfig, TechnicalCommitteeConfig, }; use entropy_runtime::{AccountId, Balance}; use entropy_shared::{ @@ -34,7 +34,7 @@ use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use sc_service::ChainType; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use sp_consensus_babe::AuthorityId as BabeId; -use sp_core::sr25519; +use sp_core::{sr25519, ByteArray}; use sp_runtime::{BoundedVec, Perbill}; pub fn devnet_three_node_initial_tss_servers( @@ -336,5 +336,9 @@ pub fn development_genesis_config( 10, )], }, + "attestation": AttestationConfig { + initial_attestation_requests: vec![(3, vec![crate::chain_spec::tss_account_id::ALICE.to_raw_vec()])], + initial_pending_attestations: vec![(crate::chain_spec::tss_account_id::ALICE.clone(), [0; 32])], + }, }) } diff --git a/node/cli/src/chain_spec/integration_tests.rs b/node/cli/src/chain_spec/integration_tests.rs index bd901dc72..a7d9b9fbe 100644 --- a/node/cli/src/chain_spec/integration_tests.rs +++ b/node/cli/src/chain_spec/integration_tests.rs @@ -17,10 +17,10 @@ use crate::chain_spec::{get_account_id_from_seed, ChainSpec}; use crate::endowed_accounts::endowed_accounts_dev; use entropy_runtime::{ - constants::currency::*, wasm_binary_unwrap, AuthorityDiscoveryConfig, BabeConfig, - BalancesConfig, ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, MaxNominations, - ParametersConfig, ProgramsConfig, RegistryConfig, SessionConfig, StakerStatus, StakingConfig, - StakingExtensionConfig, SudoConfig, TechnicalCommitteeConfig, + constants::currency::*, wasm_binary_unwrap, AttestationConfig, AuthorityDiscoveryConfig, + BabeConfig, BalancesConfig, ElectionsConfig, GrandpaConfig, ImOnlineConfig, IndicesConfig, + MaxNominations, ParametersConfig, ProgramsConfig, RegistryConfig, SessionConfig, StakerStatus, + StakingConfig, StakingExtensionConfig, SudoConfig, TechnicalCommitteeConfig, }; use entropy_runtime::{AccountId, Balance}; use entropy_shared::{ @@ -34,7 +34,7 @@ use pallet_im_online::sr25519::AuthorityId as ImOnlineId; use sc_service::ChainType; use sp_authority_discovery::AuthorityId as AuthorityDiscoveryId; use sp_consensus_babe::AuthorityId as BabeId; -use sp_core::sr25519; +use sp_core::{sr25519, ByteArray}; use sp_runtime::{BoundedVec, Perbill}; /// The configuration used for the Threshold Signature Scheme server integration tests. @@ -275,5 +275,9 @@ pub fn integration_tests_genesis_config( 10, )], }, + "attestation": AttestationConfig { + initial_attestation_requests: vec![(3, vec![crate::chain_spec::tss_account_id::ALICE.to_raw_vec()])], + initial_pending_attestations: vec![(crate::chain_spec::tss_account_id::ALICE.clone(), [0; 32])], + }, }) } diff --git a/node/cli/src/service.rs b/node/cli/src/service.rs index bf5a28bbd..72e7fac5b 100644 --- a/node/cli/src/service.rs +++ b/node/cli/src/service.rs @@ -372,6 +372,11 @@ pub fn new_full_base( b"reshare_validators", &format!("{}/validator/reshare", endpoint).into_bytes(), ); + offchain_db.local_storage_set( + sp_core::offchain::StorageKind::PERSISTENT, + b"attest", + &format!("{}/attest", endpoint).into_bytes(), + ); log::info!("Threshold Signing Sever (TSS) location changed to {}", endpoint); } } diff --git a/pallets/attestation/Cargo.toml b/pallets/attestation/Cargo.toml new file mode 100644 index 000000000..0b3b25065 --- /dev/null +++ b/pallets/attestation/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name ="pallet-attestation" +version ="0.2.0" +authors =['Entropy Cryptography '] +homepage ='https://entropy.xyz/' +license ='AGPL-3.0-or-later' +repository='https://github.com/entropyxyz/entropy-core' +edition ='2021' +publish =false + +[dependencies] +codec ={ package="parity-scale-codec", version="3.6.3", default-features=false, features=["derive"] } +scale-info ={ version="2.11", default-features=false, features=["derive"] } +log ={ version="0.4.22", default-features=false } +frame-support ={ version="29.0.0", default-features=false } +frame-system ={ version="29.0.0", default-features=false } +sp-io ={ version="31.0.0", default-features=false } +sp-core ={ version="29.0.0", default-features=false } +sp-runtime ={ version="32.0.0", default-features=false } +sp-staking ={ version="27.0.0", default-features=false } +frame-benchmarking={ version="29.0.0", default-features=false, optional=true } +sp-std ={ version="14.0.0", default-features=false } +pallet-session ={ version="29.0.0", default-features=false, optional=true } + +entropy-shared={ version="0.2.0", path="../../crates/shared", features=[ + "wasm-no-std", +], default-features=false } +pallet-staking-extension={ version="0.2.0", path="../staking", default-features=false } +tdx-quote={ git="https://github.com/entropyxyz/tdx-quote" } + +[dev-dependencies] +pallet-session ={ version="29.0.0", default-features=false } +pallet-staking ={ version="29.0.0", default-features=false } +pallet-balances ={ version="29.0.0", default-features=false } +pallet-bags-list ={ version="28.0.0", default-features=false } +pallet-timestamp ={ version="28.0.0", default-features=false } +sp-npos-elections ={ version="27.0.0", default-features=false } +frame-election-provider-support={ version="29.0.0", default-features=false } +pallet-staking-reward-curve ={ version="11.0.0" } +pallet-parameters ={ version="0.2.0", path="../parameters", default-features=false } +tdx-quote ={ git="https://github.com/entropyxyz/tdx-quote", features=["mock"] } +rand_core ="0.6.4" + +[features] +default=['std'] +runtime-benchmarks=['frame-benchmarking', 'tdx-quote/mock', 'pallet-session'] +std=[ + 'frame-benchmarking/std', + 'frame-support/std', + 'frame-system/std', + 'log/std', + 'pallet-staking-extension/std', + 'pallet-balances/std', + 'sp-io/std', + "sp-runtime/std", +] +try-runtime=['frame-support/try-runtime'] diff --git a/pallets/attestation/src/benchmarking.rs b/pallets/attestation/src/benchmarking.rs new file mode 100644 index 000000000..f3c88604f --- /dev/null +++ b/pallets/attestation/src/benchmarking.rs @@ -0,0 +1,78 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use entropy_shared::QuoteInputData; +use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, whitelisted_caller}; +use frame_system::{EventRecord, RawOrigin}; +use pallet_staking_extension::{ServerInfo, ThresholdServers, ThresholdToStash}; + +use super::*; +#[allow(unused)] +use crate::Pallet as AttestationPallet; + +// This is a randomly generated secret p256 ECDSA key +const ENCLAVE_SIGNING_KEY: [u8; 32] = [ + 167, 184, 203, 130, 240, 249, 191, 129, 206, 9, 200, 29, 99, 197, 64, 81, 135, 166, 59, 73, 31, + 27, 206, 207, 69, 248, 56, 195, 64, 92, 109, 46, +]; + +fn assert_last_event(generic_event: ::RuntimeEvent) { + let events = frame_system::Pallet::::events(); + let system_event: ::RuntimeEvent = generic_event.into(); + // compare to the last event record + let EventRecord { event, .. } = &events[events.len() - 1]; + assert_eq!(event, &system_event); +} + +benchmarks! { + attest { + let attestee: T::AccountId = whitelisted_caller(); + let nonce = [0; 32]; + + let signing_key = tdx_quote::SigningKey::from_bytes(&ENCLAVE_SIGNING_KEY.into()).unwrap(); + + let input_data = QuoteInputData::new( + &attestee, // TSS Account ID + [0; 32], // x25519 public key + nonce, + 1, // Block number + ); + let quote = tdx_quote::Quote::mock(signing_key.clone(), input_data.0).as_bytes().to_vec(); + + // Insert a pending attestation so that this quote is expected + >::insert(attestee.clone(), nonce); + + let stash_account = ::ValidatorId::try_from(attestee.clone()) + .or(Err(())) + .unwrap(); + + >::insert(attestee.clone(), stash_account.clone()); + >::insert(stash_account.clone(), ServerInfo { + tss_account: attestee.clone(), + x25519_public_key: [0; 32], + endpoint: b"http://localhost:3001".to_vec(), + }); + + }: _(RawOrigin::Signed(attestee.clone()), quote.clone()) + verify { + assert_last_event::( + Event::::AttestationMade.into() + ); + // Check that there is no longer a pending attestation + assert!(!>::contains_key(attestee)); + } +} + +impl_benchmark_test_suite!(AttestationPallet, crate::mock::new_test_ext(), crate::mock::Test); diff --git a/pallets/attestation/src/lib.rs b/pallets/attestation/src/lib.rs new file mode 100644 index 000000000..e98d315ff --- /dev/null +++ b/pallets/attestation/src/lib.rs @@ -0,0 +1,186 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! # Attestation Pallet +//! +//! This handles attestations that the TS servers are running on TDX hardware and that the binary +//! from our release is correctly loaded. +//! +//! It stores the nonces of all pending (requested) attestations, storing them under the associated +//! TSS account ID. So there may be at most one pending attestation per TS server. The nonce is just +//! a random 32 bytes, which is included in the input data to the TDX quote, to prove that this is +//! a freshly made quote. +//! +//! An attestation request is responded to by submitting the quote using the attest extrinsic. If +//! there was a pending attestation for the caller, the quote is verified. Verification currently +//! just means checking that the quote parses correctly and has a valid signature. +//! +//! It also stores a mapping of block number to TSS account IDs of nodes for who an attestation +//! request should be initiated. This is used by the propagation pallet to make a POST request to +//! the TS server's /attest endpoint whenever there are requests to be made. + +#![cfg_attr(not(feature = "std"), no_std)] +pub use pallet::*; + +#[cfg(feature = "runtime-benchmarks")] +pub mod benchmarking; + +pub mod weights; + +#[cfg(test)] +mod mock; + +#[cfg(test)] +mod tests; + +#[frame_support::pallet] +pub mod pallet { + use entropy_shared::QuoteInputData; + use frame_support::pallet_prelude::*; + use frame_system::pallet_prelude::*; + use sp_std::vec::Vec; + use tdx_quote::Quote; + + pub use crate::weights::WeightInfo; + + /// A nonce included as input for a TDX quote + type Nonce = [u8; 32]; + + #[pallet::pallet] + #[pallet::without_storage_info] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + pallet_staking_extension::Config { + /// The overarching event type. + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + /// Describes the weights of the dispatchables exposed by this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::genesis_config] + #[derive(frame_support::DefaultNoBound)] + pub struct GenesisConfig { + pub initial_pending_attestations: Vec<(T::AccountId, [u8; 32])>, + pub initial_attestation_requests: Vec<(BlockNumberFor, Vec>)>, + } + + #[pallet::genesis_build] + impl BuildGenesisConfig for GenesisConfig { + fn build(&self) { + for (account_id, nonce) in &self.initial_pending_attestations { + PendingAttestations::::insert(account_id, nonce); + } + for (block_number, account_ids) in &self.initial_attestation_requests { + AttestationRequests::::insert(block_number, account_ids); + } + } + } + + /// A map of TSS account id to quote nonce for pending attestations + #[pallet::storage] + #[pallet::getter(fn pending_attestations)] + pub type PendingAttestations = + StorageMap<_, Blake2_128Concat, T::AccountId, Nonce, OptionQuery>; + + /// A mapping between block numbers and TSS nodes for who we want to make a request for + /// attestation, used to make attestation requests via an offchain worker + #[pallet::storage] + #[pallet::getter(fn attestation_requests)] + pub type AttestationRequests = + StorageMap<_, Blake2_128Concat, BlockNumberFor, Vec>, OptionQuery>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + AttestationMade, + } + + /// Errors related to the attestation pallet + #[pallet::error] + pub enum Error { + /// Quote could not be parsed or verified + BadQuote, + /// Attestation extrinsic submitted when not requested + UnexpectedAttestation, + /// Hashed input data does not match what was expected + IncorrectInputData, + /// Cannot lookup associated stash account + NoStashAccount, + /// Cannot lookup associated TS server info + NoServerInfo, + } + + #[pallet::call] + impl Pallet { + /// A TDX quote given in response to an attestation request. + /// The quote format is specified in: + /// https://download.01.org/intel-sgx/latest/dcap-latest/linux/docs/Intel_TDX_DCAP_Quoting_Library_API.pdf + #[pallet::call_index(0)] + #[pallet::weight({ + ::WeightInfo::attest() + })] + pub fn attest(origin: OriginFor, quote: Vec) -> DispatchResult { + let who = ensure_signed(origin)?; + + // Check that we were expecting a quote from this validator by getting the associated + // nonce from PendingAttestations. + let nonce = + PendingAttestations::::get(&who).ok_or(Error::::UnexpectedAttestation)?; + + // Parse the quote (which internally verifies the signature) + let quote = Quote::from_bytes("e).map_err(|_| Error::::BadQuote)?; + + // Get associated x25519 public key from staking pallet + let x25519_public_key = { + let stash_account = pallet_staking_extension::Pallet::::threshold_to_stash(&who) + .ok_or(Error::::NoStashAccount)?; + let server_info = + pallet_staking_extension::Pallet::::threshold_server(&stash_account) + .ok_or(Error::::NoServerInfo)?; + server_info.x25519_public_key + }; + + // Get current block number + let block_number: u32 = { + let block_number = >::block_number(); + BlockNumberFor::::try_into(block_number).unwrap_or_default() + }; + + // Check report input data matches the nonce, TSS details and block number + let expected_input_data = + QuoteInputData::new(&who, x25519_public_key, nonce, block_number); + ensure!( + quote.report_input_data() == expected_input_data.0, + Error::::IncorrectInputData + ); + + // TODO #982 Check measurements match current release of entropy-tss + let _mrtd = quote.mrtd(); + + // TODO #982 Check that the attestation public key matches that from PCK certificate + let _attestation_key = quote.attestation_key; + + // Remove the entry from PendingAttestations + PendingAttestations::::remove(&who); + + // TODO #982 If anything fails, don't just return an error - do something mean + + Self::deposit_event(Event::AttestationMade); + + Ok(()) + } + } +} diff --git a/pallets/attestation/src/mock.rs b/pallets/attestation/src/mock.rs new file mode 100644 index 000000000..fea3719bc --- /dev/null +++ b/pallets/attestation/src/mock.rs @@ -0,0 +1,356 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use frame_election_provider_support::{ + bounds::{ElectionBounds, ElectionBoundsBuilder}, + onchain, SequentialPhragmen, VoteWeight, +}; +use frame_support::{ + derive_impl, parameter_types, + traits::{ConstU32, OneSessionHandler, Randomness}, +}; +use frame_system as system; +use frame_system::EnsureRoot; +use pallet_session::historical as pallet_session_historical; +use sp_core::H256; +use sp_runtime::{ + curve::PiecewiseLinear, + testing::{TestXt, UintAuthorityId}, + traits::{BlakeTwo256, ConvertInto, IdentityLookup}, + BuildStorage, Perbill, +}; +use sp_staking::{EraIndex, SessionIndex}; +use std::cell::RefCell; + +use crate as pallet_attestation; + +const NULL_ARR: [u8; 32] = [0; 32]; + +type Block = frame_system::mocking::MockBlock; +type BlockNumber = u64; +type AccountId = u64; +type Balance = u64; + +// Configure a mock runtime to test the pallet. +frame_support::construct_runtime!( + pub enum Test + { + Attestation: pallet_attestation, + System: frame_system, + Balances: pallet_balances, + Timestamp: pallet_timestamp, + Staking: pallet_staking_extension, + FrameStaking: pallet_staking, + Session: pallet_session, + Historical: pallet_session_historical, + BagsList: pallet_bags_list, + Parameters: pallet_parameters, + } +); + +impl pallet_attestation::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 42; +} + +#[derive_impl(frame_system::config_preludes::TestDefaultConfig as frame_system::DefaultConfig)] +impl system::Config for Test { + type AccountData = pallet_balances::AccountData; + type AccountId = u64; + type BaseCallFilter = frame_support::traits::Everything; + type Block = Block; + type BlockHashCount = BlockHashCount; + type BlockLength = (); + type BlockWeights = (); + type DbWeight = (); + type Hash = H256; + type Hashing = BlakeTwo256; + type Lookup = IdentityLookup; + type MaxConsumers = frame_support::traits::ConstU32<16>; + type Nonce = u64; + type OnKilledAccount = (); + type OnNewAccount = (); + type OnSetCode = (); + type PalletInfo = PalletInfo; + type RuntimeCall = RuntimeCall; + type RuntimeEvent = RuntimeEvent; + type RuntimeOrigin = RuntimeOrigin; + type SS58Prefix = SS58Prefix; + type SystemWeightInfo = (); + type Version = (); +} + +parameter_types! { + pub const MinimumPeriod: u64 = 3; +} + +impl pallet_timestamp::Config for Test { + type MinimumPeriod = MinimumPeriod; + type Moment = u64; + type OnTimestampSet = (); + type WeightInfo = (); +} + +parameter_types! { + pub const ExistentialDeposit: Balance = 10; + pub const MaxLocks: u32 = 5; +} +impl pallet_balances::Config for Test { + type AccountStore = System; + type Balance = Balance; + type DustRemoval = (); + type ExistentialDeposit = ExistentialDeposit; + type FreezeIdentifier = (); + type MaxFreezes = (); + + type MaxLocks = MaxLocks; + type MaxReserves = (); + type ReserveIdentifier = [u8; 8]; + type RuntimeEvent = RuntimeEvent; + type RuntimeHoldReason = RuntimeHoldReason; + type RuntimeFreezeReason = RuntimeFreezeReason; + type WeightInfo = (); +} + +pub struct OtherSessionHandler; +impl OneSessionHandler for OtherSessionHandler { + type Key = UintAuthorityId; + + fn on_genesis_session<'a, I>(_: I) + where + I: Iterator + 'a, + AccountId: 'a, + { + } + + fn on_new_session<'a, I>(_: bool, _: I, _: I) + where + I: Iterator + 'a, + AccountId: 'a, + { + } + + fn on_disabled(_validator_index: u32) {} +} + +impl sp_runtime::BoundToRuntimeAppPublic for OtherSessionHandler { + type Public = UintAuthorityId; +} + +parameter_types! { + pub const Period: BlockNumber = 5; + pub const Offset: BlockNumber = 0; +} + +sp_runtime::impl_opaque_keys! { + pub struct SessionKeys { + pub other: OtherSessionHandler, + } +} + +parameter_types! { + pub static ElectionsBounds: ElectionBounds = ElectionBoundsBuilder::default().build(); +} + +pub struct OnChainSeqPhragmen; +impl onchain::Config for OnChainSeqPhragmen { + type DataProvider = FrameStaking; + type MaxWinners = ConstU32<100>; + type Solver = SequentialPhragmen; + type System = Test; + type Bounds = ElectionsBounds; + type WeightInfo = (); +} + +pallet_staking_reward_curve::build! { + const REWARD_CURVE: PiecewiseLinear<'static> = curve!( + min_inflation: 0_025_000u64, + max_inflation: 0_100_000, + ideal_stake: 0_500_000, + falloff: 0_050_000, + max_piece_count: 40, + test_precision: 0_005_000, + ); +} +parameter_types! { + pub const RewardCurve: &'static sp_runtime::curve::PiecewiseLinear<'static> = &REWARD_CURVE; + + pub const MaxKeys: u32 = 10_000; + pub const MaxPeerInHeartbeats: u32 = 10_000; + pub const MaxPeerDataEncodingSize: u32 = 1_000; +} + +impl frame_system::offchain::SendTransactionTypes for Test +where + RuntimeCall: From, +{ + type Extrinsic = TestXt; + type OverarchingCall = RuntimeCall; +} + +const THRESHOLDS: [sp_npos_elections::VoteWeight; 9] = + [10, 20, 30, 40, 50, 60, 1_000, 2_000, 10_000]; + +parameter_types! { + pub static BagThresholds: &'static [sp_npos_elections::VoteWeight] = &THRESHOLDS; +} + +impl pallet_bags_list::Config for Test { + type BagThresholds = BagThresholds; + type RuntimeEvent = RuntimeEvent; + type Score = VoteWeight; + type ScoreProvider = FrameStaking; + type WeightInfo = (); +} + +parameter_types! { + pub const SessionsPerEra: SessionIndex = 2; + pub const BondingDuration: EraIndex = 0; + pub const SlashDeferDuration: EraIndex = 0; + pub const AttestationPeriod: u64 = 100; + pub const ElectionLookahead: u64 = 0; + pub const StakingUnsignedPriority: u64 = u64::MAX / 2; + pub const OffendingValidatorsThreshold: Perbill = Perbill::from_percent(17); +} + +pub struct StakingBenchmarkingConfig; +impl pallet_staking::BenchmarkingConfig for StakingBenchmarkingConfig { + type MaxNominators = ConstU32<1000>; + type MaxValidators = ConstU32<1000>; +} + +impl pallet_staking::Config for Test { + type AdminOrigin = frame_system::EnsureRoot; + type BenchmarkingConfig = StakingBenchmarkingConfig; + type BondingDuration = BondingDuration; + type Currency = Balances; + type CurrencyBalance = Balance; + type CurrencyToVote = (); + type ElectionProvider = onchain::OnChainExecution; + type EraPayout = pallet_staking::ConvertCurve; + type EventListeners = (); + type GenesisElectionProvider = Self::ElectionProvider; + type HistoryDepth = ConstU32<84>; + type MaxExposurePageSize = ConstU32<64>; + type MaxControllersInDeprecationBatch = ConstU32<100>; + type MaxUnlockingChunks = ConstU32<32>; + type NextNewSession = Session; + type NominationsQuota = pallet_staking::FixedNominationsQuota<16>; + type OffendingValidatorsThreshold = OffendingValidatorsThreshold; + type Reward = (); + type RewardRemainder = (); + type RuntimeEvent = RuntimeEvent; + type SessionInterface = Self; + type SessionsPerEra = SessionsPerEra; + type Slash = (); + type SlashDeferDuration = SlashDeferDuration; + type TargetList = pallet_staking::UseValidatorsMap; + type UnixTime = pallet_timestamp::Pallet; + type VoterList = BagsList; + type WeightInfo = (); +} + +impl pallet_session::Config for Test { + type Keys = UintAuthorityId; + type NextSessionRotation = pallet_session::PeriodicSessions; + type RuntimeEvent = RuntimeEvent; + type SessionHandler = (OtherSessionHandler,); + type SessionManager = pallet_session::historical::NoteHistoricalRoot; + type ShouldEndSession = pallet_session::PeriodicSessions; + type ValidatorId = AccountId; + type ValidatorIdOf = ConvertInto; + type WeightInfo = (); +} + +impl pallet_session::historical::Config for Test { + type FullIdentification = pallet_staking::Exposure; + type FullIdentificationOf = pallet_staking::ExposureOf; +} + +thread_local! { + pub static LAST_RANDOM: RefCell> = RefCell::new(None); +} + +pub struct TestPastRandomness; +impl Randomness for TestPastRandomness { + fn random(_subject: &[u8]) -> (H256, u64) { + LAST_RANDOM.with(|p| { + if let Some((output, known_since)) = &*p.borrow() { + (*output, *known_since) + } else { + (H256::zero(), frame_system::Pallet::::block_number()) + } + }) + } +} +parameter_types! { + pub const MaxEndpointLength: u32 = 3; +} +impl pallet_staking_extension::Config for Test { + type Currency = Balances; + type MaxEndpointLength = MaxEndpointLength; + type Randomness = TestPastRandomness; + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + +parameter_types! { + pub const UncleGenerations: u64 = 0; +} + +parameter_types! { + pub const MaxProgramHashes: u32 = 5u32; + pub const KeyVersionNumber: u8 = 1; +} + +parameter_types! { + pub const MaxBytecodeLength: u32 = 3; + pub const ProgramDepositPerByte: u32 = 5; + pub const MaxOwnedPrograms: u32 = 5; +} + +impl pallet_parameters::Config for Test { + type RuntimeEvent = RuntimeEvent; + type UpdateOrigin = EnsureRoot; + type WeightInfo = (); +} + +// Build genesis storage according to the mock runtime. +pub fn new_test_ext() -> sp_io::TestExternalities { + let mut t = system::GenesisConfig::::default().build_storage().unwrap(); + + let pallet_attestation = pallet_attestation::GenesisConfig:: { + initial_pending_attestations: vec![(0, NULL_ARR)], + initial_attestation_requests: Vec::new(), + }; + pallet_attestation.assimilate_storage(&mut t).unwrap(); + + let pallet_staking_extension = pallet_staking_extension::GenesisConfig:: { + threshold_servers: vec![ + // (ValidatorID, (AccountId, X25519PublicKey, TssServerURL)) + (5, (0, NULL_ARR, vec![20])), + ], + proactive_refresh_data: (vec![], vec![]), + mock_signer_rotate: (false, vec![], vec![]), + }; + pallet_staking_extension.assimilate_storage(&mut t).unwrap(); + + t.into() +} diff --git a/pallets/attestation/src/tests.rs b/pallets/attestation/src/tests.rs new file mode 100644 index 000000000..7e34966e2 --- /dev/null +++ b/pallets/attestation/src/tests.rs @@ -0,0 +1,45 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +use crate::mock::*; +use entropy_shared::QuoteInputData; +use frame_support::assert_ok; +use rand_core::OsRng; + +const ATTESTEE: u64 = 0; + +#[test] +fn attest() { + new_test_ext().execute_with(|| { + // We start with an existing pending attestation at genesis - get it's nonce + let nonce = Attestation::pending_attestations(ATTESTEE).unwrap(); + assert_eq!(nonce, [0; 32]); + + // For now it doesn't matter what this is, but once we handle PCK certificates this will + // need to correspond to the public key in the certificate + let signing_key = tdx_quote::SigningKey::random(&mut OsRng); + + let input_data = QuoteInputData::new( + ATTESTEE, // TSS Account ID + [0; 32], // x25519 public key + nonce, 0, // Block number + ); + + let quote = tdx_quote::Quote::mock(signing_key.clone(), input_data.0); + assert_ok!( + Attestation::attest(RuntimeOrigin::signed(ATTESTEE), quote.as_bytes().to_vec(),) + ); + }) +} diff --git a/pallets/attestation/src/weights.rs b/pallets/attestation/src/weights.rs new file mode 100644 index 000000000..3c8ef6e18 --- /dev/null +++ b/pallets/attestation/src/weights.rs @@ -0,0 +1,87 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Autogenerated weights for `pallet_attestation` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 33.0.0 +//! DATE: 2024-08-15, STEPS: `5`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `turnip`, CPU: `Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/entropy +// benchmark +// pallet +// --chain +// dev +// --pallet=pallet_attestation +// --extrinsic=* +// --steps=5 +// --repeat=2 +// --header=.maintain/AGPL-3.0-header.txt +// --output=./runtime/src/weights/ + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; + +pub trait WeightInfo { + fn attest() -> Weight; +} + +/// Weight functions for `pallet_attestation`. +pub struct SubstrateWeightInfo(PhantomData); +impl WeightInfo for SubstrateWeightInfo { + /// Storage: `Attestation::PendingAttestations` (r:1 w:1) + /// Proof: `Attestation::PendingAttestations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdToStash` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdToStash` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdServers` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdServers` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn attest() -> Weight { + // Proof Size summary in bytes: + // Measured: `661` + // Estimated: `4126` + // Minimum execution time: 2_573_915_000 picoseconds. + Weight::from_parts(2_582_997_000, 0) + .saturating_add(Weight::from_parts(0, 4126)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } +} + +impl WeightInfo for () { + /// Storage: `Attestation::PendingAttestations` (r:1 w:1) + /// Proof: `Attestation::PendingAttestations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdToStash` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdToStash` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdServers` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdServers` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn attest() -> Weight { + // Proof Size summary in bytes: + // Measured: `661` + // Estimated: `4126` + // Minimum execution time: 2_573_915_000 picoseconds. + Weight::from_parts(2_582_997_000, 0) + .saturating_add(Weight::from_parts(0, 4126)) + .saturating_add(RocksDbWeight::get().reads(3)) + .saturating_add(RocksDbWeight::get().writes(1)) + } +} diff --git a/pallets/programs/src/benchmarking.rs b/pallets/programs/src/benchmarking.rs index 9ec64645a..9644bb718 100644 --- a/pallets/programs/src/benchmarking.rs +++ b/pallets/programs/src/benchmarking.rs @@ -13,7 +13,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . -//! Benchmarking setup for pallet-propgation +//! Benchmarking setup for pallet-programs use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, whitelisted_caller}; use frame_support::{ diff --git a/pallets/propagation/Cargo.toml b/pallets/propagation/Cargo.toml index f9e688e23..082e106f0 100644 --- a/pallets/propagation/Cargo.toml +++ b/pallets/propagation/Cargo.toml @@ -32,6 +32,7 @@ entropy-shared={ version="0.2.0", path="../../crates/shared", default-features=f pallet-registry={ version="0.2.0", path="../registry", default-features=false } pallet-programs={ version="0.2.0", path="../programs", default-features=false } pallet-staking-extension={ version="0.2.0", path="../staking", default-features=false } +pallet-attestation={ version="0.2.0", path="../attestation", default-features=false } [dev-dependencies] parking_lot="0.12.3" @@ -68,5 +69,6 @@ std=[ 'pallet-programs/std', 'pallet-registry/std', 'pallet-staking-extension/std', + 'pallet-attestation/std', ] try-runtime=['frame-support/try-runtime'] diff --git a/pallets/propagation/src/lib.rs b/pallets/propagation/src/lib.rs index dcfb9f53d..31424c5bf 100644 --- a/pallets/propagation/src/lib.rs +++ b/pallets/propagation/src/lib.rs @@ -14,11 +14,11 @@ // along with this program. If not, see . #![cfg_attr(not(feature = "std"), no_std)] -//! # Propogation Pallet +//! # Propagation Pallet //! //! ## Overview //! -//! Propgates messages to signing client through offchain worker +//! Propagates messages to signing client through offchain worker pub use pallet::*; #[cfg(test)] @@ -37,7 +37,8 @@ pub mod pallet { pub use crate::weights::WeightInfo; use codec::Encode; use entropy_shared::{ - OcwMessageDkg, OcwMessageProactiveRefresh, OcwMessageReshare, ValidatorInfo, + OcwMessageAttestationRequest, OcwMessageDkg, OcwMessageProactiveRefresh, OcwMessageReshare, + ValidatorInfo, }; use frame_support::{pallet_prelude::*, sp_runtime::traits::Saturating}; use frame_system::pallet_prelude::*; @@ -53,6 +54,7 @@ pub mod pallet { + pallet_authorship::Config + pallet_registry::Config + pallet_staking_extension::Config + + pallet_attestation::Config { type RuntimeEvent: From> + IsType<::RuntimeEvent>; /// The weight information of this pallet. @@ -69,6 +71,7 @@ pub mod pallet { let _ = Self::post_reshare(block_number); let _ = Self::post_user_registration(block_number); let _ = Self::post_proactive_refresh(block_number); + let _ = Self::post_attestation_request(block_number); } fn on_initialize(block_number: BlockNumberFor) -> Weight { @@ -92,6 +95,9 @@ pub mod pallet { /// Proactive Refresh Message passed to validators /// parameters. [OcwMessageReshare] KeyReshareMessagePassed(OcwMessageReshare), + + /// Attestations request message passed + AttestationRequestMessagePassed(OcwMessageAttestationRequest), } #[pallet::call] @@ -312,5 +318,49 @@ pub mod pallet { Ok(()) } + + /// Submits a request for a TDX attestation. + pub fn post_attestation_request( + block_number: BlockNumberFor, + ) -> Result<(), http::Error> { + if let Some(attestations_to_request) = + pallet_attestation::Pallet::::attestation_requests(block_number) + { + if attestations_to_request.is_empty() { + return Ok(()); + } + + let deadline = sp_io::offchain::timestamp().add(Duration::from_millis(2_000)); + let kind = sp_core::offchain::StorageKind::PERSISTENT; + let from_local = sp_io::offchain::local_storage_get(kind, b"attest") + .unwrap_or_else(|| b"http://localhost:3001/attest".to_vec()); + let url = str::from_utf8(&from_local).unwrap_or("http://localhost:3001/attest"); + + let req_body = OcwMessageAttestationRequest { + tss_account_ids: attestations_to_request + .into_iter() + .filter_map(|v| v.try_into().ok()) + .collect(), + }; + log::debug!("propagation::post attestation: {:?}", &[req_body.encode()]); + + let pending = http::Request::post(url, vec![req_body.encode()]) + .deadline(deadline) + .send() + .map_err(|_| http::Error::IoError)?; + + let response = + pending.try_wait(deadline).map_err(|_| http::Error::DeadlineReached)??; + + if response.code != 200 { + log::warn!("Unexpected status code: {}", response.code); + return Err(http::Error::Unknown); + } + let _res_body = response.body().collect::>(); + + Self::deposit_event(Event::AttestationRequestMessagePassed(req_body)); + }; + Ok(()) + } } } diff --git a/pallets/propagation/src/mock.rs b/pallets/propagation/src/mock.rs index f0f983106..a87be8a69 100644 --- a/pallets/propagation/src/mock.rs +++ b/pallets/propagation/src/mock.rs @@ -59,6 +59,7 @@ frame_support::construct_runtime!( Historical: pallet_session_historical, BagsList: pallet_bags_list, Parameters: pallet_parameters, + Attestation: pallet_attestation, } ); @@ -368,6 +369,11 @@ impl pallet_parameters::Config for Test { type WeightInfo = (); } +impl pallet_attestation::Config for Test { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = (); +} + // Build genesis storage according to the mock runtime. pub fn new_test_ext() -> sp_io::TestExternalities { let mut t = system::GenesisConfig::::default().build_storage().unwrap(); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 9ba99f073..db503f21b 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -102,6 +102,7 @@ pallet-slashing ={ version='0.2.0', path='../pallets/slashing', default- pallet-staking-extension={ version='0.2.0', path='../pallets/staking', default-features=false } pallet-transaction-pause={ version='0.2.0', path='../pallets/transaction-pause', default-features=false } pallet-parameters ={ version='0.2.0', path='../pallets/parameters', default-features=false } +pallet-attestation ={ version='0.2.0', path='../pallets/attestation', default-features=false } pallet-oracle ={ version='0.2.0-rc.1', path='../pallets/oracle', default-features=false } entropy-shared={ version="0.2.0", path="../crates/shared", default-features=false, features=[ @@ -123,6 +124,7 @@ std=[ "frame-system/std", "frame-try-runtime/std", "log/std", + "pallet-attestation/std", "pallet-authority-discovery/std", "pallet-authorship/std", "pallet-babe/std", @@ -191,6 +193,7 @@ runtime-benchmarks=[ "frame-system-benchmarking/runtime-benchmarks", "frame-system/runtime-benchmarks", "hex-literal", + "pallet-attestation/runtime-benchmarks", "pallet-babe/runtime-benchmarks", "pallet-bags-list/runtime-benchmarks", "pallet-balances/runtime-benchmarks", diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index 779176a59..7a9f82bc3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -1494,6 +1494,11 @@ impl pallet_parameters::Config for Runtime { type WeightInfo = weights::pallet_parameters::WeightInfo; } +impl pallet_attestation::Config for Runtime { + type RuntimeEvent = RuntimeEvent; + type WeightInfo = weights::pallet_attestation::WeightInfo; +} + parameter_types! { pub const MaxOracleKeyLength: u32 = 100; pub const MaxOracleValueLength: u32 = 100; @@ -1559,6 +1564,7 @@ construct_runtime!( Propagation: pallet_propagation = 55, Parameters: pallet_parameters = 56, Oracle: pallet_oracle = 57, + Attestation: pallet_attestation = 58, } ); @@ -1616,6 +1622,7 @@ extern crate frame_benchmarking; mod benches { define_benchmarks!( [frame_benchmarking, BaselineBench::] + [pallet_attestation, Attestation] [pallet_babe, Babe] [pallet_bags_list, BagsList] [pallet_balances, Balances] diff --git a/runtime/src/weights/mod.rs b/runtime/src/weights/mod.rs index f260ccb1b..e88b496aa 100644 --- a/runtime/src/weights/mod.rs +++ b/runtime/src/weights/mod.rs @@ -30,6 +30,7 @@ pub mod frame_election_provider_support; pub mod frame_system; +pub mod pallet_attestation; pub mod pallet_bags_list; pub mod pallet_balances; pub mod pallet_bounties; diff --git a/runtime/src/weights/pallet_attestation.rs b/runtime/src/weights/pallet_attestation.rs new file mode 100644 index 000000000..57381723d --- /dev/null +++ b/runtime/src/weights/pallet_attestation.rs @@ -0,0 +1,64 @@ +// Copyright (C) 2023 Entropy Cryptography Inc. +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program 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 Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +//! Autogenerated weights for `pallet_attestation` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 33.0.0 +//! DATE: 2024-08-15, STEPS: `5`, REPEAT: `2`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `turnip`, CPU: `Intel(R) Core(TM) i7-4710MQ CPU @ 2.50GHz` +//! WASM-EXECUTION: `Compiled`, CHAIN: `Some("dev")`, DB CACHE: 1024 + +// Executed Command: +// ./target/release/entropy +// benchmark +// pallet +// --chain +// dev +// --pallet=pallet_attestation +// --extrinsic=* +// --steps=5 +// --repeat=2 +// --header=.maintain/AGPL-3.0-header.txt +// --output=./runtime/src/weights/ + +#![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_attestation`. +pub struct WeightInfo(PhantomData); +impl pallet_attestation::WeightInfo for WeightInfo { + /// Storage: `Attestation::PendingAttestations` (r:1 w:1) + /// Proof: `Attestation::PendingAttestations` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdToStash` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdToStash` (`max_values`: None, `max_size`: None, mode: `Measured`) + /// Storage: `StakingExtension::ThresholdServers` (r:1 w:0) + /// Proof: `StakingExtension::ThresholdServers` (`max_values`: None, `max_size`: None, mode: `Measured`) + fn attest() -> Weight { + // Proof Size summary in bytes: + // Measured: `661` + // Estimated: `4126` + // Minimum execution time: 2_573_915_000 picoseconds. + Weight::from_parts(2_582_997_000, 0) + .saturating_add(Weight::from_parts(0, 4126)) + .saturating_add(T::DbWeight::get().reads(3)) + .saturating_add(T::DbWeight::get().writes(1)) + } +}