From 5034e2e925ce1cedbbdbc662edfb167d355484be Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Mon, 20 Apr 2026 09:00:19 +0200 Subject: [PATCH 1/7] Migration of block state values --- plt/plt-block-state/src/block_state.rs | 1 + .../hashed_cacheable_reference.rs | 19 +++++++ .../src/block_state/migration.rs | 54 +++++++++++++++++++ 3 files changed, 74 insertions(+) create mode 100644 plt/plt-block-state/src/block_state/migration.rs diff --git a/plt/plt-block-state/src/block_state.rs b/plt/plt-block-state/src/block_state.rs index 25b602601a..2f7d54f50a 100644 --- a/plt/plt-block-state/src/block_state.rs +++ b/plt/plt-block-state/src/block_state.rs @@ -30,6 +30,7 @@ pub mod cacheable; pub mod external; pub mod hash; mod lfmb_tree; +pub mod migration; mod state; pub mod types; mod utils; 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 779f48b0fd..b8e78c68f1 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::{LockRef, OwnedOrBorrowed}; use crate::block_state_interface::BlockStateResult; use concordium_base::common::{Buffer, Get, Put}; @@ -265,6 +266,24 @@ impl Hashable for HashedCacheableRef { } } +impl Migrate for HashedCacheableRef { + fn migrate( + &self, + from_loader: &impl BlobStoreLoad, + to_storer: &mut impl BlobStoreLoad, + ) -> BlockStateResult + where + Self: Sized, + { + let migrated_value = + self.with_value(from_loader, |value| value.migrate(from_loader, to_storer))?; + let new_hcr = Self::new(migrated_value); + let mut new_inner = new_hcr.inner.write(); + new_inner.repr.get_reference_or_store(to_storer); + Ok(new_hcr) + } +} + #[cfg(test)] mod tests { use super::*; 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..33a2829ccb --- /dev/null +++ b/plt/plt-block-state/src/block_state/migration.rs @@ -0,0 +1,54 @@ +//! 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. + +use crate::block_state::blob_store::{BlobStoreLoad, Storable, StoreSerialized}; +use crate::block_state_interface::BlockStateResult; +use concordium_base::common::{Put, Serial}; +use concordium_base::hashes::Hash; +use sha2::Digest; + +/// Trait implemented by block state values to support migration when protocol version increments. +/// Migration must +/// +/// 1. copy the value from the blob store of the current protocol version to the blob store +/// of the next protocol version +/// 2. apply any changes to the representation of the block state value (data model migration) +/// +/// Step 1. must always be performed, even if there are no changes to the representation of the +/// value in the blob store. +pub trait Migrate { + /// Migrate the value from the blob store it is currently stored in + /// (`from_loader`) to the blob store for the next protocol version (`to_storer`). + /// Returns a copy of the value, that is stored in the destination blob store. + /// The migration must + /// + /// 1. load any needed data that is not already in memory via `from_loader` + /// 2. make a copy of the value, while applying changes to the representation of the value + /// if needed by the protocol update + /// 3. store the new value to the destination 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 BlobStoreLoad, + ) -> BlockStateResult where Self:Sized; +} + +impl Migrate for StoreSerialized { + fn migrate( + &self, + _from_loader: &impl BlobStoreLoad, + _to_storer: &mut impl BlobStoreLoad, + ) -> BlockStateResult { + Ok(self.clone()) + } +} From cd18035963c4c860e9d3aa9aea5863932e797808 Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Mon, 20 Apr 2026 09:17:24 +0200 Subject: [PATCH 2/7] Migration of block state values --- plt/plt-block-state/src/block_state.rs | 22 ++++++--- .../hashed_cacheable_reference.rs | 5 +- .../src/block_state/lfmb_tree.rs | 47 +++++++++++++++++++ .../src/block_state/migration.rs | 18 +++---- .../state/protocol_level_tokens.rs | 41 ++++++++++++++++ plt/plt-block-state/src/ffi/block_state.rs | 5 +- 6 files changed, 120 insertions(+), 18 deletions(-) diff --git a/plt/plt-block-state/src/block_state.rs b/plt/plt-block-state/src/block_state.rs index 2f7d54f50a..630cfcbcd8 100644 --- a/plt/plt-block-state/src/block_state.rs +++ b/plt/plt-block-state/src/block_state.rs @@ -4,6 +4,7 @@ use crate::block_state::blob_store::{BlobStoreLoad, BlobStoreStore, Loadable, St 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, SimplisticTokenKeyValueState, }; @@ -60,12 +61,6 @@ impl BlockState { pub fn into_mutable(self) -> MutableBlockState { MutableBlockState::new(self) } - - /// Migrate the PLT block state from one blob store to another. - pub fn migrate(&self, _loader: &impl BlobStoreLoad, _storer: &mut impl BlobStoreStore) -> Self { - // todo implement as part of https://linear.app/concordium/issue/PSR-67/implement-p10-to-p11-migration-for-plt-state - todo!() - } } impl Loadable for BlockState { @@ -85,6 +80,21 @@ impl Storable for BlockState { } } +impl Migrate for BlockState { + 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)?; + + Ok(Self { tokens: new_tokens }) + } +} + impl Cacheable for BlockState { fn cache_reference_values(&self, loader: &impl BlobStoreLoad) -> BlockStateResult<()> { self.tokens.cache_reference_values(loader)?; 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 b8e78c68f1..5e54b7d653 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 @@ -266,11 +266,11 @@ impl Hashable for HashedCacheableRef { } } -impl Migrate for HashedCacheableRef { +impl Migrate for HashedCacheableRef { fn migrate( &self, from_loader: &impl BlobStoreLoad, - to_storer: &mut impl BlobStoreLoad, + to_storer: &mut impl BlobStoreStore, ) -> BlockStateResult where Self: Sized, @@ -280,6 +280,7 @@ impl Migrate for HashedCacheableRef { let new_hcr = Self::new(migrated_value); let mut new_inner = new_hcr.inner.write(); new_inner.repr.get_reference_or_store(to_storer); + drop(new_inner); Ok(new_hcr) } } 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 e69fb43cbf..db573dc0c9 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}; @@ -870,6 +871,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::*; diff --git a/plt/plt-block-state/src/block_state/migration.rs b/plt/plt-block-state/src/block_state/migration.rs index 33a2829ccb..296089d411 100644 --- a/plt/plt-block-state/src/block_state/migration.rs +++ b/plt/plt-block-state/src/block_state/migration.rs @@ -2,17 +2,15 @@ //! from the blob store of the current protocol version blob store to the blob store of the next //! protocol version. -use crate::block_state::blob_store::{BlobStoreLoad, Storable, StoreSerialized}; +use crate::block_state::blob_store::{BlobStoreLoad, BlobStoreStore, StoreSerialized}; use crate::block_state_interface::BlockStateResult; -use concordium_base::common::{Put, Serial}; -use concordium_base::hashes::Hash; -use sha2::Digest; +use concordium_base::common::Serial; /// Trait implemented by block state values to support migration when protocol version increments. /// Migration must /// /// 1. copy the value from the blob store of the current protocol version to the blob store -/// of the next protocol version +/// of the next protocol version /// 2. apply any changes to the representation of the block state value (data model migration) /// /// Step 1. must always be performed, even if there are no changes to the representation of the @@ -25,7 +23,7 @@ pub trait Migrate { /// /// 1. load any needed data that is not already in memory via `from_loader` /// 2. make a copy of the value, while applying changes to the representation of the value - /// if needed by the protocol update + /// if needed by the protocol update /// 3. store the new value to the destination blob store /// /// The implementation must recursively make sure the same operations are applied to any @@ -39,15 +37,17 @@ pub trait Migrate { fn migrate( &self, from_loader: &impl BlobStoreLoad, - to_storer: &mut impl BlobStoreLoad, - ) -> BlockStateResult where Self:Sized; + to_storer: &mut impl BlobStoreStore, + ) -> BlockStateResult + where + Self: Sized; } impl Migrate for StoreSerialized { fn migrate( &self, _from_loader: &impl BlobStoreLoad, - _to_storer: &mut impl BlobStoreLoad, + _to_storer: &mut impl BlobStoreStore, ) -> BlockStateResult { Ok(self.clone()) } 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 a12bf95530..510d0a3be0 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 @@ -8,6 +8,7 @@ use crate::block_state::cacheable::Cacheable; use crate::block_state::hash; 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, TokenStateKey, TokenStateValue, }; @@ -215,6 +216,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) @@ -269,6 +289,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 1d6c0f301c..5c45b31e78 100644 --- a/plt/plt-block-state/src/ffi/block_state.rs +++ b/plt/plt-block-state/src/ffi/block_state.rs @@ -6,6 +6,7 @@ use super::status; use crate::block_state::blob_store::BlobStoreLocation; use crate::block_state::cacheable::Cacheable; use crate::block_state::hash::Hashable; +use crate::block_state::migration::Migrate; use crate::block_state::{BlockState, blob_store}; use crate::ffi::blob_store_callbacks::{LoadCallback, StoreCallback}; @@ -181,7 +182,9 @@ extern "C" fn ffi_migrate_plt_block_state( let panic_message = status::catch_unwind(move || { assert!(!block_state.is_null(), "block_state is a null pointer."); let block_state = unsafe { &*block_state }; - let new_block_state = block_state.migrate(&load_callback, &mut store_callback); + let new_block_state = block_state + .migrate(&load_callback, &mut store_callback) + .expect("Failed migrating block state"); unsafe { *destination = Box::into_raw(Box::new(new_block_state)); } From 4ac188c6cd0728bd1331037fdc0ab3eb55e336be Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Mon, 20 Apr 2026 09:57:51 +0200 Subject: [PATCH 3/7] Migration of block state values --- .../hashed_cacheable_reference.rs | 56 +++++++++++- .../src/block_state/blob_store.rs | 2 +- .../src/block_state/lfmb_tree.rs | 69 ++++++++++++--- plt/plt-block-state/tests/block_state.rs | 87 +++++++++++++++++++ 4 files changed, 198 insertions(+), 16 deletions(-) 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 5e54b7d653..7aee7acc04 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 @@ -337,6 +337,33 @@ mod tests { }); } + /// 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 val_ref = assert_matches!(&new_val.inner.read().repr, HashedCacheableRefRepr::Cache {reference,value} => { + assert_eq!(*value, StoreSerialized(1)); + *reference + }); + + // Store migrated reference and load it + let blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + let new_val2: TestRef = blob_store::load_from_store(&to_store, blob_loc).unwrap(); + assert_eq!(new_val2.with_value(&to_store, |val| Ok(val.0)).unwrap(), 1); + } + /// Test storing cached value. #[test] fn test_store_cached_value() { @@ -607,11 +634,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))); @@ -643,4 +670,29 @@ mod tests { assert_eq!(*reference, 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 and store it + 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); + + // Load migrated reference + let new_val2: NestedTestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + assert_eq!( + new_val2 + .with_value(&to_store, |val_ref| val_ref + .with_value(&to_store, |val| Ok(val.0))) + .unwrap(), + 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 db573dc0c9..30a1b36b2e 100644 --- a/plt/plt-block-state/src/block_state/lfmb_tree.rs +++ b/plt/plt-block-state/src/block_state/lfmb_tree.rs @@ -1111,7 +1111,42 @@ 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); + + // Load migrated tree + let new_tree2: TestTree = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); + + // Assert loaded tree is equal to the tree we started with + assert_trees_eq( + &from_store, + &to_store, + &tree, + &new_tree2, + format!("loaded tree of size {}", i), + ); } } @@ -1128,7 +1163,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. @@ -1242,7 +1283,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, @@ -1256,7 +1298,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); @@ -1266,15 +1308,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.clone_or_load_value(loader).unwrap(); - let val2 = val_ref2.clone_or_load_value(loader).unwrap(); + let val1 = val_ref1.clone_or_load_value(loader1).unwrap(); + let val2 = val_ref2.clone_or_load_value(loader2).unwrap(); assert_eq!(val1, val2, "{}: leaf value", context); } ( @@ -1282,12 +1325,12 @@ mod tests { Subtree::Node(height2, left_ref2, right_ref2), ) => { assert_eq!(height1, height2); - let left1 = left_ref1.clone_or_load_value(loader).unwrap(); - let right1 = right_ref1.clone_or_load_value(loader).unwrap(); - let left2 = left_ref2.clone_or_load_value(loader).unwrap(); - let right2 = right_ref2.clone_or_load_value(loader).unwrap(); - assert_subtrees_eq(loader, &left1, &left2, context.clone()); - assert_subtrees_eq(loader, &right1, &right2, context.clone()); + let left1 = left_ref1.clone_or_load_value(loader1).unwrap(); + let right1 = right_ref1.clone_or_load_value(loader1).unwrap(); + let left2 = left_ref2.clone_or_load_value(loader2).unwrap(); + let right2 = right_ref2.clone_or_load_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/tests/block_state.rs b/plt/plt-block-state/tests/block_state.rs index ee0a225e82..66e0d4635b 100644 --- a/plt/plt-block-state/tests/block_state.rs +++ b/plt/plt-block-state/tests/block_state.rs @@ -2,7 +2,9 @@ use concordium_base::base::ProtocolVersion; use concordium_base::protocol_level_tokens::{TokenId, TokenModuleRef}; +use plt_block_state::block_state::blob_store::test_stub::BlobStoreStub; use plt_block_state::block_state::hash::Hashable; +use plt_block_state::block_state::migration::Migrate; use plt_block_state::block_state::types::protocol_level_tokens::TokenConfiguration; use plt_block_state::block_state::{BlockState, blob_store}; use plt_block_state::block_state_interface::{BlockStateOperations, BlockStateQuery}; @@ -230,6 +232,91 @@ 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, &vec![0, 1], Some(vec![0, 0])); + block_state.update_token_state_value(&mut key_value_state1, &vec![0, 2], Some(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, + }; + let token2 = 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) + .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( + ProtocolVersion::P11, + new_store.clone(), + &new_immutable_state, + ); + + // Assert migrated state + assert_eq!(new_block_state.plt_list().len(), 2); + assert_eq!( + new_block_state.token_circulating_supply(&token1), + RawTokenAmount(100) + ); + assert_eq!(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, &vec![0, 1]); + assert_eq!(value, Some(vec![0, 0])); + let value = new_block_state.lookup_token_state_value(&key_value_state1, &vec![0, 2]); + assert_eq!(value, Some(vec![1, 1])); + assert_eq!( + new_block_state.token_circulating_supply(&token2), + RawTokenAmount(0) + ); + assert_eq!(new_block_state.token_configuration(&token2), configuration2); + + // Load migrated block state + let new_immutable_state2 = + blob_store::load_from_store::(&new_store, new_blob_loc) + .expect("load block state"); + let new_block_state2 = block_state_no_external::with_block_state( + ProtocolVersion::P11, + new_store, + &new_immutable_state2, + ); + + // Assert loaded state + assert_eq!(new_block_state2.plt_list().len(), 2); + assert_eq!( + new_block_state2.token_circulating_supply(&token1), + RawTokenAmount(100) + ); + assert_eq!(block_state.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, &vec![0, 1]); + assert_eq!(value, Some(vec![0, 0])); + let value = new_block_state2.lookup_token_state_value(&key_value_state1, &vec![0, 2]); + assert_eq!(value, Some(vec![1, 1])); + assert_eq!( + new_block_state2.token_circulating_supply(&token2), + RawTokenAmount(0) + ); + assert_eq!(new_block_state2.token_configuration(&token2), configuration2); +} + /// Assert hash block state with PLTs matches a fixed/snapshot hash. The hash /// must remain stable. /// From a6be27d19e0aa8d29aba5003a2f19ed1f75d16b4 Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Mon, 20 Apr 2026 10:04:34 +0200 Subject: [PATCH 4/7] Migration of block state values --- .../hashed_cacheable_reference.rs | 25 ++++++++++++----- .../src/block_state/lfmb_tree.rs | 9 +++++++ plt/plt-block-state/tests/block_state.rs | 27 ++++++++++++++----- 3 files changed, 48 insertions(+), 13 deletions(-) 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 7aee7acc04..3ba6192429 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 @@ -353,14 +353,16 @@ mod tests { // Migrate to destination blob store let new_val = val.migrate(&from_store, &mut to_store).unwrap(); - let val_ref = assert_matches!(&new_val.inner.read().repr, HashedCacheableRefRepr::Cache {reference,value} => { + let new_blob_loc = blob_store::store_to_store(&mut to_store, &new_val); + + // Assert migrated reference + assert_matches!(&new_val.inner.read().repr, HashedCacheableRefRepr::Cache {value, ..} => { assert_eq!(*value, StoreSerialized(1)); - *reference }); + assert_eq!(new_val.with_value(&to_store, |val| Ok(val.0)).unwrap(), 1); - // Store migrated reference and load it - let blob_loc = blob_store::store_to_store(&mut to_store, &new_val); - let new_val2: TestRef = blob_store::load_from_store(&to_store, blob_loc).unwrap(); + // Load migrate reference and assert + let new_val2: TestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); assert_eq!(new_val2.with_value(&to_store, |val| Ok(val.0)).unwrap(), 1); } @@ -681,11 +683,20 @@ mod tests { let val = HashedCacheableRef::new(HashedCacheableRef::new(StoreSerialized(1u64))); blob_store::store_to_store(&mut from_store, &val); - // Migrate to destination blob store and store it + // 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); - // Load migrated reference + // Assert migrated reference + assert_eq!( + new_val + .with_value(&to_store, |val_ref| val_ref + .with_value(&to_store, |val| Ok(val.0))) + .unwrap(), + 1 + ); + + // Load migrated reference and assert let new_val2: NestedTestRef = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); assert_eq!( new_val2 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 30a1b36b2e..337ed96f8a 100644 --- a/plt/plt-block-state/src/block_state/lfmb_tree.rs +++ b/plt/plt-block-state/src/block_state/lfmb_tree.rs @@ -1136,6 +1136,15 @@ mod tests { 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), + ); + // Load migrated tree let new_tree2: TestTree = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); diff --git a/plt/plt-block-state/tests/block_state.rs b/plt/plt-block-state/tests/block_state.rs index 66e0d4635b..2fad7ae63d 100644 --- a/plt/plt-block-state/tests/block_state.rs +++ b/plt/plt-block-state/tests/block_state.rs @@ -254,12 +254,13 @@ fn test_migrate_plts() { module_ref: TokenModuleRef::from([5; 32]), decimals: 4, }; - let token2 = block_state.create_token(configuration2.clone()); + 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() + .internal_block_state + .clone() .into_immutable() .migrate(&block_state.blob_store_load, &mut new_store) .unwrap(); @@ -271,6 +272,9 @@ fn test_migrate_plts() { ); // 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), @@ -282,6 +286,9 @@ fn test_migrate_plts() { assert_eq!(value, Some(vec![0, 0])); let value = new_block_state.lookup_token_state_value(&key_value_state1, &vec![0, 2]); assert_eq!(value, Some(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) @@ -289,9 +296,8 @@ fn test_migrate_plts() { assert_eq!(new_block_state.token_configuration(&token2), configuration2); // Load migrated block state - let new_immutable_state2 = - blob_store::load_from_store::(&new_store, new_blob_loc) - .expect("load block state"); + let new_immutable_state2 = blob_store::load_from_store::(&new_store, new_blob_loc) + .expect("load block state"); let new_block_state2 = block_state_no_external::with_block_state( ProtocolVersion::P11, new_store, @@ -299,6 +305,9 @@ fn test_migrate_plts() { ); // 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), @@ -310,11 +319,17 @@ fn test_migrate_plts() { assert_eq!(value, Some(vec![0, 0])); let value = new_block_state2.lookup_token_state_value(&key_value_state1, &vec![0, 2]); assert_eq!(value, Some(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_eq!( + new_block_state2.token_configuration(&token2), + configuration2 + ); } /// Assert hash block state with PLTs matches a fixed/snapshot hash. The hash From 2c671cc5540fcd7c7e0b51a0c88f1424e113cc17 Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Tue, 21 Apr 2026 14:00:03 +0200 Subject: [PATCH 5/7] Block state migration --- .../src/block_state/blob_reference.rs | 16 +++-- .../hashed_cacheable_reference.rs | 2 +- .../src/block_state/lfmb_tree.rs | 4 +- .../src/block_state/migration.rs | 29 +++++----- .../src/block_state/smart_contract_trie.rs | 58 ++++++++++++++++++- 5 files changed, 86 insertions(+), 23 deletions(-) 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 9e2f110cde..c7aef1233f 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 @@ -379,7 +379,7 @@ mod tests { assert_cached_repr(&new_val); assert_eq!(*new_val.value(&to_store).unwrap(), StoreSerialized(1)); - // Load migrate reference and assert + // 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)); } 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 eb6f1a4121..e33c4ffa17 100644 --- a/plt/plt-block-state/src/block_state/lfmb_tree.rs +++ b/plt/plt-block-state/src/block_state/lfmb_tree.rs @@ -1148,10 +1148,10 @@ mod tests { format!("loaded tree of size {}", i), ); - // Load migrated tree + // Load migrated tree from destination store let new_tree2: TestTree = blob_store::load_from_store(&to_store, new_blob_loc).unwrap(); - // Assert loaded tree is equal to the tree we started with + // Assert tree loaded from destination store is equal to the tree we started with assert_trees_eq( &from_store, &to_store, diff --git a/plt/plt-block-state/src/block_state/migration.rs b/plt/plt-block-state/src/block_state/migration.rs index 296089d411..abfce03296 100644 --- a/plt/plt-block-state/src/block_state/migration.rs +++ b/plt/plt-block-state/src/block_state/migration.rs @@ -1,30 +1,31 @@ //! 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. +//! 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. -/// Migration must +/// The migration must, /// -/// 1. copy the value from the blob store of the current protocol version to the blob store -/// of the next protocol version -/// 2. apply any changes to the representation of the block state value (data model migration) +/// * 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) /// -/// Step 1. must always be performed, even if there are no changes to the representation of the -/// value in the blob store. +/// 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 blob store for the next protocol version (`to_storer`). - /// Returns a copy of the value, that is stored in the destination blob store. - /// The migration must + /// (`from_loader`), to the new blob store for the next protocol version (`to_storer`). + /// Migration must: /// - /// 1. load any needed data that is not already in memory via `from_loader` - /// 2. make a copy of the value, while applying changes to the representation of the value - /// if needed by the protocol update - /// 3. store the new value to the destination blob store + /// * 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. 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 ecd15c493d..415b175184 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 @@ -81,8 +81,20 @@ impl Migrate for PersistentState { where Self: Sized, { - // todo ar - todo!() + 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))) } } @@ -280,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] @@ -458,4 +471,45 @@ 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); + + // 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])); + + // 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]) + ); + } } From 13ca8279a104d951e37bba6eda1d69f0ca44f90c Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Tue, 21 Apr 2026 14:07:27 +0200 Subject: [PATCH 6/7] Block state migration --- .../blob_reference/hashed_cacheable_reference.rs | 4 ++++ plt/plt-block-state/src/block_state/lfmb_tree.rs | 1 + .../src/block_state/smart_contract_trie.rs | 2 ++ plt/plt-block-state/tests/block_state.rs | 9 +++++++-- 4 files changed, 14 insertions(+), 2 deletions(-) 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 c7aef1233f..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 @@ -374,10 +374,12 @@ mod tests { // 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(); @@ -600,12 +602,14 @@ mod tests { // 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(); 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 e33c4ffa17..5278f72e1e 100644 --- a/plt/plt-block-state/src/block_state/lfmb_tree.rs +++ b/plt/plt-block-state/src/block_state/lfmb_tree.rs @@ -1147,6 +1147,7 @@ mod tests { &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(); 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 415b175184..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 @@ -492,10 +492,12 @@ mod test { // 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 = diff --git a/plt/plt-block-state/tests/block_state.rs b/plt/plt-block-state/tests/block_state.rs index dd03f0a0c1..259fc0cf16 100644 --- a/plt/plt-block-state/tests/block_state.rs +++ b/plt/plt-block-state/tests/block_state.rs @@ -363,6 +363,7 @@ fn test_migrate_plts() { new_store.clone(), &new_immutable_state, ); + drop(block_state); // Assert migrated state let token1 = new_block_state @@ -373,7 +374,7 @@ fn test_migrate_plts() { new_block_state.token_circulating_supply(&token1), RawTokenAmount(100) ); - assert_eq!(block_state.token_configuration(&token1), configuration1); + 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])); @@ -389,6 +390,7 @@ fn test_migrate_plts() { RawTokenAmount(0) ); assert_eq!(new_block_state.token_configuration(&token2), configuration2); + drop(new_block_state); // Load migrated block state let new_immutable_state2 = blob_store::load_from_store::(&new_store, new_blob_loc) @@ -408,7 +410,10 @@ fn test_migrate_plts() { new_block_state2.token_circulating_supply(&token1), RawTokenAmount(100) ); - assert_eq!(block_state.token_configuration(&token1), configuration1); + 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])); From 0aff6974f1be9f17a98170155e2e06de8c679e14 Mon Sep 17 00:00:00 2001 From: Allan Rasmussen Date: Thu, 23 Apr 2026 11:17:29 +0200 Subject: [PATCH 7/7] doc --- plt/plt-block-state/src/block_state.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plt/plt-block-state/src/block_state.rs b/plt/plt-block-state/src/block_state.rs index 288747952d..533a2696d7 100644 --- a/plt/plt-block-state/src/block_state.rs +++ b/plt/plt-block-state/src/block_state.rs @@ -116,7 +116,8 @@ impl BlockState { }) } - /// See [`Migrate::migrate`] + /// See [`Migrate::migrate`]. This function only differs by taking + /// protocol version as argument. pub fn migrate( &self, from_loader: &impl BlobStoreLoad,