Skip to content

Commit 0f81d4a

Browse files
author
Eduardo Leegwater Simões
authored
Merge pull request #320 from dusk-network/migrate-bytecode-313
Add bytecode migrations
2 parents 4b70e7f + 24cdc62 commit 0f81d4a

File tree

9 files changed

+227
-10
lines changed

9 files changed

+227
-10
lines changed

contracts/Cargo.toml

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ members = [
66
"constructor",
77
"counter",
88
"debugger",
9+
"double_counter",
910
"eventer",
1011
"everest",
1112
"fallible_counter",

contracts/double_counter/Cargo.toml

+15
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[package]
2+
name = "double_counter"
3+
version = "0.1.0"
4+
authors = [
5+
"Eduardo Leegwater Simões <[email protected]>",
6+
]
7+
edition = "2021"
8+
9+
license = "MPL-2.0"
10+
11+
[dependencies]
12+
piecrust-uplink = { path = "../../piecrust-uplink", features = ["abi", "dlmalloc"] }
13+
14+
[lib]
15+
crate-type = ["cdylib", "rlib"]

contracts/double_counter/src/lib.rs

+61
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
// This Source Code Form is subject to the terms of the Mozilla Public
2+
// License, v. 2.0. If a copy of the MPL was not distributed with this
3+
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
4+
//
5+
// Copyright (c) DUSK NETWORK. All rights reserved.
6+
7+
//! Contract to implement a simple double counter that can be read and
8+
//! have either value incremented by one count.
9+
10+
#![no_std]
11+
12+
use piecrust_uplink as uplink;
13+
14+
/// Struct that describes the state of the DoubleCounter contract
15+
pub struct DoubleCounter {
16+
left_value: i64,
17+
right_value: i64,
18+
}
19+
20+
/// State of the DoubleCounter contract
21+
static mut STATE: DoubleCounter = DoubleCounter {
22+
left_value: 0xfc,
23+
right_value: 0xcf,
24+
};
25+
26+
impl DoubleCounter {
27+
/// Read the value of the counter
28+
pub fn read_values(&self) -> (i64, i64) {
29+
(self.left_value, self.right_value)
30+
}
31+
32+
/// Increment the value of the left counter by 1
33+
pub fn increment_left(&mut self) {
34+
let value = self.left_value + 1;
35+
self.left_value = value;
36+
}
37+
38+
/// Increment the value of the right counter by 1
39+
pub fn increment_right(&mut self) {
40+
let value = self.right_value + 1;
41+
self.right_value = value;
42+
}
43+
}
44+
45+
/// Expose `Counter::read_value()` to the host
46+
#[no_mangle]
47+
unsafe fn read_values(arg_len: u32) -> u32 {
48+
uplink::wrap_call(arg_len, |_: ()| STATE.read_values())
49+
}
50+
51+
/// Expose `Counter::increment_left()` to the host
52+
#[no_mangle]
53+
unsafe fn increment_left(arg_len: u32) -> u32 {
54+
uplink::wrap_call(arg_len, |_: ()| STATE.increment_left())
55+
}
56+
57+
/// Expose `Counter::increment_right()` to the host
58+
#[no_mangle]
59+
unsafe fn increment_right(arg_len: u32) -> u32 {
60+
uplink::wrap_call(arg_len, |_: ()| STATE.increment_right())
61+
}

piecrust/CHANGELOG.md

+5
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Add `Session::migrate` to allow for swapping contract code [#313]
13+
1014
## [0.15.0] - 2024-01-24
1115

1216
### Changed
@@ -345,6 +349,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
345349
[#234]: https://github.com/dusk-network/piecrust/pull/234
346350

347351
<!-- ISSUES -->
352+
[#301]: https://github.com/dusk-network/piecrust/issues/313
348353
[#301]: https://github.com/dusk-network/piecrust/issues/301
349354
[#296]: https://github.com/dusk-network/piecrust/issues/296
350355
[#287]: https://github.com/dusk-network/piecrust/issues/287

piecrust/src/session.rs

+41
Original file line numberDiff line numberDiff line change
@@ -396,6 +396,47 @@ impl Session {
396396
})
397397
}
398398

399+
/// Migrates a `contract` to a new `bytecode`, performing modifications to
400+
/// its state as specified by the closure.
401+
///
402+
/// The closure takes a contract ID of where the new contract will be
403+
/// available during the migration, and a mutable reference to a session,
404+
/// allowing the caller to perform calls and other operations on the new
405+
/// (and old) contract.
406+
///
407+
/// At the end of the migration, the new contract will be available at the
408+
/// given `contract` ID, and the old contract will be removed from the
409+
/// state.
410+
///
411+
/// # Errors
412+
/// The migration may error during execution for a myriad of reasons. The
413+
/// caller is encouraged to drop the `Session` should an error occur as it
414+
/// will more than likely be left in an inconsistent state.
415+
pub fn migrate<'a, A, D, F, const N: usize>(
416+
mut self,
417+
contract: ContractId,
418+
bytecode: &[u8],
419+
deploy_data: D,
420+
deploy_gas_limit: u64,
421+
closure: F,
422+
) -> Result<Self, Error>
423+
where
424+
A: 'a + for<'b> Serialize<StandardBufSerializer<'b>>,
425+
D: Into<ContractData<'a, A, N>>,
426+
F: FnOnce(ContractId, &mut Session) -> Result<(), Error>,
427+
{
428+
let new_contract =
429+
self.deploy(bytecode, deploy_data, deploy_gas_limit)?;
430+
closure(new_contract, &mut self)?;
431+
432+
self.inner
433+
.contract_session
434+
.replace(contract, new_contract)
435+
.map_err(|err| PersistenceError(Arc::new(err)))?;
436+
437+
Ok(self)
438+
}
439+
399440
/// Execute a *feeder* call on the current state of this session.
400441
///
401442
/// Feeder calls are used to have the contract be able to report larger

piecrust/src/store.rs

+15-10
Original file line numberDiff line numberDiff line change
@@ -406,8 +406,13 @@ fn write_commit<P: AsRef<Path>>(
406406
let mut index = base
407407
.as_ref()
408408
.map_or(ContractIndex::default(), |base| base.index.clone());
409-
for c in &commit_contracts {
410-
index.insert(*c.0, &c.1.memory);
409+
410+
for (contract_id, contract_data) in &commit_contracts {
411+
if contract_data.is_new {
412+
index.remove_and_insert(*contract_id, &contract_data.memory);
413+
} else {
414+
index.insert(*contract_id, &contract_data.memory);
415+
}
411416
}
412417

413418
let root = *index.root();
@@ -497,6 +502,7 @@ fn write_commit_inner<P: AsRef<Path>>(
497502

498503
let mut pages = BTreeSet::new();
499504

505+
// Write dirty pages and keep track of the page indices.
500506
for (dirty_page, _, page_index) in contract_data.memory.dirty_pages() {
501507
let page_path = page_path(&memory_dir, *page_index);
502508
fs::write(page_path, dirty_page)?;
@@ -507,13 +513,16 @@ fn write_commit_inner<P: AsRef<Path>>(
507513
let module_path = bytecode_path.with_extension(OBJECTCODE_EXTENSION);
508514
let metadata_path = bytecode_path.with_extension(METADATA_EXTENSION);
509515

510-
// If the contract already existed in the base commit, we hard link the
511-
// bytecode, module, and metadata files to avoid duplicating them,
512-
// otherwise we write them to disk.
516+
// If the contract is new, we write the bytecode, module, and metadata
517+
// files to disk, otherwise we hard link them to avoid duplicating them.
513518
//
514519
// Also, if there is a base commit, we hard link the pages of the
515520
// contracts that are not dirty.
516-
if let Some(base) = &directories.base {
521+
if contract_data.is_new {
522+
fs::write(bytecode_path, &contract_data.bytecode)?;
523+
fs::write(module_path, &contract_data.module.serialize())?;
524+
fs::write(metadata_path, &contract_data.metadata)?;
525+
} else if let Some(base) = &directories.base {
517526
if let Some(elem) = base.inner.index.get(contract) {
518527
let base_bytecode_path = base.bytecode_dir.join(&contract_hex);
519528
let base_module_path =
@@ -539,10 +548,6 @@ fn write_commit_inner<P: AsRef<Path>>(
539548
}
540549
}
541550
}
542-
} else {
543-
fs::write(bytecode_path, &contract_data.bytecode)?;
544-
fs::write(module_path, &contract_data.module.serialize())?;
545-
fs::write(metadata_path, &contract_data.metadata)?;
546551
}
547552
}
548553

piecrust/src/store/session.rs

+24
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub struct ContractDataEntry {
2727
pub module: Module,
2828
pub metadata: Metadata,
2929
pub memory: Memory,
30+
pub is_new: bool,
3031
}
3132

3233
/// The representation of a session with a [`ContractStore`].
@@ -231,6 +232,7 @@ impl ContractSession {
231232
module,
232233
metadata,
233234
memory,
235+
is_new: false,
234236
})
235237
.clone();
236238

@@ -296,12 +298,34 @@ impl ContractSession {
296298
module,
297299
metadata,
298300
memory,
301+
is_new: true,
299302
},
300303
);
301304

302305
Ok(())
303306
}
304307

308+
/// Remove the `old_contract` and move the `new_contract` to the
309+
/// `old_contract`, effectively replacing the `old_contract` with
310+
/// `new_contract`.
311+
pub fn replace(
312+
&mut self,
313+
old_contract: ContractId,
314+
new_contract: ContractId,
315+
) -> io::Result<()> {
316+
let new_contract_data =
317+
self.contracts.remove(&new_contract).ok_or_else(|| {
318+
io::Error::new(
319+
io::ErrorKind::Other,
320+
format!("Contract '{new_contract}' not found"),
321+
)
322+
})?;
323+
324+
self.contracts.insert(old_contract, new_contract_data);
325+
326+
Ok(())
327+
}
328+
305329
/// Provides metadata of the contract with a given `contract_id`.
306330
pub fn contract_metadata(
307331
&self,

piecrust/src/store/tree.rs

+5
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,11 @@ impl ContractIndex {
158158
.insert(position_from_contract(&contract), *element.tree.root());
159159
}
160160

161+
pub fn remove_and_insert(&mut self, contract: ContractId, memory: &Memory) {
162+
self.contracts.remove(&contract);
163+
self.insert(contract, memory);
164+
}
165+
161166
pub fn root(&self) -> Ref<Hash> {
162167
self.tree.root()
163168
}

piecrust/tests/persistence.rs

+60
Original file line numberDiff line numberDiff line change
@@ -148,3 +148,63 @@ fn contracts_persistence() -> Result<(), Error> {
148148
);
149149
Ok(())
150150
}
151+
152+
#[test]
153+
fn migration() -> Result<(), Error> {
154+
let vm = VM::ephemeral()?;
155+
let mut session = vm.session(SessionData::builder())?;
156+
157+
let contract = session.deploy(
158+
contract_bytecode!("counter"),
159+
ContractData::builder(OWNER),
160+
LIMIT,
161+
)?;
162+
163+
session.call::<_, ()>(contract, "increment", &(), LIMIT)?;
164+
session.call::<_, ()>(contract, "increment", &(), LIMIT)?;
165+
166+
let root = session.commit()?;
167+
168+
let mut session = vm.session(SessionData::builder().base(root))?;
169+
170+
session = session.migrate(
171+
contract,
172+
contract_bytecode!("double_counter"),
173+
ContractData::builder(OWNER),
174+
LIMIT,
175+
|new_contract, session| {
176+
let old_counter_value = session
177+
.call::<_, i64>(contract, "read_value", &(), LIMIT)?
178+
.data;
179+
180+
let (left_counter_value, _) = session
181+
.call::<_, (i64, i64)>(new_contract, "read_values", &(), LIMIT)?
182+
.data;
183+
184+
let diff = old_counter_value - left_counter_value;
185+
for _ in 0..diff {
186+
session.call::<_, ()>(
187+
new_contract,
188+
"increment_left",
189+
&(),
190+
LIMIT,
191+
)?;
192+
}
193+
194+
Ok(())
195+
},
196+
)?;
197+
198+
let root = session.commit()?;
199+
200+
let mut session = vm.session(SessionData::builder().base(root))?;
201+
202+
let (left_counter, right_counter) = session
203+
.call::<_, (i64, i64)>(contract, "read_values", &(), LIMIT)?
204+
.data;
205+
206+
assert_eq!(left_counter, 0xfe);
207+
assert_eq!(right_counter, 0xcf);
208+
209+
Ok(())
210+
}

0 commit comments

Comments
 (0)