Skip to content

Commit

Permalink
Attestation pallet (#1003)
Browse files Browse the repository at this point in the history
* WIP attestation pallet

* Improve checks for attest extrinsic

* Give constant weight just to get it to compile (with warning)

* Attempt to write mock runtime

* Write trivial test to show mock setup works

* Clean attestation pallet (#1006)

* fmt

* Make an attestation in the test

* Lockfile

* Taplo

* Comments

* Make quote input constructor generic, rm authorship pallet

* Taplo

* Add benchmarks

* Taplo

* Add pallet to chain runtime

* Rm unused import

* Improve benchmark

* Generate weights

* Include weights in runtime

* Update metadata

* Submit the attest extrinsic to test the attestation pallet

* Fixes for submitting extrinsic

* Update attestation endpoint in preparation for using the propagation pallet to call it

* Propagation pallet propagates attestation requests

* Decode input from propagation pallet

* Add initial attestation requests to genesis config

* Add initial attestation requests to chainspec

* Pull chain metadata

* Make mock attestation request at block 3

* Fix test

* Tidy

* Update crates/threshold-signature-server/src/attestation/api.rs

Co-authored-by: Hernando Castano <[email protected]>

* Extra check in attestation pallet benchmarks

* Improve error message for unexpected attestation

* Use actual weight

* Add endpoint to node service

* Comments and minor changed following review

* Changelog

---------

Co-authored-by: JesseAbram <[email protected]>
Co-authored-by: Hernando Castano <[email protected]>
  • Loading branch information
3 people authored Aug 21, 2024
1 parent bba31a3 commit 305c4be
Show file tree
Hide file tree
Showing 24 changed files with 1,121 additions and 65 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
32 changes: 32 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Binary file modified crates/client/entropy_metadata.scale
Binary file not shown.
15 changes: 12 additions & 3 deletions crates/shared/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,15 @@ pub struct OcwMessageProactiveRefresh {
pub proactive_refresh_keys: Vec<Vec<u8>>,
}

/// 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))]
Expand All @@ -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<T: Encode>(
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());
Expand Down
93 changes: 67 additions & 26 deletions crates/threshold-signature-server/src/attestation/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,57 +13,98 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

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<AppState>,
_input: Bytes,
) -> Result<StatusCode, AttestationErr> {
// 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<AppState>,
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<StatusCode, AttestationErr> {
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<EntropyConfig, sp_core::sr25519::Pair>,
x25519_secret: &StaticSecret,
) -> Result<Vec<u8>, 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<EntropyConfig, sp_core::sr25519::Pair>,
_x25519_secret: &StaticSecret,
) -> Result<Vec<u8>, AttestationErr> {
// Non-mock attestation (the real thing) will go here
Err(AttestationErr::NotImplemented)
}
7 changes: 6 additions & 1 deletion crates/threshold-signature-server/src/attestation/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
55 changes: 34 additions & 21 deletions crates/threshold-signature-server/src/attestation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
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;

Expand All @@ -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(&quote).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");
}
14 changes: 9 additions & 5 deletions node/cli/src/chain_spec/dev.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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(
Expand Down Expand Up @@ -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])],
},
})
}
14 changes: 9 additions & 5 deletions node/cli/src/chain_spec/integration_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand All @@ -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.
Expand Down Expand Up @@ -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])],
},
})
}
5 changes: 5 additions & 0 deletions node/cli/src/service.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Expand Down
Loading

0 comments on commit 305c4be

Please sign in to comment.