Skip to content

Commit 24cdc62

Browse files
author
Eduardo Leegwater Simões
committed
piecrust: add Session::migrate
The new `Session::migrate` function allows for swapping contract bytecode for a different code and perform a migration of the data by allowing a sequence of operations on a `Session` to be done whilst both contracts are available, and, once this sequence of operations is done, swapping the old contract for the new one.
1 parent c2116cc commit 24cdc62

File tree

5 files changed

+90
-10
lines changed

5 files changed

+90
-10
lines changed

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
}

0 commit comments

Comments
 (0)