Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change attestation flow to be pull based #1109

Merged
merged 40 commits into from
Oct 18, 2024
Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
cd442be
Add trait for verifying attestations
HCastano Oct 10, 2024
7e64302
Clean up some of the Staking Config comments
HCastano Oct 10, 2024
0dae766
Use `AttestationHandler` in Staking pallet
HCastano Oct 10, 2024
f9c80f5
Add `Attestation::request_attestation()` extrinsic
HCastano Oct 10, 2024
b69d97e
Check that there's only one attestation request per account
HCastano Oct 10, 2024
ce9faf8
Write to `Threshold*` data structures in `validate()`
HCastano Oct 10, 2024
293496e
Allow mock attestation handler to fail
HCastano Oct 10, 2024
4912d0a
Add convenience implementation for `AttestationHandler
HCastano Oct 10, 2024
cee9776
Do some cleanup in Attestation pallet
HCastano Oct 10, 2024
7a172e4
For `validate` tests don't use the `ValidationQueue`
HCastano Oct 10, 2024
6c4a582
Input expected keys into quote verification method directly
HCastano Oct 11, 2024
8cd7f2c
Add test for checking failed quote verification
HCastano Oct 11, 2024
a742f74
Add some docs explaining how the quote request system works
HCastano Oct 11, 2024
6c07c18
Remove `AttestationQueue`, `KeyProvider`, and `ValidationQueue`
HCastano Oct 11, 2024
bf987c4
Clean up some of the staking tests
HCastano Oct 11, 2024
c1689a4
Add more relevant event after `validate()` is successful
HCastano Oct 11, 2024
54ce8c5
Update runtime implementation
HCastano Oct 11, 2024
7d07b1a
Bump metadata
HCastano Oct 11, 2024
1f83ea4
Ignore the TSS attestation tests
HCastano Oct 11, 2024
d5f2563
Feature gate `AttestationHandler`
HCastano Oct 11, 2024
3323a42
Missed a few `AttestationHandler` mock implementations
HCastano Oct 11, 2024
0d40754
Remove unused associated types
HCastano Oct 11, 2024
25aec6e
Get rid of `on_initialize()` benchmarks
HCastano Oct 11, 2024
bd27fbb
Remove `MaxPendingAttestations` associated type
HCastano Oct 11, 2024
7774d40
Add method for requesting quotes to `AttestationHandler`
HCastano Oct 16, 2024
2af32aa
Use threshold account when verifying quote
HCastano Oct 16, 2024
594ad42
Get `validate()` bench working again
HCastano Oct 16, 2024
9844c94
Add `nonce` to `request_quote()`
HCastano Oct 16, 2024
52768aa
Accept more quote types in Staking mock
HCastano Oct 16, 2024
4b2afcc
Tidy up `validate()` bench
HCastano Oct 16, 2024
12a139b
Strip down bench for `attest` and add one for `request_attestation`
HCastano Oct 16, 2024
e3171be
Use better dummy endpoint in `validate()` bench
HCastano Oct 16, 2024
485b42b
RustFmt
HCastano Oct 16, 2024
9561e97
Remove Attestation pallet from Staking mock
HCastano Oct 16, 2024
00d4ee2
Remove TODO
HCastano Oct 16, 2024
6f82b3c
Add `CHANGELOG` entry
HCastano Oct 16, 2024
bbd6309
Remove another TODO, this should become an issue
HCastano Oct 16, 2024
5abb84d
TaploFmt
HCastano Oct 16, 2024
964c1fa
Use published version of `tdx-quote`
HCastano Oct 18, 2024
db63ad2
Mention one more config type added in `CHANGELOG`
HCastano Oct 18, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,13 @@ At the moment this project **does not** adhere to

### Breaking Changes
- In [#1104](https://github.com/entropyxyz/entropy-core/pull/1104) the `/validator/rotate_network_key` endpoint was renamed to `rotate_network_key`
- In [#1109](https://github.com/entropyxyz/entropy-core/pull/1109/), the `MaxPendingAttestations` config type, the `ValidationQueue` storage
structure, and the `NodeInfoChanged` event were removed from the Staking Extension pallet. The
`KeyProvider` and `AttestationQueue` config types were removed from the Attestation pallet.

### Changed
- Use correct key rotation endpoint in OCW ([#1104](https://github.com/entropyxyz/entropy-core/pull/1104))
- Change attestation flow to be pull based ([#1109](https://github.com/entropyxyz/entropy-core/pull/1109/))

## [0.3.0-rc.1](https://github.com/entropyxyz/entropy-core/compare/release/v0.2.0...release/v0.3.0-rc.1) - 2024-10-04

Expand Down
1 change: 0 additions & 1 deletion 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.
54 changes: 28 additions & 26 deletions crates/shared/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,33 +134,35 @@ impl QuoteInputData {
}
}

/// A trait used to get different stored keys for a given account ID.
///
/// Not every account ID will have an given key, in which case the implementer is expected to
/// return `None`.
pub trait KeyProvider<T> {
/// Get an X25519 public key, if any, for the given account ID.
fn x25519_public_key(account_id: &T) -> Option<X25519PublicKey>;

/// Get a provisioning certification key, if any, for the given account ID.
fn provisioning_key(account_id: &T) -> Option<EncodedVerifyingKey>;
/// A trait for types which can handle attestation requests.
#[cfg(not(feature = "wasm"))]
pub trait AttestationHandler<AccountId> {
/// Verify that the given quote is valid and matches the given information about the attestee.
fn verify_quote(
attestee: &AccountId,
x25519_public_key: X25519PublicKey,
provisioning_certification_key: BoundedVecEncodedVerifyingKey,
quote: Vec<u8>,
) -> Result<(), sp_runtime::DispatchError>;

/// Indicate to the attestation handler that a quote is desired.
///
/// The `nonce` should be a piece of data (e.g a random number) which indicates that the quote
/// is reasonably fresh and has not been reused.
fn request_quote(attestee: &AccountId, nonce: [u8; 32]);
}

/// A trait used to describe a queue of attestations.
pub trait AttestationQueue<T> {
/// Indicate that a given attestation is ready to be moved from a pending state to a confirmed
/// state.
fn confirm_attestation(account_id: &T);

/// Request that an attestation get added to the queue for later processing.
fn push_pending_attestation(
signer: T,
tss_account: T,
x25519_public_key: X25519PublicKey,
endpoint: Vec<u8>,
provisioning_certification_key: EncodedVerifyingKey,
);
/// A convenience implementation for testing and benchmarking.
#[cfg(not(feature = "wasm"))]
impl<AccountId> AttestationHandler<AccountId> for () {
fn verify_quote(
_attestee: &AccountId,
_x25519_public_key: X25519PublicKey,
_provisioning_certification_key: BoundedVecEncodedVerifyingKey,
_quote: Vec<u8>,
) -> Result<(), sp_runtime::DispatchError> {
Ok(())
}

/// The list of pending (not processed) attestations.
fn pending_attestations() -> Vec<T>;
fn request_quote(_attestee: &AccountId, _nonce: [u8; 32]) {}
}
2 changes: 2 additions & 0 deletions crates/threshold-signature-server/src/attestation/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ use entropy_testing_utils::{
};
use serial_test::serial;

#[ignore]
#[tokio::test]
#[serial]
async fn test_attest() {
Expand Down Expand Up @@ -72,6 +73,7 @@ async fn test_attest() {
panic!("Waited 10 blocks and attestation is still pending");
}

#[ignore]
#[tokio::test]
#[serial]
async fn test_attest_validation_fail() {
Expand Down
85 changes: 9 additions & 76 deletions pallets/attestation/src/benchmarking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,13 @@
// 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 entropy_shared::{AttestationQueue, QuoteInputData};
use frame_benchmarking::{account, benchmarks, impl_benchmark_test_suite, whitelisted_caller};
use frame_benchmarking::{benchmarks, impl_benchmark_test_suite, whitelisted_caller};
use frame_system::{EventRecord, RawOrigin};

use super::*;
#[allow(unused)]
use crate::Pallet as AttestationPallet;

/// This is a randomly generated secret p256 ECDSA key - for mocking attestation
const ATTESTATION_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,
];

/// This is a randomly generated secret p256 ECDSA key - for mocking the provisioning certification
/// key
const PCK: [u8; 32] = [
117, 153, 212, 7, 220, 16, 181, 32, 110, 138, 4, 68, 208, 37, 104, 54, 1, 110, 232, 207, 100,
168, 16, 99, 66, 83, 21, 178, 81, 155, 132, 37,
];

fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
let events = frame_system::Pallet::<T>::events();
let system_event: <T as frame_system::Config>::RuntimeEvent = generic_event.into();
Expand All @@ -45,76 +31,23 @@ fn assert_last_event<T: Config>(generic_event: <T as Config>::RuntimeEvent) {
benchmarks! {
attest {
let attestee: T::AccountId = whitelisted_caller();
let nonce = [0; 32];

let attestation_key = tdx_quote::SigningKey::from_bytes(&ATTESTATION_KEY.into()).unwrap();
let pck = tdx_quote::SigningKey::from_bytes(&PCK.into()).unwrap();
let pck_encoded = tdx_quote::encode_verifying_key(pck.verifying_key()).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(attestation_key.clone(), pck, input_data.0).as_bytes().to_vec();

// Insert a pending attestation so that this quote is expected
<PendingAttestations<T>>::insert(attestee.clone(), nonce);

// We also need to write to the queue (whose specific implementation writes to the staking
// pallet in this case) to ensure that the `attest` extrinsic has all the information about our
// attestee available.
T::AttestationQueue::push_pending_attestation(
attestee.clone(),
attestee.clone(),
[0; 32],
b"http://localhost:3001".to_vec(),
pck_encoded,
);
let quote = [0; 32].to_vec();
}: _(RawOrigin::Signed(attestee.clone()), quote.clone())
verify {
assert_last_event::<T>(
Event::<T>::AttestationMade.into()
);

// Check that there is no longer a pending attestation
assert!(!<PendingAttestations<T>>::contains_key(attestee));
}

on_initialize {
// Note: We should technically be using `MaxPendingAttestations` but we don't have access to
// that here...
let s in 1 .. 250;

let caller: T::AccountId = whitelisted_caller();
let nonce = [0; 32];
let block_number = <frame_system::Pallet<T>>::block_number();

let pck = tdx_quote::SigningKey::from_bytes(&PCK.into()).unwrap();
let pck_encoded = tdx_quote::encode_verifying_key(pck.verifying_key()).unwrap();

for i in 0..s {
let threshold_account_id: T::AccountId = account("threshold", 0, i);
T::AttestationQueue::push_pending_attestation(
threshold_account_id.clone(),
threshold_account_id.clone(),
[0; 32],
b"http://localhost:3001".to_vec(),
pck_encoded,
);
}
}: {
use frame_support::traits::Hooks;
let _ = AttestationPallet::<T>::on_initialize(block_number);
} verify {
// Here we'll just spot check one account instead of `s` accounts since if one was written
// all were
let threshold_account_id: T::AccountId = account("threshold", 0, 0);
assert!(PendingAttestations::<T>::get(threshold_account_id).is_some());

// We want to ensure that all the requests that we added to the `T::AttestationQueue` were
// also added to `AttestationRequests`
assert!(AttestationRequests::<T>::get(block_number).unwrap().len() == s as usize);
request_attestation {
let attestee: T::AccountId = whitelisted_caller();
}: _(RawOrigin::Signed(attestee.clone()))
verify {
// We're expecting a pending attestation queued up
assert!(<PendingAttestations<T>>::contains_key(attestee));
}
}

Expand Down
113 changes: 63 additions & 50 deletions pallets/attestation/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ mod tests;

#[frame_support::pallet]
pub mod pallet {
use entropy_shared::{AttestationQueue, KeyProvider, QuoteInputData};
use entropy_shared::{AttestationHandler, QuoteInputData};
use frame_support::pallet_prelude::*;
use frame_support::traits::Randomness;
use frame_system::pallet_prelude::*;
Expand Down Expand Up @@ -77,10 +77,6 @@ pub mod pallet {
type WeightInfo: WeightInfo;
/// Something that provides randomness in the runtime.
type Randomness: Randomness<Self::Hash, BlockNumberFor<Self>>;
/// A type used to get different keys for a given account ID.
type KeyProvider: entropy_shared::KeyProvider<Self::AccountId>;
/// A type used to describe a queue of attestations.
type AttestationQueue: entropy_shared::AttestationQueue<Self::AccountId>;
}

#[pallet::genesis_config]
Expand Down Expand Up @@ -119,6 +115,7 @@ pub mod pallet {
#[pallet::generate_deposit(pub(super) fn deposit_event)]
pub enum Event<T: Config> {
AttestationMade,
AttestationIssued(Vec<u8>, BlockNumberFor<T>),
}

/// Errors related to the attestation pallet
Expand All @@ -140,6 +137,8 @@ pub mod pallet {
CannotDecodeVerifyingKey,
/// Could not verify PCK signature
PckVerification,
/// There's an existing attestation request for this account ID.
OutstandingAttestationRequest,
}

#[pallet::call]
Expand All @@ -151,21 +150,70 @@ pub mod pallet {
#[pallet::weight({
<T as Config>::WeightInfo::attest()
})]
pub fn attest(origin: OriginFor<T>, quote: Vec<u8>) -> DispatchResult {
pub fn attest(origin: OriginFor<T>, _quote: Vec<u8>) -> DispatchResult {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So this has been changed to do nothing and always pass for the sake of the TSS tests.

I think it makes sense to get rid of it with this new sort of model, but I would want to do that in a follow up.

We can also change the implementation a bit to actually check attestations if that's something that's desired. Let me know your thoughts.

let _who = ensure_signed(origin)?;

Self::deposit_event(Event::AttestationMade);

Ok(())
}

/// Indicates to the chain that the caller wants to make an attestation.
///
/// Once the chain is aware of this request, other extrinsics will be able to determine
/// whether or not the caller has provided a valid attestation.
#[pallet::call_index(1)]
#[pallet::weight({
<T as Config>::WeightInfo::request_attestation()
})]
pub fn request_attestation(origin: OriginFor<T>) -> DispatchResult {
let who = ensure_signed(origin)?;

// We only want one pending attestation request per account.
ensure!(
!PendingAttestations::<T>::contains_key(&who),
Error::<T>::OutstandingAttestationRequest
);

let mut nonce = [0; 32];
Self::get_randomness().fill_bytes(&mut nonce[..]);
Self::request_quote(&who, nonce);

let block_number = <frame_system::Pallet<T>>::block_number();
Self::deposit_event(Event::AttestationIssued(nonce.to_vec(), block_number));

Ok(())
}
}

impl<T: Config> Pallet<T> {
fn get_randomness() -> ChaCha20Rng {
let phrase = b"quote_creation";
// TODO: Is randomness freshness an issue here
// https://github.com/paritytech/substrate/issues/8312
let (seed, _) = T::Randomness::random(phrase);
// seed needs to be guaranteed to be 32 bytes.
let seed = <[u8; 32]>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
.expect("input is padded with zeroes; qed");
ChaChaRng::from_seed(seed)
}
}

impl<T: Config> entropy_shared::AttestationHandler<T::AccountId> for Pallet<T> {
fn verify_quote(
attestee: &T::AccountId,
x25519_public_key: entropy_shared::X25519PublicKey,
provisioning_certification_key: entropy_shared::BoundedVecEncodedVerifyingKey,
quote: Vec<u8>,
) -> Result<(), DispatchError> {
// Check that we were expecting a quote from this validator by getting the associated
// nonce from PendingAttestations.
let nonce =
PendingAttestations::<T>::get(&who).ok_or(Error::<T>::UnexpectedAttestation)?;
PendingAttestations::<T>::get(attestee).ok_or(Error::<T>::UnexpectedAttestation)?;

// Parse the quote (which internally verifies the attestation key signature)
let quote = Quote::from_bytes(&quote).map_err(|_| Error::<T>::BadQuote)?;

// Get associated x25519 public key from staking pallet
let x25519_public_key =
T::KeyProvider::x25519_public_key(&who).ok_or(Error::<T>::NoX25519KeyForAccount)?;

// Get current block number
let block_number: u32 = {
let block_number = <frame_system::Pallet<T>>::block_number();
Expand All @@ -174,7 +222,7 @@ pub mod pallet {

// 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);
QuoteInputData::new(attestee, x25519_public_key, nonce, block_number);
ensure!(
quote.report_input_data() == expected_input_data.0,
Error::<T>::IncorrectInputData
Expand All @@ -186,9 +234,6 @@ pub mod pallet {
let accepted_mrtd_values = pallet_parameters::Pallet::<T>::accepted_mrtd_values();
ensure!(accepted_mrtd_values.contains(&mrtd_value), Error::<T>::BadMrtdValue);

let provisioning_certification_key =
T::KeyProvider::provisioning_key(&who).ok_or(Error::<T>::NoPCKForAccount)?;

// Check that the attestation public key is signed with the PCK
let provisioning_certification_key = decode_verifying_key(
&provisioning_certification_key
Expand All @@ -202,47 +247,15 @@ pub mod pallet {
.verify_with_pck(provisioning_certification_key)
.map_err(|_| Error::<T>::PckVerification)?;

PendingAttestations::<T>::remove(&who);
T::AttestationQueue::confirm_attestation(&who);
PendingAttestations::<T>::remove(attestee);

// TODO #982 If anything fails, don't just return an error - do something mean

Self::deposit_event(Event::AttestationMade);

Ok(())
}
}

#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(now: BlockNumberFor<T>) -> Weight {
let pending_validators = T::AttestationQueue::pending_attestations();
let num_pending_attestations = pending_validators.len() as u32;
let mut requests = AttestationRequests::<T>::get(now).unwrap_or_default();

for account_id in pending_validators {
let mut nonce = [0; 32];
Self::get_randomness().fill_bytes(&mut nonce[..]);
PendingAttestations::<T>::insert(&account_id, nonce);
requests.push(account_id.encode());
}

AttestationRequests::<T>::insert(now, requests);

<T as Config>::WeightInfo::on_initialize(num_pending_attestations)
}
}

impl<T: Config> Pallet<T> {
pub fn get_randomness() -> ChaCha20Rng {
let phrase = b"quote_creation";
// TODO: Is randomness freshness an issue here
// https://github.com/paritytech/substrate/issues/8312
let (seed, _) = T::Randomness::random(phrase);
// seed needs to be guaranteed to be 32 bytes.
let seed = <[u8; 32]>::decode(&mut TrailingZeroInput::new(seed.as_ref()))
.expect("input is padded with zeroes; qed");
ChaChaRng::from_seed(seed)
fn request_quote(who: &T::AccountId, nonce: [u8; 32]) {
PendingAttestations::<T>::insert(who, nonce)
}
}
}
Loading
Loading