Skip to content

Commit

Permalink
Add integration-test for possible migration pattern (#1909)
Browse files Browse the repository at this point in the history
* 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 <michi@parity.io>

* Top level gitignore

* Fix tests update comments

* Update upgradeable contracts README.md

* spelling

---------

Co-authored-by: Michael Müller <michi@parity.io>
  • Loading branch information
ascjones and Michael Müller authored Jan 31, 2024
1 parent 5674b4d commit 8808da0
Show file tree
Hide file tree
Showing 9 changed files with 391 additions and 0 deletions.
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 <admin@parity.io>"]
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 <admin@parity.io>"]
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 <admin@parity.io>"]
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

0 comments on commit 8808da0

Please sign in to comment.