Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Implement InspectEnumerable for Uniques #9117

Merged
6 commits merged into from
Jul 1, 2021
Merged

Implement InspectEnumerable for Uniques #9117

6 commits merged into from
Jul 1, 2021

Conversation

apopiak
Copy link
Contributor

@apopiak apopiak commented Jun 15, 2021

This PR implements the InspectEnumerable trait for the Uniques pallet, thus allowing on-chain iteration of e.g. a user's non-fungible assets.

@apopiak apopiak added A0-please_review Pull request needs code review. B3-apinoteworthy C1-low PR touches the given topic and has a low impact on builders. D3-trivial 🧸 PR contains trivial changes in a runtime directory that do not require an audit labels Jun 15, 2021
@apopiak apopiak added this to In progress in Runtime via automation Jun 15, 2021
@apopiak
Copy link
Contributor Author

apopiak commented Jun 15, 2021

cc @Lohann

@shawntabrizi
Copy link
Member

Not for this PR, but maybe we should think about an iter_keys function which would be slightly more optimal when we dont care about the value and dont need to read or decode it.

@apopiak
Copy link
Contributor Author

apopiak commented Jun 15, 2021

I've thought the same, I'll create an issue.

frame/uniques/src/impl_nonfungibles.rs Outdated Show resolved Hide resolved
frame/uniques/src/impl_nonfungibles.rs Outdated Show resolved Hide resolved
///
/// NOTE: iterating this list invokes a storage read per item.
fn classes() -> Box<dyn Iterator<Item = Self::ClassId>> {
Box::new(ClassMetadataOf::<T, I>::iter_keys())
Copy link
Contributor

Choose a reason for hiding this comment

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

Awesome!! I didn't know that was possible to Box an Iterator

Copy link
Contributor

Choose a reason for hiding this comment

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

Indeed, this is a trait object. One day if Rust supports impl Trait in trait method return types, we'd use that here instead, but until then, Box<dyn Trait> is the only workaround.

Copy link
Contributor

Choose a reason for hiding this comment

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

I'm just wondering if is secure (or possible) to use this iterator as a pagination for multi-step migration, ex: migrate all uniques using on_initialize hook but without repeating the same key.. but I don't think it is possible.

Copy link
Contributor

@KiChjang KiChjang Jul 1, 2021

Choose a reason for hiding this comment

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

Entirely possible. The Iterator API has a rich set of operations that allow you to do a lot of things. In this particular case, I am looking at the take method which allows me to iterate and return the first n elements that I choose.

So since the storage API ultimately relies on sp_io::storage::next_key to iterate over to the next key, and that the way it works is by passing the previous storage key as an argument, what you can do is take say 10 keys in on_initialize, and then grab the previous key from the iterator and store it, so that on the next block's on_initialize, you fetch the previous key and recreate an iterator from it.

I know this is a little bit hard to digest, but it's really easy to understand if I write some sample code:

impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
	fn on_initialize(_: T::BlockNumber) -> Weight {
		if start_of_migration() {
			let mut iterator = <Self as InspectEnumberable<T::AccountId>>::classes();
			let classes = iterator.take(10);
			let previous_key = iterator.previous_key;
			store_iteration_key(previous_key);

			do_something_with_classes(classes);
		} else {
			let previous_key = fetch_iteration_key();
			let mut iterator = create_iterator_from_key(previous_key);
			let classes = iterator.take(10);

			do_something_with_classes(classes);
		}
	}
}

The caveat that I can think of here is about storage. We can't really allow anything to do a DB write while migration is in progress, otherwise it might change the storage root and hence invalidate our previous key. This also entails that we can't store the previous key into storage either, and I can think of 2 ways to work around this limitation: 1) use in-memory storage, but it's a bit unclear to me how that works; or 2) use off-chain storage.

Copy link
Contributor

@Lohann Lohann Jul 1, 2021

Choose a reason for hiding this comment

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

Genius! I have no idea how memory storage works either, but if is possible to persist the iterator, theoretically is possible to keep the storage keys write/delete working by including some extra logic before each StorageMap::remove operation, ex:

Example using uniques/src/lib.rs

pub fn destroy(
	origin: OriginFor<T>,
	#[pallet::compact] class: T::ClassId,
	witness: DestroyWitness,
) -> DispatchResult {
		// snip ...
		let previous_key = fetch_iteration_key();
		if let Some(previous_class) = previous_key {
			if previous_class == class {
				// skip current key
				let mut iterator = create_iterator_from_key(previous_class);
				store_iteration_key(iterator.next());
			}
		}
		ClassMetadataOf::<T, I>::remove(&class);
		// snip ...
	})
}

Of course it increases the weight of each operation, but the other alternative I see is migrate everything to another storage.. which doesn't seems interesting either as it duplicates every existing unique.

Runtime automation moved this from In progress to Needs Audit Jul 1, 2021
@KiChjang
Copy link
Contributor

KiChjang commented Jul 1, 2021

bot merge

@ghost
Copy link

ghost commented Jul 1, 2021

Waiting for commit status.

@ghost ghost merged commit ebf2391 into master Jul 1, 2021
@ghost ghost deleted the apopiak/impl-enumerable branch July 1, 2021 21:34
Runtime automation moved this from Needs Audit to Done Jul 1, 2021
@shawntabrizi shawntabrizi moved this from Done to Archive in Runtime Jul 8, 2021
This pull request was closed.
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
A0-please_review Pull request needs code review. C1-low PR touches the given topic and has a low impact on builders. D3-trivial 🧸 PR contains trivial changes in a runtime directory that do not require an audit
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants