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

Trigger attestation check during validate #1063

Merged
merged 47 commits into from
Oct 2, 2024

Conversation

HCastano
Copy link
Collaborator

@HCastano HCastano commented Sep 23, 2024

When somebody states their intention to become a validator on the network they need to go
through the staking extension pallet's validate() extrinsic. As part of this process we
want to ensure that anybody submitting their candidacy is running on suitable hardware,
and as such we need issue an attestation challenge.

This PR adds a call into the attestation pallet from the validate() extrinsic and only
accepts a caller as a candidate if they successfully pass the attestation check.

There were some issues around using the Attestation pallet directly from the Staking
Extension pallet due to circular dependencies, so I worked around this in two ways:

  1. I changed the Attestation pallet to be soft-coupled to the Staking Extension pallet
  2. I introduced an intermediate attestation queue to the Staking Extension pallet through
    which both pallets communicate

For (2) to work I had to add on_initialize hooks to both pallets in order to ensure
that any new attestations or confirmations got picked up and dealt with "automatically".

With the tight coupling we're not able to build out a mock runtime for the staking extension pallet
since the compiler isn't able to verify that we have an implementation of the staking extension
pallet for our mock attestation pallet (yeah, bit of a roundabout thing...).

The loose coupling gives us a bit of flexibility here, and also allows us to scope the iteraction
between the pallets in a more fine grained way.
We can't rely on the `ThresholdToStash` or `ThresholdServer` storage
entries being populated anymore.
let mut requests = AttestationRequests::<T>::get(now).unwrap_or_default();

for account_id in pending_validators {
let nonce = [0; 32]; // TODO (Nando): Fill this out properly
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@ameba23 what's the expected flow for nonce generation here?

Copy link
Contributor

Choose a reason for hiding this comment

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

It should be random - or as unpredictable as possible (eg: previous block hash). So i guess we use the on-chain randomness.

@HCastano HCastano marked this pull request as ready for review September 30, 2024 18:28
@HCastano HCastano added the Bump `spec_version` A change which affects the current runtime behaviour label Sep 30, 2024
Copy link
Contributor

@ameba23 ameba23 left a comment

Choose a reason for hiding this comment

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

🥇 Great

///
/// Not every account ID will have an given key, in which case the implementer is expected to
/// return `None`.
pub trait KeyProvider<T> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Just to be clear - these types are in entropy-shared to avoid circular dependencies between pallets. They are not intendend to be used by entropy-tss, right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Correct. These traits had to be defined somewhere that multiple unrelated pallets could access them

@@ -177,21 +184,24 @@ 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>::NoX25519KeyForAccount)?;
Copy link
Contributor

Choose a reason for hiding this comment

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

Strictly speaking we should rename this error variant as here we are getting the PCK, not the x25519 public key.

}

impl<T: Config> Pallet<T> {
pub fn get_randomness() -> ChaCha20Rng {
Copy link
Contributor

Choose a reason for hiding this comment

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

This function is in the staking pallet and is public, and the staking pallet is a dependency of this pallet. Is there a reason why we need it here as well?

In the registry pallet, its called like this:

 let mut rng = pallet_staking_extension::Pallet::<T>::get_randomness();

Copy link
Collaborator Author

@HCastano HCastano Oct 1, 2024

Choose a reason for hiding this comment

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

We can't do that anymore because the Attestation pallet doesn't (from Substrate's point of view) depend on the Staking Extension pallet anymore.

Said in code, this isn't satisfied:

pub trait pallet_attestation::Config: pallet_staking_extension::Config { }

The Registry pallet still has that trait bound so it's allowed to pull in code from the Staking Extension.


#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn on_initialize(now: BlockNumberFor<T>) -> Weight {
Copy link
Contributor

Choose a reason for hiding this comment

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

I was getting confused here, and with this hook in the staking pallet, because i was assuming on_initialize was called once at genesis. But having looked it up, its called each block, right?

Copy link
Member

Choose a reason for hiding this comment

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

that is correct

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yes, it's called at the start of each block and in a specific order depending on where the pallet was declared in the construct_runtime! macro.

requests.push(account_id.encode());
}

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

Choose a reason for hiding this comment

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

It feels a little bit like this data structure is now redundant and the propagation pallet could read directly from the attestation queue that the staking pallet add requests to. But i guess we need to keep the nonce generation logic inside this pallet, and it might also be useful for when we add other methods of triggering attestation requests.

Copy link
Member

Choose a reason for hiding this comment

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

well maybe Id like to explore this a little more because this does trigger an extra on init, the nonce creation doesn't look like it needs to be here, could potentially be done when the attest request is created, could be put into a generalize function like create request and be called instead of having to go through each pending request to add the nonce

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I agree, there probably is a way to consolidate these data structures but it's not something I wanted to prioritize for this PR. And yeah, this will probably need to change a bit once we start adding more attestation checks.

Can open a follow up issue for this.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Opened #1089.


/// This is a randomly generated secret p256 ECDSA key - for mocking the provisioning certification
/// key
const PCK: [u8; 32] = [
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: I think you also use this constant in the tests. I would put it in mock.rs and make it public, and you can use it here and it the tests.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I tried doing this but there are some issues around differing feature flags for tests and benchmarks, so we can't actually pull in constants from mock.rs in easily.

Another solution would be to move the PCK constant either to the top crate level and feature flag it there but for two uses it doesn't seem worth it to me. But ya can do it if you think it would be better

PendingAttestations::<T>::remove(&who);
T::AttestationQueue::confirm_attestation(&who);
Copy link
Contributor

Choose a reason for hiding this comment

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

You are probably already aware of this - but if and when we add other ways of triggering attestation requests, confirm_attestation is gonna get called regardless of whether this particular attestation request was related to something in the attestation queue. This is not a problem, since nothing bad happens if we call it several times when they are already in a Confirmed state.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Ya agreed. As I mentioned elsewhere, it would be nice to consolidate these if possible

// # Panics
//
// Panics if an invalid `validator_stash` or `provisioning_certification_key` are passed
// in. The caller should check (e.g, using `Self::get_stash()` that these inputs are valid.
Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if it makes any difference, but maybe you intended this to be a doccomment ///

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

It does make a difference. If we use doc comments here it would override the doc comments from the trait definition, which is what I initially wanted.

Thinking about it bit it might be safer to override in this case since it would allow us to warn users of this API through docs (e.g docs.rs) rather than through the source code itself.

Copy link
Member

@JesseAbram JesseAbram left a comment

Choose a reason for hiding this comment

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

looks good a few comments, the only thing that I don't love here is the two on_inits just a little more elaboration on those comments then id be good to approve

/// A queue of validator attestation requests.
///
/// This tracks the status of each attestation (pending or confirmed), as well as information
/// about the validator who is in the process of submitting an attestation.
Copy link
Member

Choose a reason for hiding this comment

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

the key is a TSS account? can you add that to the comments

@@ -742,4 +812,77 @@ pub mod pallet {
I::start_session(start_index);
}
}

impl<T: Config> entropy_shared::KeyProvider<T::AccountId> for Pallet<T> {
Copy link
Member

Choose a reason for hiding this comment

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

this looks inefficient to me, in both cases you call get_server_info making a read, but you call both the functions twice in the same function in attest (and maybe elsewhere) probably a good idea to take server_info as in input so you can reduce reads, or a function that gets both

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah not the most efficient thing, but basically it boils down to me not knowing how much of the server logic to expose to the attestation pallet.

I don't really want to couple it that tightly if we don't have to, and having a key provider with just the right amount of fields we need seemed like a nice compromise. For example, we could have these be computed on the fly for example, as opposed to being stuck with the storage read for ServerInfo.

requests.push(account_id.encode());
}

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

Choose a reason for hiding this comment

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

well maybe Id like to explore this a little more because this does trigger an extra on init, the nonce creation doesn't look like it needs to be here, could potentially be done when the attest request is created, could be put into a generalize function like create request and be called instead of having to go through each pending request to add the nonce

Copy link
Member

@JesseAbram JesseAbram left a comment

Choose a reason for hiding this comment

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

happy with the issue looks good

@HCastano HCastano merged commit d1f2c96 into master Oct 2, 2024
8 checks passed
@HCastano HCastano deleted the hc/add-attestation-check-to-validate branch October 2, 2024 15:16
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Bump `spec_version` A change which affects the current runtime behaviour
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants