diff --git a/plt/plt-block-state/src/block_state.rs b/plt/plt-block-state/src/block_state.rs index 274b9263b8..533a2696d7 100644 --- a/plt/plt-block-state/src/block_state.rs +++ b/plt/plt-block-state/src/block_state.rs @@ -6,6 +6,7 @@ use crate::block_state::blob_store::{ use crate::block_state::cacheable::Cacheable; use crate::block_state::external::{ExternalBlockStateOperations, ExternalBlockStateQuery}; use crate::block_state::hash::Hashable; +use crate::block_state::migration::Migrate; use crate::block_state::state::protocol_level_tokens::ProtocolLevelTokens; use crate::block_state::types::AccountWithCanonicalAddress; use crate::block_state::types::protocol_level_tokens::{ @@ -31,6 +32,7 @@ pub mod cacheable; pub mod external; pub mod hash; pub mod lfmb_tree; +pub mod migration; mod smart_contract_trie; mod state; pub mod types; @@ -80,23 +82,6 @@ impl BlockState { MutableBlockState::new(self) } - /// Migrate the PLT block state from one blob store to another. - /// - /// # Arguments - /// - /// - `from_loader` Blob store loader for the blob store we are migrating from. - /// - `to_storer` Blob store storer for the blob store we are migrating to. - /// - `to_protocol_version` Protocol version for the block state to migrate to. - pub fn migrate( - &self, - _from_loader: impl BlobStoreLoad, - _to_storer: impl BlobStoreStore, - _to_protocol_version: ProtocolVersion, - ) -> Self { - // todo ar - todo!() - } - /// See [`blob_store::load_from_store`]. This function only differs by taking /// protocol version as argument. pub fn load_from_store( @@ -130,6 +115,25 @@ impl BlockState { data: BlockStateData { tokens }, }) } + + /// See [`Migrate::migrate`]. This function only differs by taking + /// protocol version as argument. + pub fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + to_protocol_version: ProtocolVersion, + ) -> BlockStateResult + where + Self: Sized, + { + let new_tokens = self.data.tokens.migrate(from_loader, to_storer)?; + + Ok(Self { + protocol_version: to_protocol_version, + data: BlockStateData { tokens: new_tokens }, + }) + } } impl Storable for BlockState { diff --git a/plt/plt-block-state/src/block_state/blob_reference.rs b/plt/plt-block-state/src/block_state/blob_reference.rs index 13effdc4be..e2d48cecd9 100644 --- a/plt/plt-block-state/src/block_state/blob_reference.rs +++ b/plt/plt-block-state/src/block_state/blob_reference.rs @@ -1,7 +1,15 @@ -//! Blob store reference types. A blob store reference represents the value of a block state component, -//! that may be in the blob store, in memory, or both. A blob store reference to a block state component -//! is stored in the blob store as a reference to another location in the blob store where the block -//! state component is stored. This allows for +//! Definition of blob store reference types. A blob store reference may largely be though of as +//! a pointer to a value in the blob store, but the representation of the value may vary during +//! the lifetime of the reference. The value may be represented in, +//! +//! * the blob store only (the reference is a pure blob store pointer) +//! * memory (the value will eventually be written to the blob store), +//! * both the blob store and memory (the reference is a pointer +//! that also caches the value in memory). +//! +//! A blob store reference to a block state component +//! is stored in the blob store as a [blob location](super::blob_store::BlobStoreLocation) +//! This allows for //! //! * sharing block state components pointed to by blob references between different block states //! * deciding when to load the referenced block state component, diff --git a/plt/plt-block-state/src/block_state/blob_reference/hashed_cacheable_reference.rs b/plt/plt-block-state/src/block_state/blob_reference/hashed_cacheable_reference.rs index bcbf170837..c96bea01b0 100644 --- a/plt/plt-block-state/src/block_state/blob_reference/hashed_cacheable_reference.rs +++ b/plt/plt-block-state/src/block_state/blob_reference/hashed_cacheable_reference.rs @@ -7,6 +7,7 @@ use crate::block_state::blob_store::{ }; use crate::block_state::cacheable::Cacheable; use crate::block_state::hash::Hashable; +use crate::block_state::migration::Migrate; use crate::block_state::utils::OwnedOrBorrowed; use crate::block_state_interface::BlockStateResult; use concordium_base::common::{Buffer, Get, Put}; @@ -267,6 +268,22 @@ impl Hashable for HashedCacheableRef { } } +impl Migrate for HashedCacheableRef { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let migrated_value = self.value(from_loader)?.migrate(from_loader, to_storer)?; + let new_hcr = Self::new(migrated_value); + new_hcr.inner.repr.get_reference_or_store(to_storer); + Ok(new_hcr) + } +} + #[cfg(test)] mod tests { use super::*; @@ -340,6 +357,35 @@ mod tests { assert_eq!(val_ref_tmp, val_ref); } + /// Test migrate a value: + /// + /// * create value in source blob store + /// * migrate to new blob store + /// * load the new value from the new blob store + #[test] + fn test_migrate() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create new value and store it in the source blob store + let val = TestRef::new(StoreSerialized(1u64)); + blob_store::store_to_store(&mut from_store, &val); + + // Migrate to destination blob store + let new_val = val.migrate(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + drop(val); + + // Assert migrated reference + assert_cached_repr(&new_val); + assert_eq!(*new_val.value(&to_store).unwrap(), StoreSerialized(1)); + drop(new_val); + + // Load migrated reference and assert + let new_val2: TestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + assert_eq!(*new_val2.value(&to_store).unwrap(), StoreSerialized(1)); + } + /// Test storing cached value. #[test] fn test_store_cached_value() { @@ -515,11 +561,11 @@ mod tests { ); } + type NestedTestRef = HashedCacheableRef>>; + /// Test store, load and cache a reference with a nested reference. #[test] fn test_nested_reference_store_load_and_cache() { - type NestedTestRef = HashedCacheableRef>>; - let mut store = BlobStoreStub::default(); let val1 = HashedCacheableRef::new(HashedCacheableRef::new(StoreSerialized(1u64))); @@ -542,4 +588,34 @@ mod tests { assert_eq!(*val_tmp, StoreSerialized(1)); assert_eq!(val_ref_tmp, val_nested_blob_ref); } + + /// Test migrate a reference with a nested reference. + #[test] + fn test_nested_reference_migrate() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create new value and store it in the source blob store + let val = HashedCacheableRef::new(HashedCacheableRef::new(StoreSerialized(1u64))); + blob_store::store_to_store(&mut from_store, &val); + + // Migrate to destination blob store + let new_val = val.migrate(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + drop(val); + + // Assert migrated reference + assert_eq!( + *new_val.value(&to_store).unwrap().value(&to_store).unwrap(), + StoreSerialized(1) + ); + drop(new_val); + + // Load migrated reference and assert + let new_val2: NestedTestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + assert_eq!( + *new_val2.value(&to_store).unwrap().value(&to_store).unwrap(), + StoreSerialized(1) + ); + } } diff --git a/plt/plt-block-state/src/block_state/blob_store.rs b/plt/plt-block-state/src/block_state/blob_store.rs index 8db98d11c9..363e99f5ae 100644 --- a/plt/plt-block-state/src/block_state/blob_store.rs +++ b/plt/plt-block-state/src/block_state/blob_store.rs @@ -151,7 +151,7 @@ pub mod test_stub { use super::*; /// In-memory blob store stub implemented via a `Vec`. - #[derive(Default, Debug)] + #[derive(Default, Debug, Clone)] pub struct BlobStoreStub(pub Vec); impl BlobStoreStore for BlobStoreStub { diff --git a/plt/plt-block-state/src/block_state/lfmb_tree.rs b/plt/plt-block-state/src/block_state/lfmb_tree.rs index 26c61e4577..5278f72e1e 100644 --- a/plt/plt-block-state/src/block_state/lfmb_tree.rs +++ b/plt/plt-block-state/src/block_state/lfmb_tree.rs @@ -9,6 +9,7 @@ use crate::block_state::blob_store::{ use crate::block_state::cacheable::Cacheable; use crate::block_state::hash; use crate::block_state::hash::Hashable; +use crate::block_state::migration::Migrate; use crate::block_state::utils::OwnedOrBorrowed; use crate::block_state_interface::{BlockStateFailure, BlockStateResult}; use concordium_base::common::{Buffer, Get, Put}; @@ -873,6 +874,52 @@ impl Cacheable for Subtree { } } +impl Migrate for LfmbTree { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_inner = match &self.inner { + LfmbTreeInner::Empty => LfmbTreeInner::Empty, + LfmbTreeInner::NonEmpty(size, subtree) => { + LfmbTreeInner::NonEmpty(*size, subtree.migrate(from_loader, to_storer)?) + } + }; + + Ok(Self { + inner: new_inner, + _key_type: PhantomData, + }) + } +} + +impl Migrate for Subtree { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + Ok(match self { + Subtree::Leaf(value_ref) => { + let new_value_ref = value_ref.migrate(from_loader, to_storer)?; + Subtree::Leaf(new_value_ref) + } + Subtree::Node(height, left_ref, right_ref) => { + let new_left_ref = left_ref.migrate(from_loader, to_storer)?; + let new_right_ref = right_ref.migrate(from_loader, to_storer)?; + Subtree::Node(*height, new_left_ref, new_right_ref) + } + }) + } +} + #[cfg(test)] mod tests { use super::*; @@ -1067,7 +1114,52 @@ mod tests { let tree2: TestTree = blob_store::load_from_store(&store, blob_ref).unwrap(); // Assert loaded tree is equal to the tree we started with - assert_trees_eq(&store, &tree1, &tree2, format!("loaded tree of size {}", i)); + assert_trees_eq( + &store, + &store, + &tree1, + &tree2, + format!("loaded tree of size {}", i), + ); + } + } + + /// Tests migrating tree into new blob store + #[test] + fn prop_test_migrate() { + for i in 0..100 { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create tree and store it + let tree = create_tree(&mut from_store, i); + blob_store::store_to_store(&mut from_store, &tree); + + // Migrate the tree and store it + let new_tree = tree.migrate(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_tree); + + // Assert migrated tree is equal to the tree we started with + assert_trees_eq( + &from_store, + &to_store, + &tree, + &new_tree, + format!("loaded tree of size {}", i), + ); + drop(new_tree); + + // Load migrated tree from destination store + let new_tree2: TestTree = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + + // Assert tree loaded from destination store is equal to the tree we started with + assert_trees_eq( + &from_store, + &to_store, + &tree, + &new_tree2, + format!("loaded tree of size {}", i), + ); } } @@ -1084,7 +1176,13 @@ mod tests { tree2.cache_reference_values(&store).expect("cache"); // Assert cached tree is identical to the tree with started with - assert_trees_eq(&store, &tree1, &tree2, format!("cached tree of size {}", i)); + assert_trees_eq( + &store, + &store, + &tree1, + &tree2, + format!("cached tree of size {}", i), + ); // Assert that when caching again or looking up entries, we don't need to read from the blob store again. // We assert that by using UnreachableBlobStore. @@ -1198,7 +1296,8 @@ mod tests { /// Assert node structure and values in tree are equal. fn assert_trees_eq( - loader: &impl BlobStoreLoad, + loader1: &impl BlobStoreLoad, + loader2: &impl BlobStoreLoad, tree1: &LfmbTree, tree2: &LfmbTree, context: String, @@ -1212,7 +1311,7 @@ mod tests { LfmbTreeInner::NonEmpty(size2, subtree2), ) => { assert_eq!(size1, size2); - assert_subtrees_eq(loader, subtree1, subtree2, context.clone()); + assert_subtrees_eq(loader1, loader2, subtree1, subtree2, context.clone()); } (_, _) => { panic!("{}: trees not equal: {:?}, {:?}", context, tree1, tree2); @@ -1222,15 +1321,16 @@ mod tests { /// Assert node structure and values in subtree are equal. fn assert_subtrees_eq( - loader: &impl BlobStoreLoad, + loader1: &impl BlobStoreLoad, + loader2: &impl BlobStoreLoad, subtree1: &Subtree, subtree2: &Subtree, context: String, ) { match (subtree1, subtree2) { (Subtree::Leaf(val_ref1), Subtree::Leaf(val_ref2)) => { - let val1 = &*val_ref1.value(loader).unwrap(); - let val2 = &*val_ref2.value(loader).unwrap(); + let val1 = &*val_ref1.value(loader1).unwrap(); + let val2 = &*val_ref2.value(loader2).unwrap(); assert_eq!(val1, val2, "{}: leaf value", context); } ( @@ -1238,12 +1338,12 @@ mod tests { Subtree::Node(height2, left_ref2, right_ref2), ) => { assert_eq!(height1, height2); - let left1 = &*left_ref1.value(loader).unwrap(); - let right1 = &*right_ref1.value(loader).unwrap(); - let left2 = &*left_ref2.value(loader).unwrap(); - let right2 = &*right_ref2.value(loader).unwrap(); - assert_subtrees_eq(loader, left1, left2, context.clone()); - assert_subtrees_eq(loader, right1, right2, context.clone()); + let left1 = &*left_ref1.value(loader1).unwrap(); + let right1 = &*right_ref1.value(loader1).unwrap(); + let left2 = &*left_ref2.value(loader2).unwrap(); + let right2 = &*right_ref2.value(loader2).unwrap(); + assert_subtrees_eq(loader1, loader2, left1, left2, context.clone()); + assert_subtrees_eq(loader1, loader2, right1, right2, context.clone()); } (_, _) => { panic!("subtrees not equal: {:?}, {:?}", subtree1, subtree2); diff --git a/plt/plt-block-state/src/block_state/migration.rs b/plt/plt-block-state/src/block_state/migration.rs new file mode 100644 index 0000000000..abfce03296 --- /dev/null +++ b/plt/plt-block-state/src/block_state/migration.rs @@ -0,0 +1,55 @@ +//! Definition of the [`Migrate`] trait that defines migration of block state values +//! from the blob store of the current protocol version blob store to the blob store of the next +//! protocol version (each protocol version has its own blob store). + +use crate::block_state::blob_store::{BlobStoreLoad, BlobStoreStore, StoreSerialized}; +use crate::block_state_interface::BlockStateResult; +use concordium_base::common::Serial; + +/// Trait implemented by block state values to support migration when protocol version increments. +/// The migration must, +/// +/// * recursively store all [blob references](super::blob_reference) into the new blob store +/// of we migrate to (the blob store of the new protocol version) +/// * apply any changes to the representation of the block state value (data model migration) +/// +/// Since each protocol version has its own blob store, migration is always needed at +/// protocol update, even if the block state value has no data model changes. +pub trait Migrate { + /// Migrate the value from the blob store it is currently stored in + /// (`from_loader`), to the new blob store for the next protocol version (`to_storer`). + /// Migration must: + /// + /// * recursively migrate all [blob references](super::blob_reference) the value is composed of + /// to the new blob store, including storing the referenced values in the new blob store + /// * apply any changes to the value (data model migration) + /// + /// The function returns the new, migrated value, that represents the value on the new protocol + /// version, and whose [blob references](super::blob_reference) points to the new blob store. + /// + /// The implementation must recursively make sure the same operations are applied to any + /// block state components, the value may be composed of. + /// + /// # Arguments + /// + /// - `from_loader`: loader for the blob store that the value is currently stored in + /// (the blob store we migrate from) + /// - `to_loader`: storer for the blob store that we migrate to + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized; +} + +impl Migrate for StoreSerialized { + fn migrate( + &self, + _from_loader: &impl BlobStoreLoad, + _to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult { + Ok(self.clone()) + } +} diff --git a/plt/plt-block-state/src/block_state/smart_contract_trie.rs b/plt/plt-block-state/src/block_state/smart_contract_trie.rs index 64fed75d2d..44262b709b 100644 --- a/plt/plt-block-state/src/block_state/smart_contract_trie.rs +++ b/plt/plt-block-state/src/block_state/smart_contract_trie.rs @@ -9,6 +9,7 @@ use crate::block_state::blob_store::{ }; use crate::block_state::cacheable::Cacheable; use crate::block_state::hash::Hashable; +use crate::block_state::migration::Migrate; use crate::block_state_interface::{BlockStateFailure, BlockStateResult}; use concordium_base::common::Buffer; use concordium_base::hashes::Hash; @@ -71,6 +72,32 @@ impl PersistentState { } } +impl Migrate for PersistentState { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_persistent_state = self + .lock_write() + .migrate( + &mut StorerAdapter(to_storer), + &mut LoaderAdapter(from_loader), + ) + .map_err(|err| { + BlockStateFailure::BlobStoreDecode(format!( + "Error migrating PersistentState: {}", + err + )) + })?; + + Ok(Self(RwLock::new(new_persistent_state))) + } +} + struct PrefixIterator<'a, L> { loader: &'a L, trie: trie::MutableTrie, @@ -265,6 +292,7 @@ mod test { use crate::block_state::blob_store; use crate::block_state::blob_store::test_stub::{BlobStoreStub, UnreachableBlobStore}; use crate::block_state::cacheable::Cacheable; + use crate::block_state::migration::Migrate; use crate::block_state::smart_contract_trie::PersistentState; #[test] @@ -443,4 +471,47 @@ mod test { Some(vec![2, 2]) ); } + + #[test] + fn test_migrate() { + let mut from_store = BlobStoreStub::default(); + let mut to_store = BlobStoreStub::default(); + + // Create tree and store it + let state = PersistentState::empty(); + let mut mutable_state = state.thaw(); + mutable_state + .insert_value(&from_store, &[0, 1], vec![1, 1]) + .unwrap(); + mutable_state + .insert_value(&from_store, &[0, 2], vec![2, 2]) + .unwrap(); + let state = mutable_state.freeze(&from_store); + blob_store::store_to_store(&mut from_store, &state); + + // Migrate trie to new store + let new_state = state.migrate(&from_store, &mut to_store).unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut to_store, &state); + drop(state); + + // Lookup values in migrated state + assert_eq!(new_state.lookup_value(&to_store, &[0, 1]), Some(vec![1, 1])); + assert_eq!(new_state.lookup_value(&to_store, &[0, 2]), Some(vec![2, 2])); + drop(new_state); + + // Load migrate state from destination store + let new_state2: PersistentState = + blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + + // Lookup values using "unreachable" blob store since entries + // should be in memory now. + assert_eq!( + new_state2.lookup_value(&to_store, &[0, 1]), + Some(vec![1, 1]) + ); + assert_eq!( + new_state2.lookup_value(&to_store, &[0, 2]), + Some(vec![2, 2]) + ); + } } diff --git a/plt/plt-block-state/src/block_state/state/protocol_level_tokens.rs b/plt/plt-block-state/src/block_state/state/protocol_level_tokens.rs index 605c41fa5d..224ccb1bd2 100644 --- a/plt/plt-block-state/src/block_state/state/protocol_level_tokens.rs +++ b/plt/plt-block-state/src/block_state/state/protocol_level_tokens.rs @@ -7,6 +7,7 @@ use crate::block_state::blob_store::{ use crate::block_state::cacheable::Cacheable; use crate::block_state::hash::Hashable; use crate::block_state::lfmb_tree::{LfmbTree, LfmbTreeKey}; +use crate::block_state::migration::Migrate; use crate::block_state::types::protocol_level_tokens::{TokenConfiguration, TokenIndex}; use crate::block_state::utils::OwnedOrBorrowed; use crate::block_state::{hash, smart_contract_trie}; @@ -206,6 +207,25 @@ impl Loadable for ProtocolLevelTokens { } } +impl Migrate for ProtocolLevelTokens { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_tokens = self.tokens.migrate(from_loader, to_storer)?; + let new_token_id_map = self.token_id_map.clone(); + + Ok(Self { + tokens: new_tokens, + token_id_map: new_token_id_map, + }) + } +} + impl Cacheable for ProtocolLevelTokens { fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { self.tokens.cache_reference_values(loader) @@ -260,6 +280,27 @@ impl Loadable for Token { } } +impl Migrate for Token { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized, + { + let new_configuration = self.configuration.migrate(from_loader, to_storer)?; + let new_key_value_state = self.key_value_state.migrate(from_loader, to_storer)?; + let new_circulating_supply = self.circulating_supply.migrate(from_loader, to_storer)?; + + Ok(Self { + configuration: new_configuration, + key_value_state: new_key_value_state, + circulating_supply: new_circulating_supply, + }) + } +} + impl Cacheable for Token { fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { self.configuration.cache_reference_values(loader)?; diff --git a/plt/plt-block-state/src/ffi/block_state.rs b/plt/plt-block-state/src/ffi/block_state.rs index a390879979..058aba0c26 100644 --- a/plt/plt-block-state/src/ffi/block_state.rs +++ b/plt/plt-block-state/src/ffi/block_state.rs @@ -236,7 +236,7 @@ extern "C" fn ffi_store_plt_block_state( #[unsafe(no_mangle)] extern "C" fn ffi_migrate_plt_block_state( from_load_callback: LoadCallback, - to_store_callback: StoreCallback, + mut to_store_callback: StoreCallback, to_protocol_version: u64, new_block_state_out: *mut *mut BlockState, block_state: *const BlockState, @@ -250,8 +250,13 @@ extern "C" fn ffi_migrate_plt_block_state( let from_block_state = unsafe { &*block_state }; let to_protocol_version = ProtocolVersion::try_from(to_protocol_version).expect("Unknown protocol version"); - let new_block_state = - from_block_state.migrate(from_load_callback, to_store_callback, to_protocol_version); + let new_block_state = from_block_state + .migrate( + &from_load_callback, + &mut to_store_callback, + to_protocol_version, + ) + .expect("Failed migrating block state"); unsafe { *new_block_state_out = Box::into_raw(Box::new(new_block_state)); } diff --git a/plt/plt-block-state/tests/block_state.rs b/plt/plt-block-state/tests/block_state.rs index 6efea9edd1..22da306206 100644 --- a/plt/plt-block-state/tests/block_state.rs +++ b/plt/plt-block-state/tests/block_state.rs @@ -313,6 +313,122 @@ fn test_store_and_load_plts() { assert_eq!(block_state.token_configuration(&token2), configuration2); } +/// Migrate PLTs from one blob store to another. +#[test] +fn test_migrate_plts() { + let mut block_state = block_state_no_external::new_mutable_block_state(ProtocolVersion::P10); + + // Create tokens + let configuration1 = TokenConfiguration { + token_id: "token1".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 2, + }; + let token1 = block_state.create_token(configuration1.clone()); + block_state.set_token_circulating_supply(&token1, RawTokenAmount(100)); + let mut key_value_state1 = block_state.mutable_token_key_value_state(&token1); + block_state.update_token_state_value( + &mut key_value_state1, + &TokenStateKey(vec![0, 1]), + Some(TokenStateValue(vec![0, 0])), + ); + block_state.update_token_state_value( + &mut key_value_state1, + &TokenStateKey(vec![0, 2]), + Some(TokenStateValue(vec![1, 1])), + ); + block_state.set_token_key_value_state(&token1, key_value_state1); + let configuration2 = TokenConfiguration { + token_id: "token2".parse().unwrap(), + module_ref: TokenModuleRef::from([5; 32]), + decimals: 4, + }; + block_state.create_token(configuration2.clone()); + + // Migrate block state + let mut new_store = BlobStoreStub::default(); + let new_immutable_state = block_state + .internal_block_state + .clone() + .into_immutable() + .migrate( + &block_state.blob_store_load, + &mut new_store, + ProtocolVersion::P11, + ) + .unwrap(); + let new_blob_loc = blob_store::store_to_store(&mut new_store, &new_immutable_state); + let new_block_state = + block_state_no_external::with_block_state(new_store.clone(), new_immutable_state); + drop(block_state); + + // Assert migrated state + let token1 = new_block_state + .token_by_id(&"token1".parse().unwrap()) + .unwrap(); + assert_eq!(new_block_state.plt_list().len(), 2); + assert_eq!( + new_block_state.token_circulating_supply(&token1), + RawTokenAmount(100) + ); + assert_eq!(new_block_state.token_configuration(&token1), configuration1); + let key_value_state1 = new_block_state.mutable_token_key_value_state(&token1); + let value = + new_block_state.lookup_token_state_value(&key_value_state1, &TokenStateKey(vec![0, 1])); + assert_eq!(value, Some(TokenStateValue(vec![0, 0]))); + let value = + new_block_state.lookup_token_state_value(&key_value_state1, &TokenStateKey(vec![0, 2])); + assert_eq!(value, Some(TokenStateValue(vec![1, 1]))); + let token2 = new_block_state + .token_by_id(&"token2".parse().unwrap()) + .unwrap(); + assert_eq!( + new_block_state.token_circulating_supply(&token2), + RawTokenAmount(0) + ); + assert_eq!(new_block_state.token_configuration(&token2), configuration2); + drop(new_block_state); + + // Load migrated block state + let new_immutable_state2 = + BlockState::load_from_store(&new_store, new_blob_loc, ProtocolVersion::P11) + .expect("load block state"); + let new_block_state2 = + block_state_no_external::with_block_state(new_store, new_immutable_state2); + + // Assert loaded state + let token1 = new_block_state2 + .token_by_id(&"token1".parse().unwrap()) + .unwrap(); + assert_eq!(new_block_state2.plt_list().len(), 2); + assert_eq!( + new_block_state2.token_circulating_supply(&token1), + RawTokenAmount(100) + ); + assert_eq!( + new_block_state2.token_configuration(&token1), + configuration1 + ); + let key_value_state1 = new_block_state2.mutable_token_key_value_state(&token1); + let value = + new_block_state2.lookup_token_state_value(&key_value_state1, &TokenStateKey(vec![0, 1])); + assert_eq!(value, Some(TokenStateValue(vec![0, 0]))); + let value = + new_block_state2.lookup_token_state_value(&key_value_state1, &TokenStateKey(vec![0, 2])); + assert_eq!(value, Some(TokenStateValue(vec![1, 1]))); + let token2 = new_block_state2 + .token_by_id(&"token2".parse().unwrap()) + .unwrap(); + assert_eq!( + new_block_state2.token_circulating_supply(&token2), + RawTokenAmount(0) + ); + assert_eq!( + new_block_state2.token_configuration(&token2), + configuration2 + ); +} + /// Assert that hash of an empty block state matches a fixed/snapshot hash. The hash /// must remain stable. #[test]