242 lines
8.8 KiB
Plaintext
242 lines
8.8 KiB
Plaintext
// SPDX-License-Identifier: Apache 2
|
|
|
|
/// Note: This module is based on the required_version module
|
|
/// from the Sui Wormhole package:
|
|
/// https://github.com/wormhole-foundation/wormhole/blob/sui/integration_v2/sui/wormhole/sources/resources/required_version.move
|
|
|
|
/// This module implements a mechanism for version control. While keeping track
|
|
/// of the latest version of a package build, `RequiredVersion` manages the
|
|
/// minimum required version number for any method in that package. For any
|
|
/// upgrade where a particular method can have backward compatibility, the
|
|
/// minimum version would not have to change (because the method should work the
|
|
/// same way with the previous version or current version).
|
|
///
|
|
/// If there happens to be a breaking change for a particular method, this
|
|
/// module can force that the method's minimum requirement be the latest build.
|
|
/// If a previous build were used, the method would abort if a check is in place
|
|
/// with `RequiredVersion`.
|
|
///
|
|
/// There is no magic behind the way ths module works. `RequiredVersion` is
|
|
/// intended to live in a package's shared object that gets passed into its
|
|
/// methods (e.g. pyth's `State` object).
|
|
module pyth::required_version {
|
|
use sui::dynamic_field::{Self as field};
|
|
use sui::object::{Self, UID};
|
|
use sui::package::{Self, UpgradeCap};
|
|
use sui::tx_context::{TxContext};
|
|
|
|
/// Build version passed does not meet method's minimum required version.
|
|
const E_OUTDATED_VERSION: u64 = 0;
|
|
|
|
/// Container to keep track of latest build version. Dynamic fields are
|
|
/// associated with its `id`.
|
|
struct RequiredVersion has store {
|
|
id: UID,
|
|
latest_version: u64
|
|
}
|
|
|
|
struct Key<phantom MethodType> has store, drop, copy {}
|
|
|
|
/// Create new `RequiredVersion` with a configured starting version.
|
|
public fun new(version: u64, ctx: &mut TxContext): RequiredVersion {
|
|
RequiredVersion {
|
|
id: object::new(ctx),
|
|
latest_version: version
|
|
}
|
|
}
|
|
|
|
/// Retrieve latest build version.
|
|
public fun current(self: &RequiredVersion): u64 {
|
|
self.latest_version
|
|
}
|
|
|
|
/// Add specific method handling via custom `MethodType`. At the time a
|
|
/// method is added, the minimum build version associated with this method
|
|
/// by default is the latest version.
|
|
public fun add<MethodType>(self: &mut RequiredVersion) {
|
|
field::add(&mut self.id, Key<MethodType> {}, self.latest_version)
|
|
}
|
|
|
|
/// This method will abort if the version for a particular `MethodType` is
|
|
/// not up-to-date with the version of the current build.
|
|
///
|
|
/// For example, if the minimum requirement for `foobar` module (with an
|
|
/// appropriately named `MethodType` like `FooBar`) is `1` and the current
|
|
/// implementation version is `2`, this method will succeed because the
|
|
/// build meets the minimum required version of `1` in order for `foobar` to
|
|
/// work. So if someone were to use an older build like version `1`, this
|
|
/// method will succeed.
|
|
///
|
|
/// But if `check_minimum_requirement` were invoked for `foobar` when the
|
|
/// minimum requirement is `2` and the current build is only version `1`,
|
|
/// then this method will abort because the build does not meet the minimum
|
|
/// version requirement for `foobar`.
|
|
///
|
|
/// This method also assumes that the `MethodType` being checked for is
|
|
/// already a dynamic field (using `add`) during initialization.
|
|
public fun check_minimum_requirement<MethodType>(
|
|
self: &RequiredVersion,
|
|
build_version: u64
|
|
) {
|
|
assert!(
|
|
build_version >= minimum_for<MethodType>(self),
|
|
E_OUTDATED_VERSION
|
|
);
|
|
}
|
|
|
|
/// At `commit_upgrade`, use this method to update the tracker's knowledge
|
|
/// of the latest upgrade (build) version, which is obtained from the
|
|
/// `UpgradeCap` in `sui::package`.
|
|
public fun update_latest(
|
|
self: &mut RequiredVersion,
|
|
upgrade_cap: &UpgradeCap
|
|
) {
|
|
self.latest_version = package::version(upgrade_cap);
|
|
}
|
|
|
|
/// Once the global version is updated via `commit_upgrade` and there is a
|
|
/// particular method that has a breaking change, use this method to uptick
|
|
/// that method's minimum required version to the latest.
|
|
public fun require_current_version<MethodType>(self: &mut RequiredVersion) {
|
|
let min_version = field::borrow_mut(&mut self.id, Key<MethodType> {});
|
|
*min_version = self.latest_version;
|
|
}
|
|
|
|
/// Retrieve the minimum required version for a particular method (via
|
|
/// `MethodType`).
|
|
public fun minimum_for<MethodType>(self: &RequiredVersion): u64 {
|
|
*field::borrow(&self.id, Key<MethodType> {})
|
|
}
|
|
|
|
#[test_only]
|
|
public fun set_required_version<MethodType>(
|
|
self: &mut RequiredVersion,
|
|
version: u64
|
|
) {
|
|
*field::borrow_mut(&mut self.id, Key<MethodType> {}) = version;
|
|
}
|
|
|
|
#[test_only]
|
|
public fun destroy(req: RequiredVersion) {
|
|
let RequiredVersion { id, latest_version: _} = req;
|
|
object::delete(id);
|
|
}
|
|
}
|
|
|
|
#[test_only]
|
|
module pyth::required_version_test {
|
|
use sui::hash::{keccak256};
|
|
use sui::object::{Self};
|
|
use sui::package::{Self};
|
|
use sui::tx_context::{Self};
|
|
|
|
use pyth::required_version::{Self};
|
|
|
|
struct SomeMethod {}
|
|
struct AnotherMethod {}
|
|
|
|
#[test]
|
|
public fun test_check_minimum_requirement() {
|
|
let ctx = &mut tx_context::dummy();
|
|
|
|
let version = 1;
|
|
let req = required_version::new(version, ctx);
|
|
assert!(required_version::current(&req) == version, 0);
|
|
|
|
required_version::add<SomeMethod>(&mut req);
|
|
assert!(required_version::minimum_for<SomeMethod>(&req) == version, 0);
|
|
|
|
// Should not abort here.
|
|
required_version::check_minimum_requirement<SomeMethod>(&req, version);
|
|
|
|
// And should not abort if the version is anything greater than the
|
|
// current.
|
|
let new_version = version + 1;
|
|
required_version::check_minimum_requirement<SomeMethod>(
|
|
&req,
|
|
new_version
|
|
);
|
|
|
|
// Uptick based on new upgrade.
|
|
let upgrade_cap = package::test_publish(
|
|
object::id_from_address(@pyth),
|
|
ctx
|
|
);
|
|
let digest = keccak256(&x"DEADBEEF");
|
|
let policy = package::upgrade_policy(&upgrade_cap);
|
|
let upgrade_ticket =
|
|
package::authorize_upgrade(&mut upgrade_cap, policy, digest);
|
|
let upgrade_receipt = package::test_upgrade(upgrade_ticket);
|
|
package::commit_upgrade(&mut upgrade_cap, upgrade_receipt);
|
|
assert!(package::version(&upgrade_cap) == new_version, 0);
|
|
|
|
// Update to the latest version.
|
|
required_version::update_latest(&mut req, &upgrade_cap);
|
|
assert!(required_version::current(&req) == new_version, 0);
|
|
|
|
// Should still not abort here.
|
|
required_version::check_minimum_requirement<SomeMethod>(
|
|
&req,
|
|
new_version
|
|
);
|
|
|
|
// Require new version for `SomeMethod` and show that
|
|
// `check_minimum_requirement` still succeeds.
|
|
required_version::require_current_version<SomeMethod>(&mut req);
|
|
assert!(
|
|
required_version::minimum_for<SomeMethod>(&req) == new_version,
|
|
0
|
|
);
|
|
required_version::check_minimum_requirement<SomeMethod>(
|
|
&req,
|
|
new_version
|
|
);
|
|
|
|
// If another method gets added to the mix, it should automatically meet
|
|
// the minimum requirement because its version will be the latest.
|
|
required_version::add<AnotherMethod>(&mut req);
|
|
assert!(
|
|
required_version::minimum_for<AnotherMethod>(&req) == new_version,
|
|
0
|
|
);
|
|
required_version::check_minimum_requirement<SomeMethod>(
|
|
&req,
|
|
new_version
|
|
);
|
|
|
|
// Clean up.
|
|
package::make_immutable(upgrade_cap);
|
|
required_version::destroy(req);
|
|
}
|
|
|
|
#[test]
|
|
#[expected_failure(abort_code = required_version::E_OUTDATED_VERSION)]
|
|
public fun test_cannot_check_minimum_requirement_with_outdated_version() {
|
|
let ctx = &mut tx_context::dummy();
|
|
|
|
let version = 1;
|
|
let req = required_version::new(version, ctx);
|
|
assert!(required_version::current(&req) == version, 0);
|
|
|
|
required_version::add<SomeMethod>(&mut req);
|
|
|
|
// Should not abort here.
|
|
required_version::check_minimum_requirement<SomeMethod>(&req, version);
|
|
|
|
// Uptick minimum requirement and fail at `check_minimum_requirement`.
|
|
let new_version = 10;
|
|
required_version::set_required_version<SomeMethod>(
|
|
&mut req,
|
|
new_version
|
|
);
|
|
let old_version = new_version - 1;
|
|
required_version::check_minimum_requirement<SomeMethod>(
|
|
&req,
|
|
old_version
|
|
);
|
|
|
|
// Clean up.
|
|
required_version::destroy(req);
|
|
}
|
|
}
|