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

feat(upgradable)!: add hash parameter to up_deploy_code #117

Merged
merged 14 commits into from
Sep 12, 2024
24 changes: 22 additions & 2 deletions near-plugins-derive/src/upgradable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,13 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
fn up_set_staging_duration_unchecked(&self, staging_duration: near_sdk::Duration) {
self.up_storage_write(__UpgradableStorageKey::StagingDuration, &staging_duration.try_to_vec().unwrap());
}

/// Computes the `sha256` hash of `code` and panics if the conversion to `CryptoHash` fails.
fn up_hash_code(code: &[u8]) -> ::near_sdk::CryptoHash {
let hash = near_sdk::env::sha256(code);
std::convert::TryInto::try_into(hash)
.expect("sha256 should convert to CryptoHash")
}
}

#[near_bindgen]
Expand Down Expand Up @@ -176,11 +183,11 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {

fn up_staged_code_hash(&self) -> Option<::near_sdk::CryptoHash> {
self.up_staged_code()
.map(|code| std::convert::TryInto::try_into(near_sdk::env::sha256(code.as_ref())).unwrap())
.map(|code| Self::up_hash_code(code.as_ref()))
}

#[#cratename::access_control_any(roles(#(#acl_roles_code_deployers),*))]
fn up_deploy_code(&mut self, function_call_args: Option<#cratename::upgradable::FunctionCallArgs>) -> near_sdk::Promise {
fn up_deploy_code(&mut self, hash: Option<String>, function_call_args: Option<#cratename::upgradable::FunctionCallArgs>) -> 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"));

Expand All @@ -195,6 +202,19 @@ pub fn derive_upgradable(input: TokenStream) -> TokenStream {
}

let code = self.up_staged_code().unwrap_or_else(|| ::near_sdk::env::panic_str("Upgradable: No staged code"));
if let Some(hash) = hash {
let expected_hash = ::near_sdk::base64::encode(Self::up_hash_code(code.as_ref()));
if hash != expected_hash {
near_sdk::env::panic_str(
format!(
"Upgradable: Cannot deploy due to wrong hash: expected hash: {}",
expected_hash,
)
.as_str(),
)
}
}

let promise = near_sdk::Promise::new(near_sdk::env::current_account_id())
.deploy_contract(code);
match function_call_args {
Expand Down
2 changes: 2 additions & 0 deletions near-plugins-derive/tests/common/upgradable_contract.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,13 @@ impl UpgradableContract {
pub async fn up_deploy_code(
&self,
caller: &Account,
hash: Option<String>,
function_call_args: Option<FunctionCallArgs>,
) -> near_workspaces::Result<ExecutionFinalResult> {
caller
.call(self.contract.id(), "up_deploy_code")
.args_json(json!({
"hash": hash,
"function_call_args": function_call_args,
}))
.max_gas()
Expand Down
98 changes: 89 additions & 9 deletions near-plugins-derive/tests/upgradable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,13 @@ fn convert_code_to_crypto_hash(code: &[u8]) -> CryptoHash {
.expect("Code should be converted to CryptoHash")
}

/// Computes the hash `code` according the to requirements of the `hash` parameter of
/// `Upgradable::up_deploy_code`.
fn convert_code_to_deploy_hash(code: &[u8]) -> String {
let hash = near_sdk::env::sha256(code);
near_sdk::base64::encode(hash)
}

/// Smoke test of contract setup.
#[tokio::test]
async fn test_setup() -> anyhow::Result<()> {
Expand Down Expand Up @@ -439,8 +446,69 @@ async fn test_deploy_code_without_delay() -> anyhow::Result<()> {
setup.assert_staged_code(Some(code)).await;

// Deploy staged code.
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
let res = setup
.upgradable_contract
.up_deploy_code(&dao, None, None)
.await?;
assert_success_with_unit_return(res);

Ok(())
}

#[tokio::test]
async fn test_deploy_code_with_hash_success() -> anyhow::Result<()> {
let worker = near_workspaces::sandbox().await?;
let dao = worker.dev_create_account().await?;
let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?;

// Stage some code.
let code = vec![1, 2, 3];
let res = setup
.upgradable_contract
.up_stage_code(&dao, code.clone())
.await?;
assert_success_with_unit_return(res);
setup.assert_staged_code(Some(code.clone())).await;

// Deploy staged code.
let hash = convert_code_to_deploy_hash(&code);
let res = setup
.upgradable_contract
.up_deploy_code(&dao, Some(hash), None)
.await?;
assert_success_with_unit_return(res);

Ok(())
}

/// Verifies failure of `up_deploy_code(Some(hash), ...)` when `hash` does not correspond to the
/// hash of staged code.
#[tokio::test]
async fn test_deploy_code_with_hash_invalid_hash() -> anyhow::Result<()> {
let worker = near_workspaces::sandbox().await?;
let dao = worker.dev_create_account().await?;
let setup = Setup::new(worker.clone(), Some(dao.id().clone()), None).await?;

// Stage some code.
let code = vec![1, 2, 3];
let res = setup
.upgradable_contract
.up_stage_code(&dao, code.clone())
.await?;
assert_success_with_unit_return(res);
setup.assert_staged_code(Some(code.clone())).await;

// Deployment is aborted if an invalid hash is provided.
let res = setup
.upgradable_contract
.up_deploy_code(&dao, Some("invalid_hash".to_owned()), None)
.await?;
let actual_hash = convert_code_to_deploy_hash(&code);
let expected_err = format!(
"Upgradable: Cannot deploy due to wrong hash: expected hash: {}",
actual_hash
);
assert_failure_with(res, &expected_err);

Ok(())
}
Expand Down Expand Up @@ -468,7 +536,10 @@ async fn test_deploy_code_and_call_method() -> anyhow::Result<()> {
setup.assert_staged_code(Some(code)).await;

// Deploy staged code.
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
let res = setup
.upgradable_contract
.up_deploy_code(&dao, None, None)
.await?;
assert_success_with_unit_return(res);

// The newly deployed contract defines the function `is_upgraded`. Calling it successfully
Expand Down Expand Up @@ -513,7 +584,7 @@ async fn test_deploy_code_with_migration() -> anyhow::Result<()> {
};
let res = setup
.upgradable_contract
.up_deploy_code(&dao, Some(function_call_args))
.up_deploy_code(&dao, None, Some(function_call_args))
.await?;
assert_success_with_unit_return(res);

Expand Down Expand Up @@ -556,7 +627,7 @@ async fn test_deploy_code_with_migration_failure_rollback() -> anyhow::Result<()
};
let res = setup
.upgradable_contract
.up_deploy_code(&dao, Some(function_call_args))
.up_deploy_code(&dao, None, Some(function_call_args))
.await?;
assert_failure_with(res, "Failing migration on purpose");

Expand Down Expand Up @@ -664,7 +735,10 @@ async fn test_deploy_code_with_delay() -> anyhow::Result<()> {
fast_forward_beyond(&worker, staging_duration).await;

// Deploy staged code.
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
let res = setup
.upgradable_contract
.up_deploy_code(&dao, None, None)
.await?;
assert_success_with_unit_return(res);

Ok(())
Expand Down Expand Up @@ -694,7 +768,10 @@ async fn test_deploy_code_with_delay_failure_too_early() -> anyhow::Result<()> {
fast_forward_beyond(&worker, sdk_duration_from_secs(1)).await;

// Verify trying to deploy staged code fails.
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
let res = setup
.upgradable_contract
.up_deploy_code(&dao, None, None)
.await?;
assert_failure_with(res, ERR_MSG_DEPLOY_CODE_TOO_EARLY);

// Verify `code` wasn't deployed by calling a function that is defined only in the initial
Expand Down Expand Up @@ -723,7 +800,7 @@ async fn test_deploy_code_permission_failure() -> anyhow::Result<()> {
// call this method.
let res = setup
.upgradable_contract
.up_deploy_code(&setup.unauth_account, None)
.up_deploy_code(&setup.unauth_account, None, None)
.await?;
assert_insufficient_acl_permissions(
res,
Expand Down Expand Up @@ -762,7 +839,10 @@ async fn test_deploy_code_empty_failure() -> anyhow::Result<()> {
// The staging timestamp is set when staging code and removed when unstaging code. So when there
// is no code staged, there is no staging timestamp. Hence the error message regarding a missing
// staging timestamp is expected.
let res = setup.upgradable_contract.up_deploy_code(&dao, None).await?;
let res = setup
.upgradable_contract
.up_deploy_code(&dao, None, None)
.await?;
assert_failure_with(res, ERR_MSG_NO_STAGING_TS);

Ok(())
Expand Down Expand Up @@ -1010,7 +1090,7 @@ async fn test_acl_permission_scope() -> anyhow::Result<()> {
// deploy code.
let res = setup
.upgradable_contract
.up_deploy_code(&setup.unauth_account, None)
.up_deploy_code(&setup.unauth_account, None, None)
.await?;
assert_insufficient_acl_permissions(
res,
Expand Down
18 changes: 17 additions & 1 deletion near-plugins/src/upgradable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,18 @@ pub trait Upgradable {

/// Allows an authorized account to deploy the staged code. It panics if no code is staged.
///
/// # Verifying the hash of staged code
///
/// Some workflows (e.g. when a DAO interacts with an `Upgradable` contract) are facilitated if
/// deployment succeeds only in case the hash of staged code corresponds to a given hash. This
/// behavior can be enabled with the `hash` parameter. In case it is `Some(h)`, the deployment
/// succeeds only if `h` equals the base64 encoded string of the staged code's `sha256` hash. In
/// particular, the encoding according to [`near_sdk::base64::encode`] is expected. Note that
/// `near_sdk` uses a rather dated version of the `base64` crate whose API differs from current
/// versions.
///
/// Otherwise, if `hash` equals `None`, this verification step is skipped.
///
/// # Attaching a function call
///
/// If `function_call_args` are provided, code is deployed in a batch promise that contains the
Expand Down Expand Up @@ -143,7 +155,11 @@ pub trait Upgradable {
/// [asynchronous design]: https://docs.near.org/concepts/basics/transactions/overview
/// [state migration]: https://docs.near.org/develop/upgrade#migrating-the-state
/// [storage staked]: https://docs.near.org/concepts/storage/storage-staking#btw-you-can-remove-data-to-unstake-some-tokens
fn up_deploy_code(&mut self, function_call_args: Option<FunctionCallArgs>) -> Promise;
fn up_deploy_code(
&mut self,
hash: Option<String>,
function_call_args: Option<FunctionCallArgs>,
) -> Promise;

/// Initializes the duration of the delay for deploying the staged code. It defaults to zero if
/// code is staged before the staging duration is initialized. Once the staging duration has
Expand Down
2 changes: 1 addition & 1 deletion scripts/fix_dependencies.sh
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
# for some other attempts and how they failed in CI).
cargo update -p anstyle@1.0.4 --precise 1.0.2
cargo update -p anstyle-parse@0.2.2 --precise 0.2.1
cargo update -p clap@4.4.7 --precise 4.3.24
cargo update -p clap@4.4.8 --precise 4.3.24
cargo update -p clap_lex@0.5.1 --precise 0.5.0
Loading