From c4327b8c93a38a6793fb6e06b9ed76fe99637580 Mon Sep 17 00:00:00 2001 From: Henrique Nogara Date: Wed, 22 Nov 2023 12:10:14 -0300 Subject: [PATCH] Mesh 2098/store tracker address (#1559) * Add SupportedApiUpgrades storage; Add upgrade_api extrinsic * Add get_latest_api_upgrade chain extension * Add BoundedBTreeMap for managing supported apis * Add unit tests and benchmark for upgrade_api * Add draft for chain_extension_get_latest_api_upgrade benchmark * Fix test import * Change encoded_input to functional * Remove map for AccountId; Derive MaxEncodedLen * Remove unused constant; Add benhcmark weights * Cargo fmt * Split api hash storage into two: current and next upgrade * Fix transaction_version comparison * Remove hardcoded value; Remove next upgrade after updating current api hash * Fix hardcoded value; Add CodeHash to event * Fix test storage --------- Co-authored-by: Robert Gabriel Jakabosky --- pallets/contracts/src/benchmarking.rs | 45 ++++++ pallets/contracts/src/chain_extension.rs | 57 ++++++++ pallets/contracts/src/lib.rs | 146 ++++++++++++++++++-- pallets/runtime/develop/src/runtime.rs | 2 +- pallets/runtime/mainnet/src/runtime.rs | 2 +- pallets/runtime/testnet/src/runtime.rs | 2 +- pallets/runtime/tests/src/contracts_test.rs | 44 +++++- pallets/runtime/tests/src/storage.rs | 2 +- pallets/weights/src/polymesh_contracts.rs | 37 +++++ 9 files changed, 322 insertions(+), 15 deletions(-) diff --git a/pallets/contracts/src/benchmarking.rs b/pallets/contracts/src/benchmarking.rs index 4d6b599bcf..563bd51b77 100644 --- a/pallets/contracts/src/benchmarking.rs +++ b/pallets/contracts/src/benchmarking.rs @@ -563,4 +563,49 @@ benchmarks! { verify { assert_eq!(free_balance::(&addr), ENDOWMENT + 1 as Balance); } + + upgrade_api { + let current_spec_version = T::Version::get().spec_version; + let current_tx_version = T::Version::get().transaction_version; + let api_code_hash: ApiCodeHash = ApiCodeHash { hash: CodeHash::::default() }; + let chain_version = ChainVersion::new(current_spec_version, current_tx_version); + let api = Api::new(*b"POLY", current_spec_version); + let next_upgrade = NextUpgrade::new(chain_version, api_code_hash); + }: _(RawOrigin::Root, api.clone(), next_upgrade.clone()) + verify { + assert_eq!(ApiNextUpgrade::::get(&api).unwrap(), next_upgrade); + assert_eq!(CurrentApiHash::::get(&api),None); + } + + chain_extension_get_latest_api_upgrade { + let r in 0 .. CHAIN_EXTENSION_BATCHES; + + let current_spec_version = T::Version::get().spec_version; + let current_tx_version = T::Version::get().transaction_version; + + let api_code_hash: ApiCodeHash = ApiCodeHash { hash: CodeHash::::default() }; + let next_upgrade = NextUpgrade::new(ChainVersion::new(current_spec_version, current_tx_version), api_code_hash.clone()); + let output_len: u32 = api_code_hash.hash.as_ref().len() as u32; + let api = Api::new(*b"POLY", current_spec_version); + + Module::::upgrade_api( + RawOrigin::Root.into(), + api.clone(), + next_upgrade.clone(), + ).unwrap(); + + let encoded_input = (0..r * CHAIN_EXTENSION_BATCH_SIZE) + .map(|_| { + api.encode() + }) + .collect::>(); + let input_bytes = encoded_input.iter().flat_map(|a| a.clone()).collect::>(); + + let contract = Contract::::chain_extension( + r * CHAIN_EXTENSION_BATCH_SIZE, + FuncId::GetLatestApiUpgrade, + input_bytes, + output_len + ); + }: { contract.call(); } } diff --git a/pallets/contracts/src/chain_extension.rs b/pallets/contracts/src/chain_extension.rs index bacaa65651..2db720df0a 100644 --- a/pallets/contracts/src/chain_extension.rs +++ b/pallets/contracts/src/chain_extension.rs @@ -78,6 +78,7 @@ pub enum FuncId { GetTransactionVersion, GetKeyDid, KeyHasher(KeyHasher, HashSize), + GetLatestApiUpgrade, /// Deprecated Polymesh (<=5.0) chain extensions. OldCallRuntime(ExtrinsicId), @@ -99,6 +100,7 @@ impl FuncId { 0x10 => Some(Self::KeyHasher(KeyHasher::Twox, HashSize::B64)), 0x11 => Some(Self::KeyHasher(KeyHasher::Twox, HashSize::B128)), 0x12 => Some(Self::KeyHasher(KeyHasher::Twox, HashSize::B256)), + 0x13 => Some(Self::GetLatestApiUpgrade), _ => None, }, 0x1A => match func_id { @@ -129,6 +131,7 @@ impl Into for FuncId { Self::KeyHasher(KeyHasher::Twox, HashSize::B64) => (0x0000, 0x10), Self::KeyHasher(KeyHasher::Twox, HashSize::B128) => (0x0000, 0x11), Self::KeyHasher(KeyHasher::Twox, HashSize::B256) => (0x0000, 0x12), + Self::GetLatestApiUpgrade => (0x0000, 0x13), Self::OldCallRuntime(ExtrinsicId(ext_id, func_id)) => { (ext_id as u32, (func_id as u32) << 8) } @@ -401,6 +404,58 @@ where Ok(ce::RetVal::Converging(0)) } +fn get_latest_api_upgrade(env: ce::Environment) -> ce::Result +where + T: Config, + T::AccountId: UncheckedFrom + AsRef<[u8]>, + E: ce::Ext, +{ + let mut env = env.buf_in_buf_out(); + env.charge_weight(::WeightInfo::get_latest_api_upgrade())?; + let api: Api = env.read_as()?; + + let spec_version = T::Version::get().spec_version; + let tx_version = T::Version::get().transaction_version; + let current_chain_version = ChainVersion::new(spec_version, tx_version); + + let current_api_hash: Option> = CurrentApiHash::::get(&api); + let next_upgrade: Option> = ApiNextUpgrade::::get(&api); + let latest_api_hash = { + match next_upgrade { + Some(next_upgrade) => { + if next_upgrade.chain_version <= current_chain_version { + CurrentApiHash::::insert(&api, &next_upgrade.api_hash); + ApiNextUpgrade::::remove(&api); + Some(next_upgrade.api_hash) + } else { + current_api_hash + } + } + None => current_api_hash, + } + }; + + // If there are no upgrades found, return an error + if latest_api_hash.is_none() { + return Err(Error::::NoUpgradesSupported.into()); + } + + trace!( + target: "runtime", + "PolymeshExtension contract GetLatestApiUpgrade: {latest_api_hash:?}", + ); + let encoded_api_hash = latest_api_hash.unwrap_or_default().encode(); + env.write(&encoded_api_hash, false, None).map_err(|err| { + trace!( + target: "runtime", + "PolymeshExtension failed to write api code hash value into contract memory:{err:?}", + ); + Error::::ReadStorageFailed + })?; + + Ok(ce::RetVal::Converging(0)) +} + fn call_runtime( env: ce::Environment, old_call: Option, @@ -527,6 +582,7 @@ where Some(FuncId::GetTransactionVersion) => get_version(env, false), Some(FuncId::GetKeyDid) => get_key_did(env), Some(FuncId::KeyHasher(hasher, size)) => key_hasher(env, hasher, size), + Some(FuncId::GetLatestApiUpgrade) => get_latest_api_upgrade(env), Some(FuncId::OldCallRuntime(p)) => call_runtime(env, Some(p)), None => { trace!( @@ -560,6 +616,7 @@ mod tests { test_func_id(FuncId::KeyHasher(KeyHasher::Twox, HashSize::B64)); test_func_id(FuncId::KeyHasher(KeyHasher::Twox, HashSize::B128)); test_func_id(FuncId::KeyHasher(KeyHasher::Twox, HashSize::B256)); + test_func_id(FuncId::GetLatestApiUpgrade); test_func_id(FuncId::OldCallRuntime(ExtrinsicId(0x1A, 0x00))); test_func_id(FuncId::OldCallRuntime(ExtrinsicId(0x1A, 0x01))); test_func_id(FuncId::OldCallRuntime(ExtrinsicId(0x1A, 0x02))); diff --git a/pallets/contracts/src/lib.rs b/pallets/contracts/src/lib.rs index 2eb40f2fec..6dfda3f7fa 100644 --- a/pallets/contracts/src/lib.rs +++ b/pallets/contracts/src/lib.rs @@ -53,23 +53,24 @@ #[cfg(feature = "runtime-benchmarks")] pub mod benchmarking; -use codec::{Decode, Encode}; - pub mod chain_extension; -pub use chain_extension::{ExtrinsicId, PolymeshExtension}; +use codec::{Decode, Encode}; use frame_support::dispatch::{ DispatchError, DispatchErrorWithPostInfo, DispatchResult, DispatchResultWithPostInfo, }; +use frame_support::pallet_prelude::MaxEncodedLen; use frame_support::traits::Get; use frame_support::weights::Weight; use frame_support::{decl_error, decl_event, decl_module, decl_storage, ensure}; use frame_system::ensure_root; +use scale_info::TypeInfo; use sp_core::crypto::UncheckedFrom; use sp_runtime::traits::Hash; use sp_std::borrow::Cow; use sp_std::{vec, vec::Vec}; +pub use chain_extension::{ExtrinsicId, PolymeshExtension}; use pallet_contracts::Config as BConfig; use pallet_contracts_primitives::{Code, ContractResult}; use pallet_identity::ParentDid; @@ -85,6 +86,79 @@ type CodeHash = ::Hash; pub struct ContractPolymeshHooks; +#[derive(Clone, Debug, Decode, Encode, Eq, MaxEncodedLen, PartialEq, TypeInfo)] +pub struct Api { + desc: [u8; 4], + major: u32, +} + +impl Api { + pub fn new(desc: [u8; 4], major: u32) -> Self { + Self { desc, major } + } +} + +#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct ApiCodeHash { + pub hash: CodeHash, +} + +impl Default for ApiCodeHash { + fn default() -> Self { + Self { + hash: CodeHash::::default(), + } + } +} + +impl sp_std::fmt::Debug for ApiCodeHash { + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!(f, "hash: {:?}", self.hash) + } +} + +#[derive(Clone, Debug, Decode, Encode, Eq, Ord, PartialOrd, PartialEq, TypeInfo)] +pub struct ChainVersion { + spec_version: u32, + tx_version: u32, +} + +impl ChainVersion { + pub fn new(spec_version: u32, tx_version: u32) -> Self { + ChainVersion { + spec_version, + tx_version, + } + } +} + +#[derive(Clone, Decode, Encode, Eq, PartialEq, TypeInfo)] +#[scale_info(skip_type_params(T))] +pub struct NextUpgrade { + pub chain_version: ChainVersion, + pub api_hash: ApiCodeHash, +} + +impl NextUpgrade { + pub fn new(chain_version: ChainVersion, api_hash: ApiCodeHash) -> Self { + Self { + chain_version, + api_hash, + } + } +} + +impl sp_std::fmt::Debug for NextUpgrade { + fn fmt(&self, f: &mut sp_std::fmt::Formatter) -> sp_std::fmt::Result { + write!( + f, + "chain_version: {:?} api_hash: {:?}", + self.chain_version, self.api_hash + ) + } +} + impl pallet_contracts::PolymeshHooks for ContractPolymeshHooks where T::AccountId: UncheckedFrom + AsRef<[u8]>, @@ -158,6 +232,8 @@ pub trait WeightInfo { fn basic_runtime_call(n: u32) -> Weight; fn instantiate_with_code_as_primary_key(code_len: u32, salt_len: u32) -> Weight; fn instantiate_with_hash_as_primary_key(salt_len: u32) -> Weight; + fn chain_extension_get_latest_api_upgrade(r: u32) -> Weight; + fn upgrade_api() -> Weight; /// Computes the cost of instantiating where `code_len` /// and `salt_len` are specified in kilobytes. @@ -218,6 +294,10 @@ pub trait WeightInfo { .saturating_sub(Self::dummy_contract()) .saturating_sub(Self::basic_runtime_call(in_len)) } + + fn get_latest_api_upgrade() -> Weight { + cost_batched!(chain_extension_get_latest_api_upgrade) + } } /// The `Config` trait for the smart contracts pallet. @@ -225,7 +305,7 @@ pub trait Config: IdentityConfig + BConfig + frame_system::Config { /// The overarching event type. - type RuntimeEvent: From + Into<::RuntimeEvent>; + type RuntimeEvent: From> + Into<::RuntimeEvent>; /// Max value that `in_len` can take, that is, /// the length of the data sent from a contract when using the ChainExtension. @@ -239,10 +319,13 @@ pub trait Config: } decl_event! { - pub enum Event { - // This pallet does not directly define custom events. - // See `pallet_contracts` and `pallet_identity` - // for events currently emitted by extrinsics of this pallet. + pub enum Event + where + Hash = CodeHash, + { + /// Emitted when a contract starts supporting a new API upgrade + /// Contains the [`Api`], [`ChainVersion`], and the bytes for the code hash. + ApiHashUpdated(Api, ChainVersion, Hash) } } @@ -268,7 +351,11 @@ decl_error! { /// The caller is not a primary key. CallerNotAPrimaryKey, /// Secondary key permissions are missing. - MissingKeyPermissions + MissingKeyPermissions, + /// Only future chain versions are allowed. + InvalidChainVersion, + /// There are no api upgrades supported for the contract. + NoUpgradesSupported } } @@ -281,6 +368,12 @@ decl_storage! { map hasher(identity) ExtrinsicId => bool; /// Storage version. StorageVersion get(fn storage_version) build(|_| Version::new(1)): Version; + /// Stores the chain version and code hash for the next chain upgrade. + pub ApiNextUpgrade get(fn api_next_upgrade): + map hasher(twox_64_concat) Api => Option>; + /// Stores the code hash for the current api. + pub CurrentApiHash get (fn api_tracker): + map hasher(twox_64_concat) Api => Option>; } add_extra_genesis { config(call_whitelist): Vec; @@ -494,6 +587,15 @@ decl_module! { true ) } + + #[weight = ::WeightInfo::upgrade_api()] + pub fn upgrade_api( + origin, + api: Api, + next_upgrade: NextUpgrade + ) -> DispatchResult { + Self::base_upgrade_api(origin, api, next_upgrade) + } } } @@ -694,4 +796,30 @@ where Err(error) => Err(DispatchErrorWithPostInfo { post_info, error }), } } + + fn base_upgrade_api( + origin: T::RuntimeOrigin, + api: Api, + next_upgrade: NextUpgrade, + ) -> DispatchResult { + ensure_root(origin)?; + + let current_chain_version = ChainVersion::new( + T::Version::get().spec_version, + T::Version::get().transaction_version, + ); + + if next_upgrade.chain_version < current_chain_version { + return Err(Error::::InvalidChainVersion.into()); + } + + ApiNextUpgrade::::insert(&api, &next_upgrade); + + Self::deposit_event(Event::::ApiHashUpdated( + api, + next_upgrade.chain_version, + next_upgrade.api_hash.hash, + )); + Ok(()) + } } diff --git a/pallets/runtime/develop/src/runtime.rs b/pallets/runtime/develop/src/runtime.rs index c6f7d5aa1a..870e956ddd 100644 --- a/pallets/runtime/develop/src/runtime.rs +++ b/pallets/runtime/develop/src/runtime.rs @@ -437,7 +437,7 @@ construct_runtime!( // Contracts Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 46, - PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, + PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, // Preimage register. Used by `pallet_scheduler`. Preimage: pallet_preimage::{Pallet, Call, Storage, Event}, diff --git a/pallets/runtime/mainnet/src/runtime.rs b/pallets/runtime/mainnet/src/runtime.rs index 8a8439e0a8..f8dc9f4fb6 100644 --- a/pallets/runtime/mainnet/src/runtime.rs +++ b/pallets/runtime/mainnet/src/runtime.rs @@ -363,7 +363,7 @@ construct_runtime!( // Contracts Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 46, - PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, + PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, // Preimage register. Used by `pallet_scheduler`. Preimage: pallet_preimage::{Pallet, Call, Storage, Event}, diff --git a/pallets/runtime/testnet/src/runtime.rs b/pallets/runtime/testnet/src/runtime.rs index b7fd1d08a8..8ddf66597c 100644 --- a/pallets/runtime/testnet/src/runtime.rs +++ b/pallets/runtime/testnet/src/runtime.rs @@ -384,7 +384,7 @@ construct_runtime!( // Contracts Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 46, - PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, + PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config}, // Preimage register. Used by `pallet_scheduler`. Preimage: pallet_preimage::{Pallet, Call, Storage, Event}, diff --git a/pallets/runtime/tests/src/contracts_test.rs b/pallets/runtime/tests/src/contracts_test.rs index 22ad26013c..f897004a19 100644 --- a/pallets/runtime/tests/src/contracts_test.rs +++ b/pallets/runtime/tests/src/contracts_test.rs @@ -1,8 +1,9 @@ use codec::Encode; -use frame_support::dispatch::Weight; +use frame_support::dispatch::{DispatchError, Weight}; use frame_support::{ assert_err_ignore_postinfo, assert_noop, assert_ok, assert_storage_noop, StorageMap, }; +use polymesh_contracts::{Api, ApiCodeHash, ApiNextUpgrade, ChainVersion, NextUpgrade}; use sp_keyring::AccountKeyring; use sp_runtime::traits::Hash; @@ -12,7 +13,7 @@ use polymesh_primitives::{AccountId, Gas, Permissions, PortfolioPermissions, Tic use polymesh_runtime_common::Currency; use crate::ext_builder::ExtBuilder; -use crate::storage::{TestStorage, User}; +use crate::storage::{root, TestStorage, User}; // We leave it to tests in the substrate to ensure that `pallet-contracts` // is functioning correctly, so we do not add such redundant tests @@ -32,6 +33,7 @@ type MaxInLen = ::MaxInLen; type Balances = pallet_balances::Pallet; type Identity = pallet_identity::Module; type IdentityError = pallet_identity::Error; +type PolymeshContracts = polymesh_contracts::Pallet; /// Load a given wasm module represented by a .wat file /// and returns a wasm binary contents along with it's hash. @@ -221,3 +223,41 @@ fn deploy_as_child_identity() { assert_eq!(ParentDid::get(child_id), Some(alice.did)); }) } + +#[test] +fn upgrade_api_unauthorized_caller() { + ExtBuilder::default().build().execute_with(|| { + let alice = User::new(AccountKeyring::Alice); + let api = Api::new(*b"POLY", 6); + let chain_version = ChainVersion::new(6, 0); + let api_code_hash = ApiCodeHash { + hash: CodeHash::default(), + }; + let next_upgrade = NextUpgrade::new(chain_version, api_code_hash); + + assert_noop!( + Contracts::upgrade_api(alice.origin(), api, next_upgrade), + DispatchError::BadOrigin + ); + }) +} + +#[test] +fn upgrade_api() { + ExtBuilder::default().build().execute_with(|| { + let api = Api::new(*b"POLY", 6); + let chain_version = ChainVersion::new(6, 0); + let api_code_hash = ApiCodeHash { + hash: CodeHash::default(), + }; + let next_upgrade = NextUpgrade::new(chain_version, api_code_hash); + + assert_ok!(Contracts::upgrade_api( + root(), + api.clone(), + next_upgrade.clone() + )); + + assert_eq!(ApiNextUpgrade::get(&api).unwrap(), next_upgrade); + }) +} diff --git a/pallets/runtime/tests/src/storage.rs b/pallets/runtime/tests/src/storage.rs index 26fa57dfe1..a2d23703f4 100644 --- a/pallets/runtime/tests/src/storage.rs +++ b/pallets/runtime/tests/src/storage.rs @@ -326,7 +326,7 @@ frame_support::construct_runtime!( // Contracts Contracts: pallet_contracts::{Pallet, Call, Storage, Event} = 46, - PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config} = 47, + PolymeshContracts: polymesh_contracts::{Pallet, Call, Storage, Event, Config} = 47, // Preimage register. Used by `pallet_scheduler`. Preimage: pallet_preimage::{Pallet, Call, Storage, Event} = 48, diff --git a/pallets/weights/src/polymesh_contracts.rs b/pallets/weights/src/polymesh_contracts.rs index d1ea17e470..9b461d2f01 100644 --- a/pallets/weights/src/polymesh_contracts.rs +++ b/pallets/weights/src/polymesh_contracts.rs @@ -544,4 +544,41 @@ impl polymesh_contracts::WeightInfo for SubstrateWeight { .saturating_add(DbWeight::get().reads(23)) .saturating_add(DbWeight::get().writes(13)) } + // Storage: PolymeshContracts ApiNextUpgrade (r:0 w:1) + // Proof Skipped: PolymeshContracts ApiNextUpgrade (max_values: None, max_size: None, mode: Measured) + fn upgrade_api() -> Weight { + // Minimum execution time: 31_700 nanoseconds. + Weight::from_ref_time(32_030_000).saturating_add(DbWeight::get().writes(1)) + } + // Storage: Identity KeyRecords (r:2 w:0) + // Proof Skipped: Identity KeyRecords (max_values: None, max_size: None, mode: Measured) + // Storage: System Account (r:1 w:0) + // Proof: System Account (max_values: None, max_size: Some(128), added: 2603, mode: MaxEncodedLen) + // Storage: Contracts ContractInfoOf (r:1 w:1) + // Proof: Contracts ContractInfoOf (max_values: None, max_size: Some(290), added: 2765, mode: MaxEncodedLen) + // Storage: Contracts CodeStorage (r:1 w:0) + // Proof: Contracts CodeStorage (max_values: None, max_size: Some(126001), added: 128476, mode: MaxEncodedLen) + // Storage: Timestamp Now (r:1 w:0) + // Proof: Timestamp Now (max_values: Some(1), max_size: Some(8), added: 503, mode: MaxEncodedLen) + // Storage: Identity IsDidFrozen (r:1 w:0) + // Proof Skipped: Identity IsDidFrozen (max_values: None, max_size: None, mode: Measured) + // Storage: Instance2Group ActiveMembers (r:1 w:0) + // Proof Skipped: Instance2Group ActiveMembers (max_values: Some(1), max_size: None, mode: Measured) + // Storage: Identity Claims (r:2 w:0) + // Proof Skipped: Identity Claims (max_values: None, max_size: None, mode: Measured) + // Storage: PolymeshContracts CurrentApiHash (r:1 w:1) + // Proof Skipped: PolymeshContracts CurrentApiHash (max_values: None, max_size: None, mode: Measured) + // Storage: PolymeshContracts ApiNextUpgrade (r:1 w:1) + // Proof Skipped: PolymeshContracts ApiNextUpgrade (max_values: None, max_size: None, mode: Measured) + // Storage: System EventTopics (r:2 w:2) + // Proof Skipped: System EventTopics (max_values: None, max_size: None, mode: Measured) + /// The range of component `r` is `[0, 20]`. + fn chain_extension_get_latest_api_upgrade(r: u32) -> Weight { + // Minimum execution time: 474_616 nanoseconds. + Weight::from_ref_time(526_554_207) + // Standard Error: 566_740 + .saturating_add(Weight::from_ref_time(415_236_821).saturating_mul(r.into())) + .saturating_add(DbWeight::get().reads(14)) + .saturating_add(DbWeight::get().writes(5)) + } }