|
|
@@ -1,241 +0,0 @@
|
|
|
-// 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);
|
|
|
- }
|
|
|
-}
|