Skip to content

Commit

Permalink
Add support for the delayed upgrade (Near-One#44)
Browse files Browse the repository at this point in the history
* Add support for the delayed upgrade

* Reimplement the delayed upgrade functionality

* Upgradable: Add update duration interface

* Upgradable: revert chanegs of using base64 instead borsh

* Fix commnets

* Fix comment

* Improve storage management for upgradable plugin

* Improve upgradable plugin:
* Remove `UpdateStagingDurationTimestamp`
* Add helper functions
* Improve naming

* Fix staging timestamp validation

* Add `update_delay_duration` tests

* Minor improvment

* Apply cargo fmt

* Add setters & use borsh ser/de

* Update readme

* Revert changes of removing `StagingDurationTimestamp`

* Fix type of `new_staging_duration_timestamp`

* Fix `self` mutability

* Remove `StagingTimestamp` on removing staged code
  • Loading branch information
karim-en authored Feb 3, 2023
1 parent 1fcc95c commit 244d008
Show file tree
Hide file tree
Showing 8 changed files with 331 additions and 26 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
/target
Cargo.lock
near-plugins/tests/contracts/*/target
examples/target

# Ignore IDE data
.vscode/
Expand Down
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ Documentation of all methods provided by `Pausable` is available in the [definit

### [Upgradable](/near-plugins/src/upgradable.rs)

Allows a contract to be upgraded by owner without having a Full Access Key.
Allows a contract to be upgraded by owner with delay and without having a Full Access Key.

Contract example using _Upgradable_ plugin. Note that it requires the contract to be Ownable.

Expand All @@ -77,6 +77,7 @@ impl Counter {
fn new() -> Self {
let mut contract = Self {};
contract.owner_set(Some(near_sdk::env::predecessor_account_id()));
contract.up_init_staging_duration(std::time::Duration::from_secs(60).as_nanos().try_into().unwrap()); // 1 minute
contract
}
}
Expand All @@ -85,6 +86,9 @@ impl Counter {
To upgrade the contract first call `up_stage_code` passing the binary as first argument serialized as borsh. Then call `up_deploy_code`.
This functions must be called from the owner.

To update the staging delay first call `up_stage_update_staging_duration` passing the new delay duration. Then call `up_apply_update_staging_duration`.
This functions must be called from the owner.

Documentation of all methods provided by the derived implementation of `Upgradable` is available in the [definition of the trait](/near-plugins/src/upgradable.rs). More examples and guidelines for interacting with an `Upgradable` contract can be found [here](/examples/upgradable-examples/README.md).

### [AccessControllable](/near-plugins/src/access_controllable.rs)
Expand Down
22 changes: 11 additions & 11 deletions examples/upgradable-examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,26 +66,26 @@ To upgrade the contract first call up_stage_code passing the binary as first arg

## The contract methods description
### up_storage_key
`up_storage_key` is a _view_ method that returns a key of the storage slot for stage code.
By default, `b"__CODE__"` is used. For changing, the attribute `upgradable` can be used.
`up_storage_prefix` is a _view_ method that returns the storage prefix for slots related to upgradable.
By default, `b"__up__"` is used. For changing, the attribute `upgradable` can be used.

```shell
$ near view <CONTRACT_ACCOUNT> up_storage_key
View call: <CONTRACT_ACCOUNT>.up_storage_key()
$ near view <CONTRACT_ACCOUNT> up_storage_prefix
View call: <CONTRACT_ACCOUNT>.up_storage_prefix()
[
95, 95, 80, 65, 85,
83, 69, 68, 95, 95
95, 95, 117,
112, 95, 95
]
$ python3
>>> print(' '.join(str(b) for b in bytes("__CODE__", 'utf8')))
95 95 67 79 68 69 95 95
>>> print(' '.join(str(b) for b in bytes("__up__", 'utf8')))
95 95 117 112 95 95
```
Example of changing paused storage key:
Example of changing the storage prefix:
```rust
#[near_bindgen]
#[derive(Ownable, Upgradable, Default, BorshSerialize, BorshDeserialize)]
#[upgradable(code_storage_key="OTHER_CODE_STORAGE_KEY")]
#[upgradable(storage_prefix="OTHER_CODE_STORAGE_PREFIX")]
struct Counter {
counter: u64,
}
Expand All @@ -105,7 +105,7 @@ But it doesn't work in that way because we can't provide in Bash so long args...
For running `up_satge_code` take a look on `up_stage_code/src/main.rs` script.
```shell
$ cd up_stage_code
$ cargo run -- "<PATH_TO_KEY_FOR_CONTRACT_ACCOUNT>"
$ cargo run -- -p '<PATH_TO_KEY_FOR_CONTRACT_ACCOUNT>'
$ cd ..
```
Where `<PATH_TO_KEY_FOR_CONTRACT_ACCOUNT>` is `$HOME/.near-credentials/testnet/<CONTRACT_ACCOUNT>.json`
Expand Down
7 changes: 5 additions & 2 deletions examples/upgradable-examples/up_stage_code/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,12 @@ async fn main() {

let contract: Account = match &*args.network {
"testnet" => get_contract!(testnet, args.path_to_key),
"mainnet" => get_contract!(mainnet, args.path_to_key),
"mainnet" => get_contract!(mainnet, args.path_to_key),
"betanet" => get_contract!(betanet, args.path_to_key),
network => panic!("Unknown network {}. Possible networks: testnet, mainnet, betanet", network)
network => panic!(
"Unknown network {}. Possible networks: testnet, mainnet, betanet",
network
),
};

let wasm = std::fs::read(&args.wasm).unwrap();
Expand Down
1 change: 1 addition & 0 deletions examples/upgradable-examples/upgradable_base/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ impl Counter {
pub fn new() -> Self {
let mut contract = Self { counter: 0 };
contract.owner_set(Some(near_sdk::env::predecessor_account_id()));
contract.up_init_staging_duration(std::time::Duration::from_secs(60).as_nanos().try_into().unwrap()); // 1 minute
contract
}

Expand Down
135 changes: 126 additions & 9 deletions near-plugins-derive/src/upgradable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@ use syn::{parse_macro_input, DeriveInput};
#[derive(FromDeriveInput, Default)]
#[darling(default, attributes(upgradable), forward_attrs(allow, doc, cfg))]
struct Opts {
code_storage_key: Option<String>,
storage_prefix: Option<String>,
}

const DEFAULT_STORAGE_PREFIX: &str = "__up__";

/// Generates the token stream for the `Upgradable` macro.
pub fn derive_upgradable(input: TokenStream) -> TokenStream {
let cratename = cratename();
Expand All @@ -18,29 +20,94 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
let opts = Opts::from_derive_input(&input).expect("Wrong options");
let DeriveInput { ident, .. } = input;

let code_storage_key = opts
.code_storage_key
.unwrap_or_else(|| "__CODE__".to_string());
let storage_prefix = opts
.storage_prefix
.unwrap_or_else(|| DEFAULT_STORAGE_PREFIX.to_string());

let output = quote! {
/// Used to make storage prefixes unique. Not to be used directly,
/// instead it should be prepended to the storage prefix specified by
/// the user.
#[derive(::near_sdk::borsh::BorshSerialize)]
enum __UpgradableStorageKey {
Code,
StagingTimestamp,
StagingDuration,
NewStagingDuration,
NewStagingDurationTimestamp,
}

impl #ident {
fn up_get_timestamp(&self, key: __UpgradableStorageKey) -> Option<::near_sdk::Timestamp> {
near_sdk::env::storage_read(self.up_storage_key(key).as_ref()).map(|timestamp_bytes| {
::near_sdk::Timestamp::try_from_slice(&timestamp_bytes).unwrap_or_else(|_|
near_sdk::env::panic_str("Upgradable: Invalid u64 timestamp format")
)
})
}

fn up_get_duration(&self, key: __UpgradableStorageKey) -> Option<::near_sdk::Duration> {
near_sdk::env::storage_read(self.up_storage_key(key).as_ref()).map(|duration_bytes| {
::near_sdk::Duration::try_from_slice(&duration_bytes).unwrap_or_else(|_|
near_sdk::env::panic_str("Upgradable: Invalid u64 Duration format")
)
})
}

fn up_set_timestamp(&self, key: __UpgradableStorageKey, value: ::near_sdk::Timestamp) {
self.up_storage_write(key, &value.try_to_vec().unwrap());
}

fn up_set_duration(&self, key: __UpgradableStorageKey, value: ::near_sdk::Duration) {
self.up_storage_write(key, &value.try_to_vec().unwrap());
}

fn up_storage_key(&self, key: __UpgradableStorageKey) -> Vec<u8> {
let key_vec = key
.try_to_vec()
.unwrap_or_else(|_| ::near_sdk::env::panic_str("Storage key should be serializable"));
[(#storage_prefix).as_bytes(), key_vec.as_slice()].concat()
}

fn up_storage_write(&self, key: __UpgradableStorageKey, value: &[u8]) {
near_sdk::env::storage_write(self.up_storage_key(key).as_ref(), &value);
}

fn up_set_staging_duration_unchecked(&self, staging_duration: near_sdk::Duration) {
self.up_storage_write(__UpgradableStorageKey::StagingDuration, &staging_duration.try_to_vec().unwrap());
}
}

#[near_bindgen]
impl Upgradable for #ident {
fn up_storage_key(&self) -> Vec<u8>{
(#code_storage_key).as_bytes().to_vec()
fn up_storage_prefix(&self) -> &'static [u8] {
(#storage_prefix).as_bytes()
}

fn up_get_delay_status(&self) -> #cratename::UpgradableDurationStatus {
near_plugins::UpgradableDurationStatus {
staging_duration: self.up_get_duration(__UpgradableStorageKey::StagingDuration),
staging_timestamp: self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp),
new_staging_duration: self.up_get_duration(__UpgradableStorageKey::NewStagingDuration),
new_staging_duration_timestamp: self.up_get_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp),
}
}

#[#cratename::only(owner)]
fn up_stage_code(&mut self, #[serializer(borsh)] code: Vec<u8>) {
if code.is_empty() {
near_sdk::env::storage_remove(self.up_storage_key().as_ref());
near_sdk::env::storage_remove(self.up_storage_key(__UpgradableStorageKey::Code).as_ref());
near_sdk::env::storage_remove(self.up_storage_key(__UpgradableStorageKey::StagingTimestamp).as_ref());
} else {
near_sdk::env::storage_write(self.up_storage_key().as_ref(), code.as_ref());
let timestamp = near_sdk::env::block_timestamp() + self.up_get_duration(__UpgradableStorageKey::StagingDuration).unwrap_or(0);
self.up_storage_write(__UpgradableStorageKey::Code, &code);
self.up_set_timestamp(__UpgradableStorageKey::StagingTimestamp, timestamp);
}
}

#[result_serializer(borsh)]
fn up_staged_code(&self) -> Option<Vec<u8>> {
near_sdk::env::storage_read(self.up_storage_key().as_ref())
near_sdk::env::storage_read(self.up_storage_key(__UpgradableStorageKey::Code).as_ref())
}

fn up_staged_code_hash(&self) -> Option<::near_sdk::CryptoHash> {
Expand All @@ -50,9 +117,59 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {

#[#cratename::only(owner)]
fn up_deploy_code(&mut self) -> near_sdk::Promise {
let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::StagingTimestamp)
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging timestamp isn't set"));

if near_sdk::env::block_timestamp() < staging_timestamp {
near_sdk::env::panic_str(
format!(
"Upgradable: Deploy code too early: staging ends on {}",
staging_timestamp
)
.as_str(),
);
}

near_sdk::Promise::new(near_sdk::env::current_account_id())
.deploy_contract(self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code")))
}

#[#cratename::only(owner)]
fn up_init_staging_duration(&mut self, staging_duration: near_sdk::Duration) {
near_sdk::require!(self.up_get_duration(__UpgradableStorageKey::StagingDuration).is_none(), "Upgradable: staging duration was already initialized");
self.up_set_staging_duration_unchecked(staging_duration);
}

#[#cratename::only(owner)]
fn up_stage_update_staging_duration(&mut self, staging_duration: near_sdk::Duration) {
let current_staging_duration = self.up_get_duration(__UpgradableStorageKey::StagingDuration)
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: staging duration isn't initialized"));

self.up_set_duration(__UpgradableStorageKey::NewStagingDuration, staging_duration);
let staging_duration_timestamp = near_sdk::env::block_timestamp() + current_staging_duration;
self.up_set_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp, staging_duration_timestamp);
}

#[#cratename::only(owner)]
fn up_apply_update_staging_duration(&mut self) {
let staging_timestamp = self.up_get_timestamp(__UpgradableStorageKey::NewStagingDurationTimestamp)
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged update"));

if near_sdk::env::block_timestamp() < staging_timestamp {
near_sdk::env::panic_str(
format!(
"Upgradable: Update duration too early: staging ends on {}",
staging_timestamp
)
.as_str(),
);
}

let new_duration = self.up_get_duration(__UpgradableStorageKey::NewStagingDuration)
.unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged duration update"));

self.up_set_duration(__UpgradableStorageKey::StagingDuration, new_duration);
}
}
};

Expand Down
1 change: 1 addition & 0 deletions near-plugins/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ pub use near_plugins_derive::{
pub use ownable::Ownable;
pub use pausable::Pausable;
pub use upgradable::Upgradable;
pub use upgradable::UpgradableDurationStatus;

// Re-exporting these dependencies avoids requiring contracts to depend on them.
// For example, without re-exporting `bitflags` a contract using the access
Expand Down
Loading

0 comments on commit 244d008

Please sign in to comment.