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 soroban_env_host::host::LegacyEpoch #754

Merged
merged 1 commit into from
Apr 11, 2023
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
83 changes: 83 additions & 0 deletions soroban-env-host/src/host.rs
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,82 @@ impl Default for DiagnosticLevel {
}
}

/// LegacyEpoch defines a set of _past_ (legacy) behaviour that we have logic in
/// the host to handle, that can be activated by the embedder to replay past
/// contracts "the same way they ran before".
///
/// Legacy epochs are added when some change in behaviour is detected by the
/// embedder, during the second phase of the embedder upgrading the host, when
/// the embedder performs a replay of some past transactions in order to expire
/// old versions of the host, and notices some behaviour that diverges: that
/// does not replay identically. At that point the _current_ behaviour is always
/// considered part of `LegacyEpoch:Current` and a new _earlier_ epoch is added
/// before it, that is named for the _previous_ behaviour.
///
/// To illustrate this with an example, first suppose that host versions are
/// being _sequentially numbered_ by the embedder as v74, v75, v76, v77, etc. We
/// do not assign such versions in the host itself, since the host does not know
/// precisely when the embedder takes updates (and in any case the embedder
/// needs to pin the entire dependency tree of versions the host's dependencies
/// are resolved to in the embedding).
///
/// Now suppose that host v75 accidentally shipped a bug in the binary search
/// function in `MeteredVector`, and suppose further that the embedder
/// accidentally recorded a transaction using that buggy binary search. So there
/// is a transaction labeled with host logic v75 that needs to run on bad binary
/// search to execute correctly.
///
/// Now suppose it's time for the embedder to upgrade. Upgrading only works when
/// the embedder embeds a _pair_ of host versions. Without loss of generality we
/// can assume they have v75 and v76, the network is running on v76, and so
/// their "upgrade" consists of expiring v75 (which is no longer recording new
/// transactions) and activating v77.
///
/// To do this, the embedder replays all transactions labeled with v75 on v76.
/// They will observe divergence on the transaction that relies on the bad
/// binary search. Once the embedder has to debugged that fact and isolated it,
/// they will want to add an enum entry `LegacyEpoch::BadBinSearch` to the host,
/// designating the _old_ behaviour. Then when replaying a v75 transaction, the
/// embedder will pass `LegacyEpoch::BadBinSearch`, and the host would be
/// modified to keep the old bad binary search logic under a guard like `if
/// self.is_in_legacy_epoch(LegacyEpoch::BadBinSearch) ... `.
///
/// Unfortunately such a change cannot be made to v76 since it's already frozen,
/// in the field and recording transactions: it's the version in the live
/// network. However, the change _can_ be made to the as-yet-unnumbered host
/// that will soon be designated v77, since that version is still not yet
/// finalized or deployed. So v77 will contain the legacy epoch-guarded copy of
/// the old, bad code path.
///
/// Schematically, the upgrade looks like this:
///
/// ```text
///
/// [v75: has only bad logic] (expiring)
/// ||
/// \/
/// [v76: has only good logic] (active)
/// ||
/// \/
/// [v77: has both, with epoch guard] (to be deployed)
///
///```
///
/// Once the embedder confirms that replay of all "v75 transactions" succeed on
/// host v77 emulating "the parts of v75 relevant to any actually-recorded
/// transactions" (as indicated with `LegacyEpoch::BadBinSearch`) the embedder
/// can do a release containing only hosts v76 and v77, and forget the rest of
/// v75.

#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
pub enum LegacyEpoch {
#[default]
Current,
}

#[derive(Clone, Default)]
pub(crate) struct HostImpl {
legacy_epoch: RefCell<LegacyEpoch>,
source_account: RefCell<Option<AccountId>>,
ledger: RefCell<Option<LedgerInfo>>,
objects: RefCell<Vec<HostObject>>,
Expand Down Expand Up @@ -241,6 +315,7 @@ impl Host {
/// [`Env::get_contract_data`].
pub fn with_storage_and_budget(storage: Storage, budget: Budget) -> Self {
Self(Rc::new(HostImpl {
legacy_epoch: Default::default(),
source_account: RefCell::new(None),
ledger: RefCell::new(None),
objects: Default::default(),
Expand All @@ -260,6 +335,14 @@ impl Host {
}))
}

pub fn set_legacy_epoch(&self, legacy_epoch: LegacyEpoch) {
*self.0.legacy_epoch.borrow_mut() = legacy_epoch
}

pub fn is_in_legacy_epoch(&self, legacy_epoch: LegacyEpoch) -> bool {
*self.0.legacy_epoch.borrow() <= legacy_epoch
}

pub fn set_source_account(&self, source_account: AccountId) {
*self.0.source_account.borrow_mut() = Some(source_account);
}
Expand Down
2 changes: 1 addition & 1 deletion soroban-env-host/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,6 @@ pub use host::testutils::call_with_suppressed_panic_hook;
pub use host::ContractFunctionSet;
pub use host::{
metered_map::MeteredOrdMap, metered_vector::MeteredVector, DiagnosticLevel, Host, HostError,
LedgerInfo,
LedgerInfo, LegacyEpoch,
};
pub use soroban_env_common::*;