From a0813f9e18ed36aceeabb242dd5f96a1579c7a9e Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 31 Jan 2024 12:07:08 +0000 Subject: [PATCH] Add `integration-test` for possible migration pattern (#1909) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * WIP * Update versions * WIP * WIP migration * WIP * Make test pass * Move e2e tests mod to own file * Update comment * Update example for new e2e API * Update integration-tests/upgradeable-contracts/set-code-hash-migration/lib.rs Co-authored-by: Michael Müller * Top level gitignore * Fix tests update comments * Update upgradeable contracts README.md * spelling --------- Co-authored-by: Michael Müller --- .../upgradeable-contracts/.gitignore | 2 + .../upgradeable-contracts/README.md | 15 +++ .../set-code-hash-migration/Cargo.toml | 28 ++++++ .../set-code-hash-migration/e2e_tests.rs | 94 +++++++++++++++++++ .../set-code-hash-migration/lib.rs | 69 ++++++++++++++ .../migration/Cargo.toml | 19 ++++ .../set-code-hash-migration/migration/lib.rs | 70 ++++++++++++++ .../updated-incrementer/Cargo.toml | 19 ++++ .../updated-incrementer/lib.rs | 75 +++++++++++++++ 9 files changed, 391 insertions(+) create mode 100644 integration-tests/upgradeable-contracts/.gitignore create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/lib.rs create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/migration/lib.rs create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml create mode 100644 integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs diff --git a/integration-tests/upgradeable-contracts/.gitignore b/integration-tests/upgradeable-contracts/.gitignore new file mode 100644 index 0000000000..ff75a1d9fa --- /dev/null +++ b/integration-tests/upgradeable-contracts/.gitignore @@ -0,0 +1,2 @@ +**/target/ +Cargo.lock \ No newline at end of file diff --git a/integration-tests/upgradeable-contracts/README.md b/integration-tests/upgradeable-contracts/README.md index 85bd7b5c81..74a25d764c 100644 --- a/integration-tests/upgradeable-contracts/README.md +++ b/integration-tests/upgradeable-contracts/README.md @@ -12,6 +12,21 @@ This is exactly what `set_code_hash()` function does. However, developers needs to be mindful of storage compatibility. You can read more about storage compatibility on [use.ink](https://use.ink/basics/upgradeable-contracts#replacing-contract-code-with-set_code_hash) +## [`set-code-hash`](set-code-hash-migration/) + +When upgrading a contract, the new code may have a different storage layout. This example illustrates a method to +migrate the storage from the old layout to the new layout. It does so by using an intermediate `migration` contract +which performs the storage upgrade. The workflow is as follows: + + +1. Upload a `migration` contract with a message `migrate` which performs the storage migration. +2. Set code hash to the `migration` contract. +3. Upload the upgraded version of the original contract. +4. Call `migrate` on the `migration` contract, passing the code hash of the new updated incrementer contract from `3.` +This must happen as a single message, because following the storage migration, the contract will not be able to be +called again, since it will fail to load the migrated storage. + + ## [Delegator](delegator/) Delegator patter is based around a low level cross contract call function `delegate_call`. diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/Cargo.toml b/integration-tests/upgradeable-contracts/set-code-hash-migration/Cargo.toml new file mode 100644 index 0000000000..31a6b2cb2b --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "incrementer" +version = "5.0.0-alpha" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../crates/ink", default-features = false } + +migration = { path = "./migration", default-features = false, features = ["ink-as-dependency"] } +updated-incrementer = { path = "./updated-incrementer", default-features = false, features = ["ink-as-dependency"] } + +[dev-dependencies] +ink_e2e = { path = "../../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "migration/std", + "updated-incrementer/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs b/integration-tests/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs new file mode 100644 index 0000000000..dbbd029a8b --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/e2e_tests.rs @@ -0,0 +1,94 @@ +use super::incrementer::*; +use ink_e2e::ContractsBackend; + +type E2EResult = std::result::Result>; + +#[ink_e2e::test] +async fn migration_works(mut client: Client) -> E2EResult<()> { + // Given + let mut constructor = IncrementerRef::new(); + let contract = client + .instantiate("incrementer", &ink_e2e::alice(), &mut constructor) + .submit() + .await + .expect("instantiate failed"); + let mut call_builder = contract.call_builder::(); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?; + assert_eq!(get_res.return_value(), 0); + + let inc = call_builder.inc(); + let _inc_result = client + .call(&ink_e2e::alice(), &inc) + .submit() + .await + .expect("`inc` failed"); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?; + let pre_migration_value = get_res.return_value(); + assert_eq!(pre_migration_value, 1); + + // Upload the code for the contract to be updated to after the migration. + let new_code_hash = client + .upload("updated-incrementer", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `updated-incrementer` failed") + .code_hash; + let new_code_hash = new_code_hash.as_ref().try_into().unwrap(); + + // Upload the code for the migration contract. + let migration_contract = client + .upload("migration", &ink_e2e::alice()) + .submit() + .await + .expect("uploading `migration` failed"); + let migration_code_hash = migration_contract.code_hash.as_ref().try_into().unwrap(); + + // When + + // Set the code hash to the migration contract + let set_code = call_builder.set_code(migration_code_hash); + let _set_code_result = client + .call(&ink_e2e::alice(), &set_code) + .submit() + .await + .expect("`set_code` failed"); + + // Call the migration contract with a new value for `inc_by` and the code hash + // of the updated contract. + const NEW_INC_BY: u8 = 4; + let migrate = contract + .call_builder::() + .migrate(NEW_INC_BY, new_code_hash); + + let _migration_result = client + .call(&ink_e2e::alice(), &migrate) + .submit() + .await + .expect("`migrate` failed"); + + // Then + let inc = contract + .call_builder::() + .inc(); + + let _inc_result = client + .call(&ink_e2e::alice(), &inc) + .submit() + .await + .expect("`inc` failed"); + + let get = call_builder.get(); + let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?; + + // Remember, we updated our incrementer contract to increment by `4`. + assert_eq!( + get_res.return_value(), + pre_migration_value + NEW_INC_BY as u32 + ); + + Ok(()) +} diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/lib.rs b/integration-tests/upgradeable-contracts/set-code-hash-migration/lib.rs new file mode 100644 index 0000000000..ea67cf199a --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/lib.rs @@ -0,0 +1,69 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +//! Demonstrates how to use [`set_code_hash`](https://docs.rs/ink_env/latest/ink_env/fn.set_code_hash.html) +//! to swap out the `code_hash` of an on-chain contract. +//! +//! We will swap the code of our `Incrementer` contract with that of the `Incrementer` +//! found in the `updated_incrementer` folder. +//! +//! See the included End-to-End tests an example update workflow. + +#[ink::contract] +pub mod incrementer { + /// Track a counter in storage. + /// + /// # Note + /// + /// Is is important to realize that after the call to `set_code_hash` the contract's + /// storage remains the same. + /// + /// If you change the storage layout in your storage struct you may introduce + /// undefined behavior to your contract! + #[ink(storage)] + #[derive(Default)] + pub struct Incrementer { + count: u32, + } + + impl Incrementer { + /// Creates a new counter smart contract initialized with the given base value. + #[ink(constructor)] + pub fn new() -> Self { + Default::default() + } + + /// Increments the counter value which is stored in the contract's storage. + #[ink(message)] + pub fn inc(&mut self) { + self.count = self.count.checked_add(1).unwrap(); + ink::env::debug_println!( + "The new count is {}, it was modified using the original contract code.", + self.count + ); + } + + /// Returns the counter value which is stored in this contract's storage. + #[ink(message)] + pub fn get(&self) -> u32 { + self.count + } + + /// Modifies the code which is used to execute calls to this contract address + /// (`AccountId`). + /// + /// We use this to upgrade the contract logic. We don't do any authorization here, + /// any caller can execute this method. + /// + /// In a production contract you would do some authorization here! + #[ink(message)] + pub fn set_code(&mut self, code_hash: Hash) { + self.env().set_code_hash(&code_hash).unwrap_or_else(|err| { + panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}") + }); + ink::env::debug_println!("Switched code hash to {:?}.", code_hash); + } + } +} + +#[cfg(all(test, feature = "e2e-tests"))] +mod e2e_tests; diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml b/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml new file mode 100644 index 0000000000..af5cae8ab2 --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "migration" +version = "5.0.0-alpha" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../../crates/ink", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/lib.rs b/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/lib.rs new file mode 100644 index 0000000000..93d13cc6f1 --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/migration/lib.rs @@ -0,0 +1,70 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +pub mod incrementer { + + /// Storage struct matches exactly that of the original `incrementer` contract, from + /// which we are migrating. + #[ink(storage)] + pub struct Incrementer { + count: u32, + } + + #[ink::storage_item] + pub struct IncrementerNew { + count: u64, + inc_by: u8, + } + + impl Incrementer { + /// Creates a new counter smart contract initialized with the given base value. + /// + /// # Note + /// + /// When upgrading using the `set_code_hash` workflow we only need to point to a + /// contract's uploaded code hash, **not** an instantiated contract's + /// `AccountId`. + /// + /// Because of this we will never actually call the constructor of this contract. + #[ink(constructor)] + pub fn new() -> Self { + unreachable!( + "Constructors are not called when upgrading using `set_code_hash`." + ) + } + + /// Run the migration to the data layout for the upgraded contract. + /// Once the storage migration has successfully completed, the contract will be + /// upgraded to the supplied code hash. + /// + /// In a production contract you would do some authorization here! + /// + /// # Note + /// + /// This function necessarily accepts a `&self` instead of a `&mut self` because + /// we are modifying storage directly for the migration. + /// + /// The `self` in `&mut self` is the original `Incrementer` storage struct, and + /// would be implicitly written to storage following the function execution, + /// overwriting the migrated storage. + #[ink(message)] + pub fn migrate(&self, inc_by: u8, code_hash: Hash) { + let incrementer_new = IncrementerNew { + count: self.count as u64, + inc_by, + }; + + // overwrite the original storage struct with the migrated storage struct, + // which has a layout compatible with the new contract code. + const STORAGE_KEY: u32 = + ::KEY; + ink::env::set_contract_storage(&STORAGE_KEY, &incrementer_new); + + ink::env::set_code_hash::<::Env>(&code_hash) + .unwrap_or_else(|err| { + panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}") + }) + } + } +} diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml b/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml new file mode 100644 index 0000000000..49b24990c9 --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "updated-incrementer" +version = "5.0.0-alpha" +authors = ["Parity Technologies "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../../../crates/ink", default-features = false } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", +] +ink-as-dependency = [] diff --git a/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs b/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs new file mode 100644 index 0000000000..7ac05c80e3 --- /dev/null +++ b/integration-tests/upgradeable-contracts/set-code-hash-migration/updated-incrementer/lib.rs @@ -0,0 +1,75 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] +#![allow(clippy::new_without_default)] + +#[ink::contract] +pub mod incrementer { + + /// Track a counter in storage. + /// + /// # Note + /// + /// We have changed the storage layout: + /// - `count` is now a `u64` instead of a `u32`. + /// - We have added a new field `inc_by` which controls how many to increment by. + #[ink(storage)] + pub struct Incrementer { + count: u64, + inc_by: u8, + } + + impl Incrementer { + /// Creates a new counter smart contract initialized with the given base value. + /// + /// # Note + /// + /// When upgrading using the `set_code_hash` workflow we only need to point to a + /// contract's uploaded code hash, **not** an instantiated contract's + /// `AccountId`. + /// + /// Because of this we will never actually call the constructor of this contract. + #[ink(constructor)] + pub fn new() -> Self { + unreachable!( + "Constructors are not called when upgrading using `set_code_hash`." + ) + } + + /// Increments the counter value which is stored in the contract's storage. + /// + /// # Note + /// + /// In this upgraded contract the value is incremented by the value in the + /// `inc_by` field. + #[ink(message)] + pub fn inc(&mut self) { + self.count = self.count.checked_add(self.inc_by.into()).unwrap(); + } + + /// Set the value by which the counter will be incremented. + #[ink(message)] + pub fn set_inc_by(&mut self, inc_by: u8) { + self.inc_by = inc_by; + } + + /// Returns the counter value which is stored in this contract's storage. + #[ink(message)] + pub fn get(&self) -> u64 { + self.count + } + + /// Modifies the code which is used to execute calls to this contract address + /// (`AccountId`). + /// + /// We use this to upgrade the contract logic. We don't do any authorization here, + /// any caller can execute this method. + /// + /// In a production contract you would do some authorization here! + #[ink(message)] + pub fn set_code(&mut self, code_hash: Hash) { + self.env().set_code_hash(&code_hash).unwrap_or_else(|err| { + panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}") + }); + ink::env::debug_println!("Switched code hash to {:?}.", code_hash); + } + } +}