Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add integration-test for possible migration pattern #1909

Merged
merged 20 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions integration-tests/upgradeable-contracts/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
**/target/
Cargo.lock
15 changes: 15 additions & 0 deletions integration-tests/upgradeable-contracts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "incrementer"
version = "5.0.0-alpha"
authors = ["Parity Technologies <[email protected]>"]
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 = []
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
use super::incrementer::*;
use ink_e2e::ContractsBackend;

type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;

#[ink_e2e::test]
async fn migration_works<Client: E2EBackend>(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::<Incrementer>();

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::<migration::incrementer::Incrementer>()
.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::<updated_incrementer::incrementer::Incrementer>()
.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(())
}
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "migration"
version = "5.0.0-alpha"
authors = ["Parity Technologies <[email protected]>"]
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 = []
Original file line number Diff line number Diff line change
@@ -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 =
<Incrementer as ink::storage::traits::StorageKey>::KEY;
ink::env::set_contract_storage(&STORAGE_KEY, &incrementer_new);

ink::env::set_code_hash::<<Self as ink::env::ContractEnv>::Env>(&code_hash)
.unwrap_or_else(|err| {
panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}")
})
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
[package]
name = "updated-incrementer"
version = "5.0.0-alpha"
authors = ["Parity Technologies <[email protected]>"]
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 = []
Loading
Loading