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

[FRAME] State Diff Guard #4204

Open
wants to merge 15 commits into
base: master
Choose a base branch
from

Conversation

dastansam
Copy link
Contributor

@dastansam dastansam commented Apr 18, 2024

Motivation

Adds a guard that prevents unexpected storage changes. StateDiffGuard:

  • initializes map of prefixes to the mutation policy (can/not change)
  • upon initialization takes the snapshot of the current state
  • when dropped, iterates every storage key, gets its mutation policy, defaulting to AnythingElse if present.
  • applies the mutation policy, if present

Example

Code below panics since migration is changing SomeDoubleMap

use frame_support::storage::StateDiffGuard;

pub struct UncheckedMigrateToV1<T: Config>(PhantomData<T>);

impl<T: Config> UncheckedOnRuntimeUpgrade for UncheckedMigrateToV1<T> {
    fn on_runtime_upgrade() -> Weight {
        // migration logic here
        SomeDoubleMap::remove(_);

        Weight::default()
    }

    #[cfg(feature = "try-runtime")]
    fn try_on_runtime_upgrade() -> Result<Weight, &'static str> {
        // Using StateDiffGuard to ensure storage items are correctly migrated
        let _guard = StateDiffGuard::builder()
            .must_change_if_exists(SomeMap::<T>::storage_info())
            .must_not_change(SomeDoubleMap::<T>::storage_info())
            .can_not_change(GuardSubject::AnythingElse)
            .build();

        // try runtime upgrade logic here
        let weight = Self::on_runtime_upgrade();

        // any other logic
        Ok(weight)
    }
}

part of #240

polkadot address: 16FqwPZ8GRC5U5D4Fu7W33nA55ZXzXGWHwmbnE1eT6pxuqcT

@dastansam dastansam changed the title add StateDiffGuard initial impl migrations: State difference guard and recorder Apr 18, 2024
@dastansam dastansam changed the title migrations: State difference guard and recorder migrations: state difference guard and recorder Apr 19, 2024
@dastansam dastansam force-pushed the feat/state-diff-guard branch from f3cc891 to 730d9a9 Compare April 19, 2024 00:30
@dastansam dastansam force-pushed the feat/state-diff-guard branch from 8e68898 to ddf1711 Compare April 19, 2024 12:04
#[derive(Default, Debug)]
pub struct StateDiffGuard {
// Storage prefixes that are expected to change.
whitelisted_prefixes: BTreeSet<StoragePrefix>,
Copy link
Member

@ggwpez ggwpez Apr 19, 2024

Choose a reason for hiding this comment

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

Suggested change
whitelisted_prefixes: BTreeSet<StoragePrefix>,
must_change_prefixes: BTreeSet<StoragePrefix>,

The name here is not really expressing that they must change. It is also possible to make this more abstract by having a Map<Prefix, Change> where Change can be { Can | Must } x { Change | NotChange } (PS: not sure if these enum values are correct - hopefully you get the idea), but i think its fine for the beginning.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

made some improvements here, let me know if it's in the right direction, thanks)

@dastansam dastansam force-pushed the feat/state-diff-guard branch from e93ecb2 to d1e0384 Compare April 29, 2024 00:37
@paritytech-cicd-pr
Copy link

The CI pipeline was cancelled due to failure one of the required jobs.
Job name: cargo-clippy
Logs: https://gitlab.parity.io/parity/mirrors/polkadot-sdk/-/jobs/6079245

@dastansam dastansam force-pushed the feat/state-diff-guard branch from d1e0384 to 12e6f44 Compare April 29, 2024 00:52
@dastansam dastansam requested a review from ggwpez April 29, 2024 00:52
@dastansam dastansam marked this pull request as ready for review April 29, 2024 00:52
@dastansam dastansam requested a review from a team as a code owner April 29, 2024 00:52
@kianenigma
Copy link
Contributor

This looks like a very good PR. @dastansam sorry for the delay, are you still around for finishing? @ggwpez @liamaharon and I can provide reviews.

@kianenigma
Copy link
Contributor

What I see missing is integrating this into polkadot_sdk_docs::reference_docs::migrations. It could be as simple as a basic mention.

@dastansam
Copy link
Contributor Author

This looks like a very good PR. @dastansam sorry for the delay, are you still around for finishing? @ggwpez @liamaharon and I can provide reviews.

thanks, for sure. Looking forward for your reviews)

@dastansam
Copy link
Contributor Author

hey @ggwpez, thanks for the updates. I am willing to finish this off if you have any other comments?

Signed-off-by: Oliver Tale-Yazdi <[email protected]>
Signed-off-by: Oliver Tale-Yazdi <[email protected]>
Signed-off-by: Oliver Tale-Yazdi <[email protected]>
Signed-off-by: Oliver Tale-Yazdi <[email protected]>
@ggwpez
Copy link
Member

ggwpez commented Jan 10, 2025

Just wanted to improve the API a bit but I think it should be good now.

PS: Maybe still update the MR description to show a small copy&paste example, possibly one of the tests to make it understandable what is being added.

@ggwpez ggwpez added the T1-FRAME This PR/Issue is related to core FRAME, the framework. label Jan 10, 2025
@ggwpez ggwpez changed the title migrations: state difference guard and recorder [FRAME] State Diff Guard Jan 10, 2025
let check_passed = self.apply_mutation_policy();

// No need to double panic, eg. inside a test assertion failure.
if sp_std::thread::panicking() {
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
if sp_std::thread::panicking() {
#[cfg(feature = "std")]
if std::thread::panicking() {

Copy link
Member

Choose a reason for hiding this comment

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

You did not apply this.

substrate/frame/support/src/storage/state_diff_guard.rs Outdated Show resolved Hide resolved
let mut state = BTreeMap::new();

let mut previous_key = Vec::new();
while let Some(next) = sp_io::storage::next_key(&previous_key) {
Copy link
Member

Choose a reason for hiding this comment

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

Why are we reading the entire state? We should only read the state based on the prefixes we will compare later.

Copy link
Contributor Author

@dastansam dastansam Jan 10, 2025

Choose a reason for hiding this comment

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

so that we can get the keys that were not meant to be changed (i.e maybe accidentally removed). see this line

Copy link
Contributor

Choose a reason for hiding this comment

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

Not from the runtime, but from the node side we could probably do this more elegently by using the ProofRecording types that are used for state proofs.

I think from the runtime there is no way other than (naively) reading the entire state pre and post operation, and compare them.

@dastansam dastansam requested a review from bkchr January 15, 2025 18:55
@dastansam
Copy link
Contributor Author

@bkchr one more review, please

Copy link
Member

@bkchr bkchr left a comment

Choose a reason for hiding this comment

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

Given the current way it is implemented, we can never use this with any production chain. The time it would take to run the migrations would grow by a lot if every migration would read the entire state.

IMO if we really want to have this, we need to change the internals. The user needs to declare which storage entries are allowed to change. At the start we would read these keys from the state. (Could still hit a big map, but we can not really go better with what we have at hand)

After the operation is finished, we could do the checks that the keys that should have changed, indeed have changed and also record the current keys/values. Now we could do a trick and run the migration and the checking in a storage transaction. After the migration and the checking succeeded, we would record the storage root and revert the transaction. Then we would apply the changed keys again and check that the storage root matches. If the storage root doesn't match, something else was touched. Problem would be that we would not know which key changed.

//! In the example above, the guard will panic if any storage entry that doesn't match `SomeMap` or
//! `SomeDoubleMap` prefixes is changed.

use alloc::{collections::btree_map::BTreeMap, vec::Vec};
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
use alloc::{collections::btree_map::BTreeMap, vec::Vec};
use alloc::{collections::btree_map::BTreeMap, vec::Vec, vec};

//!
//! # Example
//!
//! Use the `StateDiffGuard` in a migration:
Copy link
Member

Choose a reason for hiding this comment

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

IMO this can not be used in any migration with a reasonable state, because of the behavior of reading the entire state.

This would take ages.

let check_passed = self.apply_mutation_policy();

// No need to double panic, eg. inside a test assertion failure.
if sp_std::thread::panicking() {
Copy link
Member

Choose a reason for hiding this comment

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

You did not apply this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
T1-FRAME This PR/Issue is related to core FRAME, the framework.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants