Skip to content

Commit a0813f9

Browse files
ascjonesMichael Müller
and
Michael Müller
committed
Add integration-test for possible migration pattern (#1909)
* 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 <[email protected]> * Top level gitignore * Fix tests update comments * Update upgradeable contracts README.md * spelling --------- Co-authored-by: Michael Müller <[email protected]>
1 parent cc52b41 commit a0813f9

File tree

9 files changed

+391
-0
lines changed

9 files changed

+391
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
**/target/
2+
Cargo.lock

integration-tests/upgradeable-contracts/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,21 @@ This is exactly what `set_code_hash()` function does.
1212
However, developers needs to be mindful of storage compatibility.
1313
You can read more about storage compatibility on [use.ink](https://use.ink/basics/upgradeable-contracts#replacing-contract-code-with-set_code_hash)
1414

15+
## [`set-code-hash`](set-code-hash-migration/)
16+
17+
When upgrading a contract, the new code may have a different storage layout. This example illustrates a method to
18+
migrate the storage from the old layout to the new layout. It does so by using an intermediate `migration` contract
19+
which performs the storage upgrade. The workflow is as follows:
20+
21+
22+
1. Upload a `migration` contract with a message `migrate` which performs the storage migration.
23+
2. Set code hash to the `migration` contract.
24+
3. Upload the upgraded version of the original contract.
25+
4. Call `migrate` on the `migration` contract, passing the code hash of the new updated incrementer contract from `3.`
26+
This must happen as a single message, because following the storage migration, the contract will not be able to be
27+
called again, since it will fail to load the migrated storage.
28+
29+
1530
## [Delegator](delegator/)
1631

1732
Delegator patter is based around a low level cross contract call function `delegate_call`.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[package]
2+
name = "incrementer"
3+
version = "5.0.0-alpha"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
ink = { path = "../../../crates/ink", default-features = false }
10+
11+
migration = { path = "./migration", default-features = false, features = ["ink-as-dependency"] }
12+
updated-incrementer = { path = "./updated-incrementer", default-features = false, features = ["ink-as-dependency"] }
13+
14+
[dev-dependencies]
15+
ink_e2e = { path = "../../../crates/e2e" }
16+
17+
[lib]
18+
path = "lib.rs"
19+
20+
[features]
21+
default = ["std"]
22+
std = [
23+
"ink/std",
24+
"migration/std",
25+
"updated-incrementer/std",
26+
]
27+
ink-as-dependency = []
28+
e2e-tests = []
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use super::incrementer::*;
2+
use ink_e2e::ContractsBackend;
3+
4+
type E2EResult<T> = std::result::Result<T, Box<dyn std::error::Error>>;
5+
6+
#[ink_e2e::test]
7+
async fn migration_works<Client: E2EBackend>(mut client: Client) -> E2EResult<()> {
8+
// Given
9+
let mut constructor = IncrementerRef::new();
10+
let contract = client
11+
.instantiate("incrementer", &ink_e2e::alice(), &mut constructor)
12+
.submit()
13+
.await
14+
.expect("instantiate failed");
15+
let mut call_builder = contract.call_builder::<Incrementer>();
16+
17+
let get = call_builder.get();
18+
let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?;
19+
assert_eq!(get_res.return_value(), 0);
20+
21+
let inc = call_builder.inc();
22+
let _inc_result = client
23+
.call(&ink_e2e::alice(), &inc)
24+
.submit()
25+
.await
26+
.expect("`inc` failed");
27+
28+
let get = call_builder.get();
29+
let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?;
30+
let pre_migration_value = get_res.return_value();
31+
assert_eq!(pre_migration_value, 1);
32+
33+
// Upload the code for the contract to be updated to after the migration.
34+
let new_code_hash = client
35+
.upload("updated-incrementer", &ink_e2e::alice())
36+
.submit()
37+
.await
38+
.expect("uploading `updated-incrementer` failed")
39+
.code_hash;
40+
let new_code_hash = new_code_hash.as_ref().try_into().unwrap();
41+
42+
// Upload the code for the migration contract.
43+
let migration_contract = client
44+
.upload("migration", &ink_e2e::alice())
45+
.submit()
46+
.await
47+
.expect("uploading `migration` failed");
48+
let migration_code_hash = migration_contract.code_hash.as_ref().try_into().unwrap();
49+
50+
// When
51+
52+
// Set the code hash to the migration contract
53+
let set_code = call_builder.set_code(migration_code_hash);
54+
let _set_code_result = client
55+
.call(&ink_e2e::alice(), &set_code)
56+
.submit()
57+
.await
58+
.expect("`set_code` failed");
59+
60+
// Call the migration contract with a new value for `inc_by` and the code hash
61+
// of the updated contract.
62+
const NEW_INC_BY: u8 = 4;
63+
let migrate = contract
64+
.call_builder::<migration::incrementer::Incrementer>()
65+
.migrate(NEW_INC_BY, new_code_hash);
66+
67+
let _migration_result = client
68+
.call(&ink_e2e::alice(), &migrate)
69+
.submit()
70+
.await
71+
.expect("`migrate` failed");
72+
73+
// Then
74+
let inc = contract
75+
.call_builder::<updated_incrementer::incrementer::Incrementer>()
76+
.inc();
77+
78+
let _inc_result = client
79+
.call(&ink_e2e::alice(), &inc)
80+
.submit()
81+
.await
82+
.expect("`inc` failed");
83+
84+
let get = call_builder.get();
85+
let get_res = client.call(&ink_e2e::alice(), &get).dry_run().await?;
86+
87+
// Remember, we updated our incrementer contract to increment by `4`.
88+
assert_eq!(
89+
get_res.return_value(),
90+
pre_migration_value + NEW_INC_BY as u32
91+
);
92+
93+
Ok(())
94+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
#![cfg_attr(not(feature = "std"), no_std, no_main)]
2+
3+
//! Demonstrates how to use [`set_code_hash`](https://docs.rs/ink_env/latest/ink_env/fn.set_code_hash.html)
4+
//! to swap out the `code_hash` of an on-chain contract.
5+
//!
6+
//! We will swap the code of our `Incrementer` contract with that of the `Incrementer`
7+
//! found in the `updated_incrementer` folder.
8+
//!
9+
//! See the included End-to-End tests an example update workflow.
10+
11+
#[ink::contract]
12+
pub mod incrementer {
13+
/// Track a counter in storage.
14+
///
15+
/// # Note
16+
///
17+
/// Is is important to realize that after the call to `set_code_hash` the contract's
18+
/// storage remains the same.
19+
///
20+
/// If you change the storage layout in your storage struct you may introduce
21+
/// undefined behavior to your contract!
22+
#[ink(storage)]
23+
#[derive(Default)]
24+
pub struct Incrementer {
25+
count: u32,
26+
}
27+
28+
impl Incrementer {
29+
/// Creates a new counter smart contract initialized with the given base value.
30+
#[ink(constructor)]
31+
pub fn new() -> Self {
32+
Default::default()
33+
}
34+
35+
/// Increments the counter value which is stored in the contract's storage.
36+
#[ink(message)]
37+
pub fn inc(&mut self) {
38+
self.count = self.count.checked_add(1).unwrap();
39+
ink::env::debug_println!(
40+
"The new count is {}, it was modified using the original contract code.",
41+
self.count
42+
);
43+
}
44+
45+
/// Returns the counter value which is stored in this contract's storage.
46+
#[ink(message)]
47+
pub fn get(&self) -> u32 {
48+
self.count
49+
}
50+
51+
/// Modifies the code which is used to execute calls to this contract address
52+
/// (`AccountId`).
53+
///
54+
/// We use this to upgrade the contract logic. We don't do any authorization here,
55+
/// any caller can execute this method.
56+
///
57+
/// In a production contract you would do some authorization here!
58+
#[ink(message)]
59+
pub fn set_code(&mut self, code_hash: Hash) {
60+
self.env().set_code_hash(&code_hash).unwrap_or_else(|err| {
61+
panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}")
62+
});
63+
ink::env::debug_println!("Switched code hash to {:?}.", code_hash);
64+
}
65+
}
66+
}
67+
68+
#[cfg(all(test, feature = "e2e-tests"))]
69+
mod e2e_tests;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "migration"
3+
version = "5.0.0-alpha"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
ink = { path = "../../../../crates/ink", default-features = false }
10+
11+
[lib]
12+
path = "lib.rs"
13+
14+
[features]
15+
default = ["std"]
16+
std = [
17+
"ink/std",
18+
]
19+
ink-as-dependency = []
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#![cfg_attr(not(feature = "std"), no_std, no_main)]
2+
#![allow(clippy::new_without_default)]
3+
4+
#[ink::contract]
5+
pub mod incrementer {
6+
7+
/// Storage struct matches exactly that of the original `incrementer` contract, from
8+
/// which we are migrating.
9+
#[ink(storage)]
10+
pub struct Incrementer {
11+
count: u32,
12+
}
13+
14+
#[ink::storage_item]
15+
pub struct IncrementerNew {
16+
count: u64,
17+
inc_by: u8,
18+
}
19+
20+
impl Incrementer {
21+
/// Creates a new counter smart contract initialized with the given base value.
22+
///
23+
/// # Note
24+
///
25+
/// When upgrading using the `set_code_hash` workflow we only need to point to a
26+
/// contract's uploaded code hash, **not** an instantiated contract's
27+
/// `AccountId`.
28+
///
29+
/// Because of this we will never actually call the constructor of this contract.
30+
#[ink(constructor)]
31+
pub fn new() -> Self {
32+
unreachable!(
33+
"Constructors are not called when upgrading using `set_code_hash`."
34+
)
35+
}
36+
37+
/// Run the migration to the data layout for the upgraded contract.
38+
/// Once the storage migration has successfully completed, the contract will be
39+
/// upgraded to the supplied code hash.
40+
///
41+
/// In a production contract you would do some authorization here!
42+
///
43+
/// # Note
44+
///
45+
/// This function necessarily accepts a `&self` instead of a `&mut self` because
46+
/// we are modifying storage directly for the migration.
47+
///
48+
/// The `self` in `&mut self` is the original `Incrementer` storage struct, and
49+
/// would be implicitly written to storage following the function execution,
50+
/// overwriting the migrated storage.
51+
#[ink(message)]
52+
pub fn migrate(&self, inc_by: u8, code_hash: Hash) {
53+
let incrementer_new = IncrementerNew {
54+
count: self.count as u64,
55+
inc_by,
56+
};
57+
58+
// overwrite the original storage struct with the migrated storage struct,
59+
// which has a layout compatible with the new contract code.
60+
const STORAGE_KEY: u32 =
61+
<Incrementer as ink::storage::traits::StorageKey>::KEY;
62+
ink::env::set_contract_storage(&STORAGE_KEY, &incrementer_new);
63+
64+
ink::env::set_code_hash::<<Self as ink::env::ContractEnv>::Env>(&code_hash)
65+
.unwrap_or_else(|err| {
66+
panic!("Failed to `set_code_hash` to {code_hash:?} due to {err:?}")
67+
})
68+
}
69+
}
70+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "updated-incrementer"
3+
version = "5.0.0-alpha"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2021"
6+
publish = false
7+
8+
[dependencies]
9+
ink = { path = "../../../../crates/ink", default-features = false }
10+
11+
[lib]
12+
path = "lib.rs"
13+
14+
[features]
15+
default = ["std"]
16+
std = [
17+
"ink/std",
18+
]
19+
ink-as-dependency = []

0 commit comments

Comments
 (0)