From c93e31f5aac15e6abea9c1def842e0a5f396ab61 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 27 Oct 2023 17:35:18 +0100 Subject: [PATCH 01/34] Add storage command --- crates/cargo-contract/src/cmd/mod.rs | 1 + crates/cargo-contract/src/cmd/storage.rs | 92 ++++++++++++++++++++++++ 2 files changed, 93 insertions(+) create mode 100644 crates/cargo-contract/src/cmd/storage.rs diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index aa1078f95..2489beb85 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -21,6 +21,7 @@ pub mod encode; pub mod info; pub mod instantiate; pub mod remove; +pub mod storage; pub mod upload; pub mod verify; diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs new file mode 100644 index 000000000..1f0868de3 --- /dev/null +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -0,0 +1,92 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use super::{ + basic_display_format_extended_contract_info, + display_all_contracts, + DefaultConfig, +}; +use anyhow::{ + anyhow, + Result, +}; +use contract_extrinsics::{url_to_string, Balance, CodeHash, ContractInfo, ErrorVariant, ContractArtifacts, fetch_contract_info}; +use std::{ + fmt::Debug, + io::Write, + path::PathBuf, +}; +use subxt::{ + backend::{ + legacy::LegacyRpcMethods, + rpc::RpcClient, + }, + Config, + OnlineClient, +}; + +#[derive(Debug, clap::Args)] +#[clap(name = "storage", about = "Inspect contract storage")] +pub struct StorageCommand { + /// The address of the contract to inspect storage of. + #[clap( + name = "contract", + long, + env = "CONTRACT", + required_unless_present = "all" + )] + contract: ::AccountId, + /// Path to a contract build artifact file: a raw `.wasm` file, a `.contract` bundle, + /// or a `.json` metadata file. + #[clap(value_parser, conflicts_with = "manifest_path")] + file: Option, + /// Path to the `Cargo.toml` of the contract. + #[clap(long, value_parser)] + manifest_path: Option, + /// Websockets url of a substrate node. + #[clap( + name = "url", + long, + value_parser, + default_value = "ws://localhost:9944" + )] + url: url::Url, + /// Export the storage output in JSON format. + #[clap(name = "output-json", long)] + output_json: bool, +} + +impl StorageCommand { + pub async fn run(&self) -> Result<(), ErrorVariant> { + let rpc_cli = RpcClient::from_url(url_to_string(&self.url)).await?; + let client = + OnlineClient::::from_rpc_client(rpc_cli.clone()).await?; + let rpc = LegacyRpcMethods::::new(rpc_cli.clone()); + + let contract_artifacts = ContractArtifacts::from_manifest_or_file( + self.manifest_path.as_ref(), + self.file.as_ref(), + )?; + + let contract_info = fetch_contract_info(&self.contract, &rpc, &client).await? + .ok_or(anyhow!( + "No contract information was found for account id {}", + contract + ))?; + + Ok(()) + } +} From 345441eb9d830bf278c492461e2c930adc5a5972 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 9 Nov 2023 09:23:06 +0000 Subject: [PATCH 02/34] WIP separate contract_info.rs --- crates/cargo-contract/src/cmd/storage.rs | 30 +++- crates/extrinsics/src/contract_info.rs | 177 +++++++++++++++++++++++ 2 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 crates/extrinsics/src/contract_info.rs diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 1f0868de3..19c9689c0 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -31,8 +31,8 @@ use std::{ }; use subxt::{ backend::{ - legacy::LegacyRpcMethods, - rpc::RpcClient, + legacy::{rpc_methods::Bytes, LegacyRpcMethods}, + rpc::{RpcClient, rpc_params}, }, Config, OnlineClient, @@ -84,9 +84,33 @@ impl StorageCommand { let contract_info = fetch_contract_info(&self.contract, &rpc, &client).await? .ok_or(anyhow!( "No contract information was found for account id {}", - contract + self.contract ))?; + let trie_id = hex::decode(contract_info.trie_id())?; + let prefixed_storage_key = sp_core::storage::ChildInfo::new_default(&trie_id).into_prefixed_storage_key(); + Ok(()) } + + /// Fetch the raw bytes for a given storage key + pub async fn state_get_storage( + client: &RpcClient, + prefixed_storage_key: sp_core::storage::PrefixedStorageKey, + key: &[u8], + hash: Option<::Hash>, + ) -> Result>, subxt::Error> { + // todo: add jsonrpc dependency. + let params = rpc_params![to_hex(key), hash]; + let data: Option = self.client.request("childstate_getStorage", params).await?; + Ok(data.map(|b| b.0)) + } + + // #[method(name = "childstate_getStorage", blocking)] + // fn storage( + // &self, + // child_storage_key: PrefixedStorageKey, + // key: StorageKey, + // hash: Option, + // ) -> RpcResult>; } diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs new file mode 100644 index 000000000..1622e988e --- /dev/null +++ b/crates/extrinsics/src/contract_info.rs @@ -0,0 +1,177 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use colored::Colorize; +use env_check::compare_node_env_with_contract; +use subxt::utils::AccountId32; + +use anyhow::{ + anyhow, + Context, + Result, +}; +use std::path::PathBuf; + +use crate::runtime_api::api; +use contract_build::{ + CrateMetadata, + DEFAULT_KEY_COL_WIDTH, +}; +use scale::{ + Decode, + Encode, +}; +use subxt::{ + blocks, + config, + tx, + Config, + OnlineClient, +}; +use subxt_signer::sr25519::Keypair; + +use std::{ + option::Option, + path::Path, +}; +use subxt::backend::legacy::LegacyRpcMethods; + +/// Fetch the contract info from the storage using the provided client. +pub async fn fetch_contract_info( + contract: &AccountId32, + rpc: &LegacyRpcMethods, + client: &Client, +) -> Result> { + let info_contract_call = api::storage().contracts().contract_info_of(contract); + + let best_block = get_best_block(rpc).await?; + + let contract_info_of = client + .storage() + .at(best_block) + .fetch(&info_contract_call) + .await?; + + match contract_info_of { + Some(info_result) => { + let convert_trie_id = hex::encode(info_result.trie_id.0); + Ok(Some(ContractInfo { + trie_id: convert_trie_id, + code_hash: info_result.code_hash, + storage_items: info_result.storage_items, + storage_item_deposit: info_result.storage_item_deposit, + })) + } + None => Ok(None), + } +} + +#[derive(serde::Serialize)] +pub struct ContractInfo { + trie_id: String, + code_hash: CodeHash, + storage_items: u32, + storage_item_deposit: Balance, +} + +impl ContractInfo { + /// Convert and return contract info in JSON format. + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } + + /// Return the trie_id of the contract. + pub fn trie_id(&self) -> &str { + &self.trie_id + } + + /// Return the code_hash of the contract. + pub fn code_hash(&self) -> &CodeHash { + &self.code_hash + } + + /// Return the number of storage items of the contract. + pub fn storage_items(&self) -> u32 { + self.storage_items + } + + /// Return the storage item deposit of the contract. + pub fn storage_item_deposit(&self) -> Balance { + self.storage_item_deposit + } +} + +/// Fetch the contract wasm code from the storage using the provided client and code hash. +pub async fn fetch_wasm_code( + client: &Client, + rpc: &LegacyRpcMethods, + hash: &CodeHash, +) -> Result>> { + let pristine_code_address = api::storage().contracts().pristine_code(hash); + let best_block = get_best_block(rpc).await?; + + let pristine_bytes = client + .storage() + .at(best_block) + .fetch(&pristine_code_address) + .await? + .map(|v| v.0); + + Ok(pristine_bytes) +} + +/// Parse a contract account address from a storage key. Returns error if a key is +/// malformated. +fn parse_contract_account_address( + storage_contract_account_key: &[u8], + storage_contract_root_key_len: usize, +) -> Result { + // storage_contract_account_key is a concatenation of contract_info_of root key and + // Twox64Concat(AccountId) + let mut account = storage_contract_account_key + .get(storage_contract_root_key_len + 8..) + .ok_or(anyhow!("Unexpected storage key size"))?; + AccountId32::decode(&mut account) + .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) +} + +/// Fetch all contract addresses from the storage using the provided client and count of +/// requested elements starting from an optional address +pub async fn fetch_all_contracts( + client: &Client, + rpc: &LegacyRpcMethods, +) -> Result> { + let root_key = api::storage() + .contracts() + .contract_info_of_iter() + .to_root_bytes(); + + let best_block = get_best_block(rpc).await?; + let mut keys = client + .storage() + .at(best_block) + .fetch_raw_keys(root_key.clone()) + .await?; + + let mut contract_accounts = Vec::new(); + while let Some(result) = keys.next().await { + let key = result?; + let contract_account = parse_contract_account_address(&key, root_key.len())?; + contract_accounts.push(contract_account); + } + + Ok(contract_accounts) +} From c2e6a609e8e9a1001fbd9c8c4bc383070f706cbb Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 9 Nov 2023 09:34:08 +0000 Subject: [PATCH 03/34] Extract contract_info queries to separate mod --- crates/extrinsics/src/contract_info.rs | 163 +++++++++++++++++++++++++ crates/extrinsics/src/lib.rs | 139 ++------------------- 2 files changed, 172 insertions(+), 130 deletions(-) create mode 100644 crates/extrinsics/src/contract_info.rs diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs new file mode 100644 index 000000000..405dd6d04 --- /dev/null +++ b/crates/extrinsics/src/contract_info.rs @@ -0,0 +1,163 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use anyhow::{ + anyhow, + Result, +}; + +use super::{ + get_best_block, + runtime_api::api, + Balance, + Client, + CodeHash, + DefaultConfig, +}; + +use scale::Decode; +use std::option::Option; +use subxt::{ + backend::legacy::LegacyRpcMethods, + utils::AccountId32, +}; + +/// Fetch the contract info from the storage using the provided client. +pub async fn fetch_contract_info( + contract: &AccountId32, + rpc: &LegacyRpcMethods, + client: &Client, +) -> Result> { + let info_contract_call = api::storage().contracts().contract_info_of(contract); + + let best_block = get_best_block(rpc).await?; + + let contract_info_of = client + .storage() + .at(best_block) + .fetch(&info_contract_call) + .await?; + + match contract_info_of { + Some(info_result) => { + let convert_trie_id = hex::encode(info_result.trie_id.0); + Ok(Some(ContractInfo { + trie_id: convert_trie_id, + code_hash: info_result.code_hash, + storage_items: info_result.storage_items, + storage_item_deposit: info_result.storage_item_deposit, + })) + } + None => Ok(None), + } +} + +#[derive(serde::Serialize)] +pub struct ContractInfo { + trie_id: String, + code_hash: CodeHash, + storage_items: u32, + storage_item_deposit: Balance, +} + +impl ContractInfo { + /// Convert and return contract info in JSON format. + pub fn to_json(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } + + /// Return the trie_id of the contract. + pub fn trie_id(&self) -> &str { + &self.trie_id + } + + /// Return the code_hash of the contract. + pub fn code_hash(&self) -> &CodeHash { + &self.code_hash + } + + /// Return the number of storage items of the contract. + pub fn storage_items(&self) -> u32 { + self.storage_items + } + + /// Return the storage item deposit of the contract. + pub fn storage_item_deposit(&self) -> Balance { + self.storage_item_deposit + } +} + +/// Fetch the contract wasm code from the storage using the provided client and code hash. +pub async fn fetch_wasm_code( + client: &Client, + rpc: &LegacyRpcMethods, + hash: &CodeHash, +) -> Result>> { + let pristine_code_address = api::storage().contracts().pristine_code(hash); + let best_block = get_best_block(rpc).await?; + + let pristine_bytes = client + .storage() + .at(best_block) + .fetch(&pristine_code_address) + .await? + .map(|v| v.0); + + Ok(pristine_bytes) +} + +/// Parse a contract account address from a storage key. Returns error if a key is +/// malformated. +fn parse_contract_account_address( + storage_contract_account_key: &[u8], + storage_contract_root_key_len: usize, +) -> Result { + // storage_contract_account_key is a concatenation of contract_info_of root key and + // Twox64Concat(AccountId) + let mut account = storage_contract_account_key + .get(storage_contract_root_key_len + 8..) + .ok_or(anyhow!("Unexpected storage key size"))?; + AccountId32::decode(&mut account) + .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) +} + +/// Fetch all contract addresses from the storage using the provided client and count of +/// requested elements starting from an optional address +pub async fn fetch_all_contracts( + client: &Client, + rpc: &LegacyRpcMethods, +) -> Result> { + let root_key = api::storage() + .contracts() + .contract_info_of_iter() + .to_root_bytes(); + + let best_block = get_best_block(rpc).await?; + let mut keys = client + .storage() + .at(best_block) + .fetch_raw_keys(root_key.clone()) + .await?; + + let mut contract_accounts = Vec::new(); + while let Some(result) = keys.next().await { + let key = result?; + let contract_account = parse_contract_account_address(&key, root_key.len())?; + contract_accounts.push(contract_account); + } + + Ok(contract_accounts) +} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index a777df673..35c91a8af 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -16,6 +16,7 @@ mod balance; mod call; +mod contract_info; mod env_check; mod error; mod events; @@ -31,7 +32,6 @@ mod integration_tests; use colored::Colorize; use env_check::compare_node_env_with_contract; -use subxt::utils::AccountId32; use anyhow::{ anyhow, @@ -40,7 +40,6 @@ use anyhow::{ }; use std::path::PathBuf; -use crate::runtime_api::api; use contract_build::{ CrateMetadata, DEFAULT_KEY_COL_WIDTH, @@ -50,9 +49,11 @@ use scale::{ Encode, }; use subxt::{ + backend::legacy::LegacyRpcMethods, blocks, config, tx, + utils::AccountId32, Config, OnlineClient, }; @@ -62,7 +63,6 @@ use std::{ option::Option, path::Path, }; -use subxt::backend::legacy::LegacyRpcMethods; pub use balance::{ BalanceVariant, @@ -73,6 +73,12 @@ pub use call::{ CallExec, CallRequest, }; +pub use contract_info::{ + fetch_all_contracts, + fetch_contract_info, + fetch_wasm_code, + ContractInfo, +}; use contract_metadata::ContractMetadata; pub use contract_transcode::ContractMessageTranscoder; pub use error::{ @@ -342,36 +348,6 @@ async fn get_best_block( .ok_or(subxt::Error::Other("Best block not found".into())) } -/// Fetch the contract info from the storage using the provided client. -pub async fn fetch_contract_info( - contract: &AccountId32, - rpc: &LegacyRpcMethods, - client: &Client, -) -> Result> { - let info_contract_call = api::storage().contracts().contract_info_of(contract); - - let best_block = get_best_block(rpc).await?; - - let contract_info_of = client - .storage() - .at(best_block) - .fetch(&info_contract_call) - .await?; - - match contract_info_of { - Some(info_result) => { - let convert_trie_id = hex::encode(info_result.trie_id.0); - Ok(Some(ContractInfo { - trie_id: convert_trie_id, - code_hash: info_result.code_hash, - storage_items: info_result.storage_items, - storage_item_deposit: info_result.storage_item_deposit, - })) - } - None => Ok(None), - } -} - fn check_env_types( client: &OnlineClient, transcoder: &ContractMessageTranscoder, @@ -382,103 +358,6 @@ where compare_node_env_with_contract(client.metadata().types(), transcoder.metadata()) } -#[derive(serde::Serialize)] -pub struct ContractInfo { - trie_id: String, - code_hash: CodeHash, - storage_items: u32, - storage_item_deposit: Balance, -} - -impl ContractInfo { - /// Convert and return contract info in JSON format. - pub fn to_json(&self) -> Result { - Ok(serde_json::to_string_pretty(self)?) - } - - /// Return the trie_id of the contract. - pub fn trie_id(&self) -> &str { - &self.trie_id - } - - /// Return the code_hash of the contract. - pub fn code_hash(&self) -> &CodeHash { - &self.code_hash - } - - /// Return the number of storage items of the contract. - pub fn storage_items(&self) -> u32 { - self.storage_items - } - - /// Return the storage item deposit of the contract. - pub fn storage_item_deposit(&self) -> Balance { - self.storage_item_deposit - } -} - -/// Fetch the contract wasm code from the storage using the provided client and code hash. -pub async fn fetch_wasm_code( - client: &Client, - rpc: &LegacyRpcMethods, - hash: &CodeHash, -) -> Result>> { - let pristine_code_address = api::storage().contracts().pristine_code(hash); - let best_block = get_best_block(rpc).await?; - - let pristine_bytes = client - .storage() - .at(best_block) - .fetch(&pristine_code_address) - .await? - .map(|v| v.0); - - Ok(pristine_bytes) -} - -/// Parse a contract account address from a storage key. Returns error if a key is -/// malformated. -fn parse_contract_account_address( - storage_contract_account_key: &[u8], - storage_contract_root_key_len: usize, -) -> Result { - // storage_contract_account_key is a concatenation of contract_info_of root key and - // Twox64Concat(AccountId) - let mut account = storage_contract_account_key - .get(storage_contract_root_key_len + 8..) - .ok_or(anyhow!("Unexpected storage key size"))?; - AccountId32::decode(&mut account) - .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) -} - -/// Fetch all contract addresses from the storage using the provided client and count of -/// requested elements starting from an optional address -pub async fn fetch_all_contracts( - client: &Client, - rpc: &LegacyRpcMethods, -) -> Result> { - let root_key = api::storage() - .contracts() - .contract_info_of_iter() - .to_root_bytes(); - - let best_block = get_best_block(rpc).await?; - let mut keys = client - .storage() - .at(best_block) - .fetch_raw_keys(root_key.clone()) - .await?; - - let mut contract_accounts = Vec::new(); - while let Some(result) = keys.next().await { - let key = result?; - let contract_account = parse_contract_account_address(&key, root_key.len())?; - contract_accounts.push(contract_account); - } - - Ok(contract_accounts) -} - // Converts a Url into a String representation without excluding the default port. pub fn url_to_string(url: &url::Url) -> String { match (url.port(), url.port_or_known_default()) { From 6064a38a5e7ed375c98ff1941b367a63bff6904f Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 9 Nov 2023 09:43:39 +0000 Subject: [PATCH 04/34] Extract contract_artifacts to separate mod --- crates/extrinsics/src/contract_artifacts.rs | 157 ++++++++++++++++++++ crates/extrinsics/src/lib.rs | 142 +----------------- 2 files changed, 160 insertions(+), 139 deletions(-) create mode 100644 crates/extrinsics/src/contract_artifacts.rs diff --git a/crates/extrinsics/src/contract_artifacts.rs b/crates/extrinsics/src/contract_artifacts.rs new file mode 100644 index 000000000..5ef4f2212 --- /dev/null +++ b/crates/extrinsics/src/contract_artifacts.rs @@ -0,0 +1,157 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use super::{ + ContractMessageTranscoder, + ContractMetadata, + CrateMetadata, + WasmCode, +}; +use anyhow::{ + Context, + Result, +}; +use colored::Colorize; +use std::path::{ + Path, + PathBuf, +}; + +/// Contract artifacts for use with extrinsic commands. +#[derive(Debug)] +pub struct ContractArtifacts { + /// The original artifact path + artifacts_path: PathBuf, + /// The expected path of the file containing the contract metadata. + metadata_path: PathBuf, + /// The deserialized contract metadata if the expected metadata file exists. + metadata: Option, + /// The Wasm code of the contract if available. + pub code: Option, +} + +impl ContractArtifacts { + /// Load contract artifacts. + pub fn from_manifest_or_file( + manifest_path: Option<&PathBuf>, + file: Option<&PathBuf>, + ) -> Result { + let artifact_path = match (manifest_path, file) { + (manifest_path, None) => { + let crate_metadata = CrateMetadata::from_manifest_path( + manifest_path, + contract_build::Target::Wasm, + )?; + + if crate_metadata.contract_bundle_path().exists() { + crate_metadata.contract_bundle_path() + } else if crate_metadata.metadata_path().exists() { + crate_metadata.metadata_path() + } else { + anyhow::bail!( + "Failed to find any contract artifacts in target directory. \n\ + Run `cargo contract build --release` to generate the artifacts." + ) + } + } + (None, Some(artifact_file)) => artifact_file.clone(), + (Some(_), Some(_)) => { + anyhow::bail!("conflicting options: --manifest-path and --file") + } + }; + Self::from_artifact_path(artifact_path.as_path()) + } + /// Given a contract artifact path, load the contract code and metadata where + /// possible. + fn from_artifact_path(path: &Path) -> Result { + tracing::debug!("Loading contracts artifacts from `{}`", path.display()); + let (metadata_path, metadata, code) = + match path.extension().and_then(|ext| ext.to_str()) { + Some("contract") | Some("json") => { + let metadata = ContractMetadata::load(path)?; + let code = metadata.clone().source.wasm.map(|wasm| WasmCode(wasm.0)); + (PathBuf::from(path), Some(metadata), code) + } + Some("wasm") => { + let file_name = path.file_stem() + .context("WASM bundle file has unreadable name")? + .to_str() + .context("Error parsing filename string")?; + let code = Some(WasmCode(std::fs::read(path)?)); + let dir = path.parent().map_or_else(PathBuf::new, PathBuf::from); + let metadata_path = dir.join(format!("{file_name}.json")); + if !metadata_path.exists() { + (metadata_path, None, code) + } else { + let metadata = ContractMetadata::load(&metadata_path)?; + (metadata_path, Some(metadata), code) + } + } + Some(ext) => anyhow::bail!( + "Invalid artifact extension {ext}, expected `.contract`, `.json` or `.wasm`" + ), + None => { + anyhow::bail!( + "Artifact path has no extension, expected `.contract`, `.json`, or `.wasm`" + ) + } + }; + + if let Some(contract_metadata) = metadata.as_ref() { + if let Err(e) = contract_metadata.check_ink_compatibility() { + eprintln!("{} {}", "warning:".yellow().bold(), e.to_string().bold()); + } + } + Ok(Self { + artifacts_path: path.into(), + metadata_path, + metadata, + code, + }) + } + + /// Get the path of the artifact file used to load the artifacts. + pub fn artifact_path(&self) -> &Path { + self.artifacts_path.as_path() + } + + /// Get contract metadata, if available. + /// + /// ## Errors + /// - No contract metadata could be found. + /// - Invalid contract metadata. + pub fn metadata(&self) -> Result { + self.metadata.clone().ok_or_else(|| { + anyhow::anyhow!( + "No contract metadata found. Expected file {}", + self.metadata_path.as_path().display() + ) + }) + } + + /// Get the code hash from the contract metadata. + pub fn code_hash(&self) -> Result<[u8; 32]> { + let metadata = self.metadata()?; + Ok(metadata.source.hash.0) + } + + /// Construct a [`ContractMessageTranscoder`] from contract metadata. + pub fn contract_transcoder(&self) -> Result { + let metadata = self.metadata()?; + ContractMessageTranscoder::try_from(metadata) + .context("Failed to deserialize ink project metadata from contract metadata") + } +} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 35c91a8af..84bf5048b 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -16,6 +16,7 @@ mod balance; mod call; +mod contract_artifacts; mod contract_info; mod env_check; mod error; @@ -30,16 +31,9 @@ mod upload; #[cfg(feature = "integration-tests")] mod integration_tests; -use colored::Colorize; use env_check::compare_node_env_with_contract; -use anyhow::{ - anyhow, - Context, - Result, -}; -use std::path::PathBuf; - +use anyhow::Result; use contract_build::{ CrateMetadata, DEFAULT_KEY_COL_WIDTH, @@ -59,11 +53,6 @@ use subxt::{ }; use subxt_signer::sr25519::Keypair; -use std::{ - option::Option, - path::Path, -}; - pub use balance::{ BalanceVariant, TokenMetadata, @@ -73,6 +62,7 @@ pub use call::{ CallExec, CallRequest, }; +pub use contract_artifacts::ContractArtifacts; pub use contract_info::{ fetch_all_contracts, fetch_contract_info, @@ -116,132 +106,6 @@ pub type Client = OnlineClient; pub type Balance = u128; pub type CodeHash = ::Hash; -/// Contract artifacts for use with extrinsic commands. -#[derive(Debug)] -pub struct ContractArtifacts { - /// The original artifact path - artifacts_path: PathBuf, - /// The expected path of the file containing the contract metadata. - metadata_path: PathBuf, - /// The deserialized contract metadata if the expected metadata file exists. - metadata: Option, - /// The Wasm code of the contract if available. - pub code: Option, -} - -impl ContractArtifacts { - /// Load contract artifacts. - pub fn from_manifest_or_file( - manifest_path: Option<&PathBuf>, - file: Option<&PathBuf>, - ) -> Result { - let artifact_path = match (manifest_path, file) { - (manifest_path, None) => { - let crate_metadata = CrateMetadata::from_manifest_path( - manifest_path, - contract_build::Target::Wasm, - )?; - - if crate_metadata.contract_bundle_path().exists() { - crate_metadata.contract_bundle_path() - } else if crate_metadata.metadata_path().exists() { - crate_metadata.metadata_path() - } else { - anyhow::bail!( - "Failed to find any contract artifacts in target directory. \n\ - Run `cargo contract build --release` to generate the artifacts." - ) - } - } - (None, Some(artifact_file)) => artifact_file.clone(), - (Some(_), Some(_)) => { - anyhow::bail!("conflicting options: --manifest-path and --file") - } - }; - Self::from_artifact_path(artifact_path.as_path()) - } - /// Given a contract artifact path, load the contract code and metadata where - /// possible. - fn from_artifact_path(path: &Path) -> Result { - tracing::debug!("Loading contracts artifacts from `{}`", path.display()); - let (metadata_path, metadata, code) = - match path.extension().and_then(|ext| ext.to_str()) { - Some("contract") | Some("json") => { - let metadata = ContractMetadata::load(path)?; - let code = metadata.clone().source.wasm.map(|wasm| WasmCode(wasm.0)); - (PathBuf::from(path), Some(metadata), code) - } - Some("wasm") => { - let file_name = path.file_stem() - .context("WASM bundle file has unreadable name")? - .to_str() - .context("Error parsing filename string")?; - let code = Some(WasmCode(std::fs::read(path)?)); - let dir = path.parent().map_or_else(PathBuf::new, PathBuf::from); - let metadata_path = dir.join(format!("{file_name}.json")); - if !metadata_path.exists() { - (metadata_path, None, code) - } else { - let metadata = ContractMetadata::load(&metadata_path)?; - (metadata_path, Some(metadata), code) - } - } - Some(ext) => anyhow::bail!( - "Invalid artifact extension {ext}, expected `.contract`, `.json` or `.wasm`" - ), - None => { - anyhow::bail!( - "Artifact path has no extension, expected `.contract`, `.json`, or `.wasm`" - ) - } - }; - - if let Some(contract_metadata) = metadata.as_ref() { - if let Err(e) = contract_metadata.check_ink_compatibility() { - eprintln!("{} {}", "warning:".yellow().bold(), e.to_string().bold()); - } - } - Ok(Self { - artifacts_path: path.into(), - metadata_path, - metadata, - code, - }) - } - - /// Get the path of the artifact file used to load the artifacts. - pub fn artifact_path(&self) -> &Path { - self.artifacts_path.as_path() - } - - /// Get contract metadata, if available. - /// - /// ## Errors - /// - No contract metadata could be found. - /// - Invalid contract metadata. - pub fn metadata(&self) -> Result { - self.metadata.clone().ok_or_else(|| { - anyhow!( - "No contract metadata found. Expected file {}", - self.metadata_path.as_path().display() - ) - }) - } - - /// Get the code hash from the contract metadata. - pub fn code_hash(&self) -> Result<[u8; 32]> { - let metadata = self.metadata()?; - Ok(metadata.source.hash.0) - } - - /// Construct a [`ContractMessageTranscoder`] from contract metadata. - pub fn contract_transcoder(&self) -> Result { - let metadata = self.metadata()?; - ContractMessageTranscoder::try_from(metadata) - .context("Failed to deserialize ink project metadata from contract metadata") - } -} - /// The Wasm code of a contract. #[derive(Debug)] pub struct WasmCode(Vec); From ef2e4e61b7f8d49d4e9941a6405e5cd43ec0ad32 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 9 Nov 2023 10:39:48 +0000 Subject: [PATCH 05/34] Delete contract_info.rs --- crates/extrinsics/src/contract_info.rs | 177 ------------------------- 1 file changed, 177 deletions(-) delete mode 100644 crates/extrinsics/src/contract_info.rs diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs deleted file mode 100644 index 1622e988e..000000000 --- a/crates/extrinsics/src/contract_info.rs +++ /dev/null @@ -1,177 +0,0 @@ -// Copyright 2018-2023 Parity Technologies (UK) Ltd. -// This file is part of cargo-contract. -// -// cargo-contract is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// cargo-contract is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with cargo-contract. If not, see . - -use colored::Colorize; -use env_check::compare_node_env_with_contract; -use subxt::utils::AccountId32; - -use anyhow::{ - anyhow, - Context, - Result, -}; -use std::path::PathBuf; - -use crate::runtime_api::api; -use contract_build::{ - CrateMetadata, - DEFAULT_KEY_COL_WIDTH, -}; -use scale::{ - Decode, - Encode, -}; -use subxt::{ - blocks, - config, - tx, - Config, - OnlineClient, -}; -use subxt_signer::sr25519::Keypair; - -use std::{ - option::Option, - path::Path, -}; -use subxt::backend::legacy::LegacyRpcMethods; - -/// Fetch the contract info from the storage using the provided client. -pub async fn fetch_contract_info( - contract: &AccountId32, - rpc: &LegacyRpcMethods, - client: &Client, -) -> Result> { - let info_contract_call = api::storage().contracts().contract_info_of(contract); - - let best_block = get_best_block(rpc).await?; - - let contract_info_of = client - .storage() - .at(best_block) - .fetch(&info_contract_call) - .await?; - - match contract_info_of { - Some(info_result) => { - let convert_trie_id = hex::encode(info_result.trie_id.0); - Ok(Some(ContractInfo { - trie_id: convert_trie_id, - code_hash: info_result.code_hash, - storage_items: info_result.storage_items, - storage_item_deposit: info_result.storage_item_deposit, - })) - } - None => Ok(None), - } -} - -#[derive(serde::Serialize)] -pub struct ContractInfo { - trie_id: String, - code_hash: CodeHash, - storage_items: u32, - storage_item_deposit: Balance, -} - -impl ContractInfo { - /// Convert and return contract info in JSON format. - pub fn to_json(&self) -> Result { - Ok(serde_json::to_string_pretty(self)?) - } - - /// Return the trie_id of the contract. - pub fn trie_id(&self) -> &str { - &self.trie_id - } - - /// Return the code_hash of the contract. - pub fn code_hash(&self) -> &CodeHash { - &self.code_hash - } - - /// Return the number of storage items of the contract. - pub fn storage_items(&self) -> u32 { - self.storage_items - } - - /// Return the storage item deposit of the contract. - pub fn storage_item_deposit(&self) -> Balance { - self.storage_item_deposit - } -} - -/// Fetch the contract wasm code from the storage using the provided client and code hash. -pub async fn fetch_wasm_code( - client: &Client, - rpc: &LegacyRpcMethods, - hash: &CodeHash, -) -> Result>> { - let pristine_code_address = api::storage().contracts().pristine_code(hash); - let best_block = get_best_block(rpc).await?; - - let pristine_bytes = client - .storage() - .at(best_block) - .fetch(&pristine_code_address) - .await? - .map(|v| v.0); - - Ok(pristine_bytes) -} - -/// Parse a contract account address from a storage key. Returns error if a key is -/// malformated. -fn parse_contract_account_address( - storage_contract_account_key: &[u8], - storage_contract_root_key_len: usize, -) -> Result { - // storage_contract_account_key is a concatenation of contract_info_of root key and - // Twox64Concat(AccountId) - let mut account = storage_contract_account_key - .get(storage_contract_root_key_len + 8..) - .ok_or(anyhow!("Unexpected storage key size"))?; - AccountId32::decode(&mut account) - .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) -} - -/// Fetch all contract addresses from the storage using the provided client and count of -/// requested elements starting from an optional address -pub async fn fetch_all_contracts( - client: &Client, - rpc: &LegacyRpcMethods, -) -> Result> { - let root_key = api::storage() - .contracts() - .contract_info_of_iter() - .to_root_bytes(); - - let best_block = get_best_block(rpc).await?; - let mut keys = client - .storage() - .at(best_block) - .fetch_raw_keys(root_key.clone()) - .await?; - - let mut contract_accounts = Vec::new(); - while let Some(result) = keys.next().await { - let key = result?; - let contract_account = parse_contract_account_address(&key, root_key.len())?; - contract_accounts.push(contract_account); - } - - Ok(contract_accounts) -} From dcfe4190d2c7677002c7cf1c83c8d8cbc586253b Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 9 Nov 2023 12:19:25 +0000 Subject: [PATCH 06/34] Fetch contract root storage and wire up cmd --- crates/cargo-contract/src/cmd/info.rs | 39 ++--- crates/cargo-contract/src/cmd/mod.rs | 1 + crates/cargo-contract/src/cmd/storage.rs | 94 +++++------ crates/cargo-contract/src/main.rs | 7 + crates/extrinsics/src/contract_info.rs | 202 ++++++++++++++--------- crates/extrinsics/src/lib.rs | 4 +- 6 files changed, 195 insertions(+), 152 deletions(-) diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index 5d165d878..521102908 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -25,27 +25,17 @@ use anyhow::{ }; use contract_analyze::determine_language; use contract_extrinsics::{ - fetch_all_contracts, - fetch_contract_info, - fetch_wasm_code, - url_to_string, Balance, CodeHash, ContractInfo, + ContractInfoRpc, ErrorVariant, }; use std::{ fmt::Debug, io::Write, }; -use subxt::{ - backend::{ - legacy::LegacyRpcMethods, - rpc::RpcClient, - }, - Config, - OnlineClient, -}; +use subxt::Config; #[derive(Debug, clap::Args)] #[clap(name = "info", about = "Get infos from a contract")] @@ -79,14 +69,11 @@ pub struct InfoCommand { impl InfoCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { - let rpc_cli = RpcClient::from_url(url_to_string(&self.url)).await?; - let client = - OnlineClient::::from_rpc_client(rpc_cli.clone()).await?; - let rpc = LegacyRpcMethods::::new(rpc_cli.clone()); + let rpc = ContractInfoRpc::new(&self.url).await?; // All flag applied if self.all { - let contracts = fetch_all_contracts(&client, &rpc).await?; + let contracts = rpc.fetch_all_contracts().await?; if self.output_json { let contracts_json = serde_json::json!({ @@ -105,19 +92,19 @@ impl InfoCommand { .as_ref() .expect("Contract argument was not provided"); - let info_to_json = fetch_contract_info(contract, &rpc, &client) - .await? - .ok_or(anyhow!( + let info_to_json = + rpc.fetch_contract_info(contract).await?.ok_or(anyhow!( "No contract information was found for account id {}", contract ))?; - let wasm_code = fetch_wasm_code(&client, &rpc, info_to_json.code_hash()) - .await? - .ok_or(anyhow!( - "Contract wasm code was not found for account id {}", - contract - ))?; + let wasm_code = + rpc.fetch_wasm_code(info_to_json.code_hash()) + .await? + .ok_or(anyhow!( + "Contract wasm code was not found for account id {}", + contract + ))?; // Binary flag applied if self.binary { if self.output_json { diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 2489beb85..b87d56aae 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -38,6 +38,7 @@ pub(crate) use self::{ }, instantiate::InstantiateCommand, remove::RemoveCommand, + storage::StorageCommand, upload::UploadCommand, verify::VerifyCommand, }; diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 19c9689c0..70ca86a79 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -14,29 +14,21 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use super::{ - basic_display_format_extended_contract_info, - display_all_contracts, - DefaultConfig, -}; +use super::DefaultConfig; use anyhow::{ anyhow, Result, }; -use contract_extrinsics::{url_to_string, Balance, CodeHash, ContractInfo, ErrorVariant, ContractArtifacts, fetch_contract_info}; +use contract_extrinsics::{ + ContractArtifacts, + ContractInfoRpc, + ErrorVariant, +}; use std::{ fmt::Debug, - io::Write, path::PathBuf, }; -use subxt::{ - backend::{ - legacy::{rpc_methods::Bytes, LegacyRpcMethods}, - rpc::{RpcClient, rpc_params}, - }, - Config, - OnlineClient, -}; +use subxt::Config; #[derive(Debug, clap::Args)] #[clap(name = "storage", about = "Inspect contract storage")] @@ -64,53 +56,57 @@ pub struct StorageCommand { default_value = "ws://localhost:9944" )] url: url::Url, - /// Export the storage output in JSON format. - #[clap(name = "output-json", long)] - output_json: bool, } impl StorageCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { - let rpc_cli = RpcClient::from_url(url_to_string(&self.url)).await?; - let client = - OnlineClient::::from_rpc_client(rpc_cli.clone()).await?; - let rpc = LegacyRpcMethods::::new(rpc_cli.clone()); + let rpc = ContractInfoRpc::new(&self.url).await?; - let contract_artifacts = ContractArtifacts::from_manifest_or_file( + // todo: to be used for metadata of storage entries + let _contract_artifacts = ContractArtifacts::from_manifest_or_file( self.manifest_path.as_ref(), self.file.as_ref(), )?; - let contract_info = fetch_contract_info(&self.contract, &rpc, &client).await? - .ok_or(anyhow!( - "No contract information was found for account id {}", - self.contract - ))?; + let contract_info = + rpc.fetch_contract_info(&self.contract) + .await? + .ok_or(anyhow!( + "No contract information was found for account id {}", + self.contract + ))?; + + let child_storage_key = contract_info.prefixed_storage_key(); + let root_key = [0u8, 0, 0, 0]; + + let root_storage = rpc + .fetch_contract_storage(&child_storage_key, &root_key, None) + .await?; + + let root_cell = ContractStorageCell { + key: hex::encode(root_key), + value: hex::encode(root_storage.unwrap_or_default()), + }; - let trie_id = hex::decode(contract_info.trie_id())?; - let prefixed_storage_key = sp_core::storage::ChildInfo::new_default(&trie_id).into_prefixed_storage_key(); + let contract_storage = ContractStorage { root: root_cell }; + + println!( + "{json}", + json = serde_json::to_string_pretty(&contract_storage)? + ); Ok(()) } +} - /// Fetch the raw bytes for a given storage key - pub async fn state_get_storage( - client: &RpcClient, - prefixed_storage_key: sp_core::storage::PrefixedStorageKey, - key: &[u8], - hash: Option<::Hash>, - ) -> Result>, subxt::Error> { - // todo: add jsonrpc dependency. - let params = rpc_params![to_hex(key), hash]; - let data: Option = self.client.request("childstate_getStorage", params).await?; - Ok(data.map(|b| b.0)) - } +#[derive(serde::Serialize)] +struct ContractStorage { + root: ContractStorageCell, +} + +#[derive(serde::Serialize)] - // #[method(name = "childstate_getStorage", blocking)] - // fn storage( - // &self, - // child_storage_key: PrefixedStorageKey, - // key: StorageKey, - // hash: Option, - // ) -> RpcResult>; +struct ContractStorageCell { + key: String, + value: String, } diff --git a/crates/cargo-contract/src/main.rs b/crates/cargo-contract/src/main.rs index 9d81f213b..dd06607a5 100644 --- a/crates/cargo-contract/src/main.rs +++ b/crates/cargo-contract/src/main.rs @@ -27,6 +27,7 @@ use self::cmd::{ InfoCommand, InstantiateCommand, RemoveCommand, + StorageCommand, UploadCommand, VerifyCommand, }; @@ -138,6 +139,9 @@ enum Command { /// Display information about a contract #[clap(name = "info")] Info(InfoCommand), + /// Inspect the on-chain storage of a contract. + #[clap(name = "storage")] + Storage(StorageCommand), /// Verifies that a given contract binary matches the build result of the specified /// workspace. #[clap(name = "verify")] @@ -220,6 +224,9 @@ fn exec(cmd: Command) -> Result<()> { Command::Info(info) => { runtime.block_on(async { info.run().await.map_err(format_err) }) } + Command::Storage(storage) => { + runtime.block_on(async { storage.run().await.map_err(format_err) }) + } Command::Verify(verify) => { let result = verify.run().map_err(format_err)?; diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 405dd6d04..a58aca336 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -22,6 +22,7 @@ use anyhow::{ use super::{ get_best_block, runtime_api::api, + url_to_string, Balance, Client, CodeHash, @@ -29,39 +30,131 @@ use super::{ }; use scale::Decode; +use sp_core::storage::PrefixedStorageKey; use std::option::Option; use subxt::{ - backend::legacy::LegacyRpcMethods, + backend::{ + legacy::{ + rpc_methods::Bytes, + LegacyRpcMethods, + }, + rpc::{ + rpc_params, + RpcClient, + }, + }, utils::AccountId32, + Config, + OnlineClient, }; -/// Fetch the contract info from the storage using the provided client. -pub async fn fetch_contract_info( - contract: &AccountId32, - rpc: &LegacyRpcMethods, - client: &Client, -) -> Result> { - let info_contract_call = api::storage().contracts().contract_info_of(contract); - - let best_block = get_best_block(rpc).await?; - - let contract_info_of = client - .storage() - .at(best_block) - .fetch(&info_contract_call) - .await?; - - match contract_info_of { - Some(info_result) => { - let convert_trie_id = hex::encode(info_result.trie_id.0); - Ok(Some(ContractInfo { - trie_id: convert_trie_id, - code_hash: info_result.code_hash, - storage_items: info_result.storage_items, - storage_item_deposit: info_result.storage_item_deposit, - })) +/// Methods for querying contracts over RPC. +pub struct ContractInfoRpc { + rpc_client: RpcClient, + rpc_methods: LegacyRpcMethods, + client: Client, +} + +impl ContractInfoRpc { + /// Create a new instance of the ContractsRpc. + pub async fn new(url: &url::Url) -> Result { + let rpc_client = RpcClient::from_url(url_to_string(&url)).await?; + let client = + OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + let rpc_methods = LegacyRpcMethods::::new(rpc_client.clone()); + + Ok(Self { + rpc_client, + rpc_methods, + client, + }) + } + + /// Fetch the contract info from the storage using the provided client. + pub async fn fetch_contract_info( + &self, + contract: &AccountId32, + ) -> Result> { + let info_contract_call = api::storage().contracts().contract_info_of(contract); + + let best_block = get_best_block(&self.rpc_methods).await?; + + let contract_info_of = self + .client + .storage() + .at(best_block) + .fetch(&info_contract_call) + .await?; + + match contract_info_of { + Some(info_result) => { + let convert_trie_id = hex::encode(info_result.trie_id.0); + Ok(Some(ContractInfo { + trie_id: convert_trie_id, + code_hash: info_result.code_hash, + storage_items: info_result.storage_items, + storage_item_deposit: info_result.storage_item_deposit, + })) + } + None => Ok(None), + } + } + + pub async fn fetch_contract_storage( + &self, + child_storage_key: &PrefixedStorageKey, + key: &[u8], + block_hash: Option<::Hash>, + ) -> Result>> { + let params = rpc_params![child_storage_key, hex::encode(key), block_hash]; + let data: Option = self + .rpc_client + .request("childstate_getStorage", params) + .await?; + Ok(data.map(|b| b.0)) + } + + /// Fetch the contract wasm code from the storage using the provided client and code + /// hash. + pub async fn fetch_wasm_code(&self, hash: &CodeHash) -> Result>> { + let pristine_code_address = api::storage().contracts().pristine_code(hash); + let best_block = get_best_block(&self.rpc_methods).await?; + + let pristine_bytes = self + .client + .storage() + .at(best_block) + .fetch(&pristine_code_address) + .await? + .map(|v| v.0); + + Ok(pristine_bytes) + } + + /// Fetch all contract addresses from the storage using the provided client and count + /// of requested elements starting from an optional address + pub async fn fetch_all_contracts(&self) -> Result> { + let root_key = api::storage() + .contracts() + .contract_info_of_iter() + .to_root_bytes(); + + let best_block = get_best_block(&self.rpc_methods).await?; + let mut keys = self + .client + .storage() + .at(best_block) + .fetch_raw_keys(root_key.clone()) + .await?; + + let mut contract_accounts = Vec::new(); + while let Some(result) = keys.next().await { + let key = result?; + let contract_account = parse_contract_account_address(&key, root_key.len())?; + contract_accounts.push(contract_account); } - None => Ok(None), + + Ok(contract_accounts) } } @@ -98,29 +191,18 @@ impl ContractInfo { pub fn storage_item_deposit(&self) -> Balance { self.storage_item_deposit } -} -/// Fetch the contract wasm code from the storage using the provided client and code hash. -pub async fn fetch_wasm_code( - client: &Client, - rpc: &LegacyRpcMethods, - hash: &CodeHash, -) -> Result>> { - let pristine_code_address = api::storage().contracts().pristine_code(hash); - let best_block = get_best_block(rpc).await?; - - let pristine_bytes = client - .storage() - .at(best_block) - .fetch(&pristine_code_address) - .await? - .map(|v| v.0); - - Ok(pristine_bytes) + /// Get the prefixed storage key for the contract, used to access the contract's + /// storage + pub fn prefixed_storage_key(&self) -> PrefixedStorageKey { + let trie_id = hex::decode(&self.trie_id) + .expect("trie_id should be valid hex encoded bytes."); + sp_core::storage::ChildInfo::new_default(&trie_id).into_prefixed_storage_key() + } } /// Parse a contract account address from a storage key. Returns error if a key is -/// malformated. +/// malformed. fn parse_contract_account_address( storage_contract_account_key: &[u8], storage_contract_root_key_len: usize, @@ -133,31 +215,3 @@ fn parse_contract_account_address( AccountId32::decode(&mut account) .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) } - -/// Fetch all contract addresses from the storage using the provided client and count of -/// requested elements starting from an optional address -pub async fn fetch_all_contracts( - client: &Client, - rpc: &LegacyRpcMethods, -) -> Result> { - let root_key = api::storage() - .contracts() - .contract_info_of_iter() - .to_root_bytes(); - - let best_block = get_best_block(rpc).await?; - let mut keys = client - .storage() - .at(best_block) - .fetch_raw_keys(root_key.clone()) - .await?; - - let mut contract_accounts = Vec::new(); - while let Some(result) = keys.next().await { - let key = result?; - let contract_account = parse_contract_account_address(&key, root_key.len())?; - contract_accounts.push(contract_account); - } - - Ok(contract_accounts) -} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 84bf5048b..fcd2c58e9 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -64,10 +64,8 @@ pub use call::{ }; pub use contract_artifacts::ContractArtifacts; pub use contract_info::{ - fetch_all_contracts, - fetch_contract_info, - fetch_wasm_code, ContractInfo, + ContractInfoRpc, }; use contract_metadata::ContractMetadata; pub use contract_transcode::ContractMessageTranscoder; From 4d79680e9a1b45478b162f1cd9a4bf80887646d9 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 10 Nov 2023 11:44:05 +0000 Subject: [PATCH 07/34] Fetch contract storage for root --- Cargo.lock | 1 + crates/cargo-contract/src/cmd/storage.rs | 21 ++++++-- crates/extrinsics/Cargo.toml | 1 + crates/extrinsics/src/contract_info.rs | 64 +++++++++++++++++++++++- crates/extrinsics/src/lib.rs | 1 + crates/transcode/Cargo.toml | 2 +- 6 files changed, 83 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d4ea586c6..691263346 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1034,6 +1034,7 @@ version = "4.0.0-alpha" dependencies = [ "anyhow", "assert_cmd", + "blake2", "clap", "colored", "contract-build", diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 70ca86a79..f9d85433b 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -22,6 +22,7 @@ use anyhow::{ use contract_extrinsics::{ ContractArtifacts, ContractInfoRpc, + ContractStorageKey, ErrorVariant, }; use std::{ @@ -77,15 +78,27 @@ impl StorageCommand { ))?; let child_storage_key = contract_info.prefixed_storage_key(); - let root_key = [0u8, 0, 0, 0]; + let root_key = ContractStorageKey::new([0u8, 0, 0, 0]); + + let storage_keys = rpc + .fetch_storage_keys_paged(&child_storage_key, None, 100, None, None) + .await?; + + for storage_key in storage_keys { + println!("storage key: {}", hex::encode(storage_key)); + } let root_storage = rpc .fetch_contract_storage(&child_storage_key, &root_key, None) - .await?; + .await? + .ok_or(anyhow!( + "No contract storage was found for account id {}", + self.contract + ))?; let root_cell = ContractStorageCell { - key: hex::encode(root_key), - value: hex::encode(root_storage.unwrap_or_default()), + key: root_key.hashed_to_hex(), + value: hex::encode(root_storage), }; let contract_storage = ContractStorage { root: root_cell }; diff --git a/crates/extrinsics/Cargo.toml b/crates/extrinsics/Cargo.toml index 170b5e901..06179a121 100644 --- a/crates/extrinsics/Cargo.toml +++ b/crates/extrinsics/Cargo.toml @@ -20,6 +20,7 @@ contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } contract-transcode = { version = "4.0.0-alpha", path = "../transcode" } anyhow = "1.0.75" +blake2 = { version = "0.10.6", default-features = false } clap = { version = "4.4.7", features = ["derive", "env"] } futures = { version = "0.3.29", default-features = false, features = ["std"] } tracing = "0.1.40" diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index a58aca336..4b4c498ec 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -103,10 +103,12 @@ impl ContractInfoRpc { pub async fn fetch_contract_storage( &self, child_storage_key: &PrefixedStorageKey, - key: &[u8], + key: &ContractStorageKey, block_hash: Option<::Hash>, ) -> Result>> { - let params = rpc_params![child_storage_key, hex::encode(key), block_hash]; + let key_hex = key.hashed_to_hex(); + tracing::debug!("fetch_contract_storage: child_storage_key: {child_storage_key:?} for key: {key_hex:?}"); + let params = rpc_params![child_storage_key, key_hex, block_hash]; let data: Option = self .rpc_client .request("childstate_getStorage", params) @@ -114,6 +116,30 @@ impl ContractInfoRpc { Ok(data.map(|b| b.0)) } + pub async fn fetch_storage_keys_paged( + &self, + child_storage_key: &PrefixedStorageKey, + prefix: Option<&[u8]>, + count: u32, + start_key: Option<&[u8]>, + block_hash: Option<::Hash>, + ) -> Result>> { + let prefix_hex = prefix.map(|p| format!("0x{}", hex::encode(p))); + let start_key_hex = start_key.map(|p| format!("0x{}", hex::encode(p))); + let params = rpc_params![ + child_storage_key, + prefix_hex, + count, + start_key_hex, + block_hash + ]; + let data: Vec = self + .rpc_client + .request("childstate_getKeysPaged", params) + .await?; + Ok(data.into_iter().map(|b| b.0).collect()) + } + /// Fetch the contract wasm code from the storage using the provided client and code /// hash. pub async fn fetch_wasm_code(&self, hash: &CodeHash) -> Result>> { @@ -215,3 +241,37 @@ fn parse_contract_account_address( AccountId32::decode(&mut account) .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) } + +/// Represents a 32 bit storage key within a contract's storage. +pub struct ContractStorageKey { + raw: [u8; 4], +} + +impl ContractStorageKey { + /// Create a new instance of the ContractStorageKey. + pub fn new(raw: [u8; 4]) -> Self { + Self { raw } + } + + /// Returns the hex encoded hashed `blake2_128_concat` representation of the storage + /// key. + pub fn hashed_to_hex(&self) -> String { + use blake2::digest::{ + consts::U16, + Digest as _, + }; + + let mut blake2_128 = blake2::Blake2b::::new(); + blake2_128.update(&self.raw); + let result = blake2_128.finalize(); + + let concat = result + .as_slice() + .iter() + .chain(self.raw.iter()) + .cloned() + .collect::>(); + + hex::encode(concat) + } +} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index fcd2c58e9..9cd50227c 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -66,6 +66,7 @@ pub use contract_artifacts::ContractArtifacts; pub use contract_info::{ ContractInfo, ContractInfoRpc, + ContractStorageKey, }; use contract_metadata::ContractMetadata; pub use contract_transcode::ContractMessageTranscoder; diff --git a/crates/transcode/Cargo.toml b/crates/transcode/Cargo.toml index 03e1d8b4f..8ff3ad937 100644 --- a/crates/transcode/Cargo.toml +++ b/crates/transcode/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] anyhow = "1.0.75" base58 = { version = "0.2.0" } -blake2 = { version = "0.10.4", default-features = false } +blake2 = { version = "0.10.6", default-features = false } contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } escape8259 = "0.5.2" hex = "0.4.3" From f682131646898d78a2eac2b4a13702db709fb84e Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 10 Nov 2023 18:13:04 +0000 Subject: [PATCH 08/34] Comment --- crates/extrinsics/src/contract_info.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 4b4c498ec..c6bdec3bc 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -100,6 +100,9 @@ impl ContractInfoRpc { } } + /// Fetch the contract storage at the given key. + /// + /// For more information about how storage keys are calculated see: https://use.ink/datastructures/storage-in-metadata pub async fn fetch_contract_storage( &self, child_storage_key: &PrefixedStorageKey, From c4b597279423a760d4ac0a1ea13384534ba3b57e Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 10 Nov 2023 18:14:35 +0000 Subject: [PATCH 09/34] add todo --- crates/cargo-contract/src/cmd/storage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index f9d85433b..9cf87c070 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -80,6 +80,7 @@ impl StorageCommand { let child_storage_key = contract_info.prefixed_storage_key(); let root_key = ContractStorageKey::new([0u8, 0, 0, 0]); + // todo: fetch all storage keys and map to metadata? let storage_keys = rpc .fetch_storage_keys_paged(&child_storage_key, None, 100, None, None) .await?; From 8b38c19f0a8b1ecafdfc650139c15f169beab63e Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 20 Nov 2023 13:40:06 +0000 Subject: [PATCH 10/34] Revert info files to `master` --- crates/cargo-contract/src/cmd/info.rs | 34 +++++--- crates/extrinsics/src/contract_info.rs | 105 ++++++++++--------------- 2 files changed, 65 insertions(+), 74 deletions(-) diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index 2b4eb46c1..492b621a3 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -25,17 +25,27 @@ use anyhow::{ }; use contract_analyze::determine_language; use contract_extrinsics::{ + fetch_all_contracts, + fetch_contract_info, + fetch_wasm_code, + url_to_string, Balance, CodeHash, ContractInfo, - ContractInfoRpc, ErrorVariant, }; use std::{ fmt::Debug, io::Write, }; -use subxt::Config; +use subxt::{ + backend::{ + legacy::LegacyRpcMethods, + rpc::RpcClient, + }, + Config, + OnlineClient, +}; #[derive(Debug, clap::Args)] #[clap(name = "info", about = "Get infos from a contract")] @@ -69,11 +79,14 @@ pub struct InfoCommand { impl InfoCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { - let rpc = ContractInfoRpc::new(&self.url).await?; + let rpc_cli = RpcClient::from_url(url_to_string(&self.url)).await?; + let client = + OnlineClient::::from_rpc_client(rpc_cli.clone()).await?; + let rpc = LegacyRpcMethods::::new(rpc_cli.clone()); // All flag applied if self.all { - let contracts = rpc.fetch_all_contracts().await?; + let contracts = fetch_all_contracts(&client, &rpc).await?; if self.output_json { let contracts_json = serde_json::json!({ @@ -94,13 +107,12 @@ impl InfoCommand { let info_to_json = fetch_contract_info(contract, &rpc, &client).await?; - let wasm_code = - rpc.fetch_wasm_code(info_to_json.code_hash()) - .await? - .ok_or(anyhow!( - "Contract wasm code was not found for account id {}", - contract - ))?; + let wasm_code = fetch_wasm_code(&client, &rpc, info_to_json.code_hash()) + .await? + .ok_or(anyhow!( + "Contract wasm code was not found for account id {}", + contract + ))?; // Binary flag applied if self.binary { if self.output_json { diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 1137109d2..6294e5fd9 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -22,7 +22,6 @@ use anyhow::{ use super::{ get_best_block, runtime_api::api, - url_to_string, Balance, Client, CodeHash, @@ -30,7 +29,6 @@ use super::{ }; use scale::Decode; -use sp_core::storage::PrefixedStorageKey; use std::option::Option; use subxt::{ backend::legacy::LegacyRpcMethods, @@ -41,32 +39,8 @@ use subxt::{ }, storage::dynamic, utils::AccountId32, - Config, - OnlineClient, }; -/// Methods for querying contracts over RPC. -pub struct ContractInfoRpc { - rpc_client: RpcClient, - rpc_methods: LegacyRpcMethods, - client: Client, -} - -impl ContractInfoRpc { - /// Create a new instance of the ContractsRpc. - pub async fn new(url: &url::Url) -> Result { - let rpc_client = RpcClient::from_url(url_to_string(&url)).await?; - let client = - OnlineClient::::from_rpc_client(rpc_client.clone()).await?; - let rpc_methods = LegacyRpcMethods::::new(rpc_client.clone()); - - Ok(Self { - rpc_client, - rpc_methods, - client, - }) - } - /// Return the account data for an account ID. async fn get_account_balance( account: &AccountId32, @@ -226,18 +200,29 @@ impl ContractInfo { pub fn storage_total_deposit(&self) -> Balance { self.storage_total_deposit } +} - /// Get the prefixed storage key for the contract, used to access the contract's - /// storage - pub fn prefixed_storage_key(&self) -> PrefixedStorageKey { - let trie_id = hex::decode(&self.trie_id) - .expect("trie_id should be valid hex encoded bytes."); - sp_core::storage::ChildInfo::new_default(&trie_id).into_prefixed_storage_key() - } +/// Fetch the contract wasm code from the storage using the provided client and code hash. +pub async fn fetch_wasm_code( + client: &Client, + rpc: &LegacyRpcMethods, + hash: &CodeHash, +) -> Result>> { + let pristine_code_address = api::storage().contracts().pristine_code(hash); + let best_block = get_best_block(rpc).await?; + + let pristine_bytes = client + .storage() + .at(best_block) + .fetch(&pristine_code_address) + .await? + .map(|v| v.0); + + Ok(pristine_bytes) } /// Parse a contract account address from a storage key. Returns error if a key is -/// malformed. +/// malformated. fn parse_contract_account_address( storage_contract_account_key: &[u8], storage_contract_root_key_len: usize, @@ -251,38 +236,32 @@ fn parse_contract_account_address( .map_err(|err| anyhow!("AccountId deserialization error: {}", err)) } -/// Represents a 32 bit storage key within a contract's storage. -pub struct ContractStorageKey { - raw: [u8; 4], -} +/// Fetch all contract addresses from the storage using the provided client and count of +/// requested elements starting from an optional address +pub async fn fetch_all_contracts( + client: &Client, + rpc: &LegacyRpcMethods, +) -> Result> { + let root_key = api::storage() + .contracts() + .contract_info_of_iter() + .to_root_bytes(); -impl ContractStorageKey { - /// Create a new instance of the ContractStorageKey. - pub fn new(raw: [u8; 4]) -> Self { - Self { raw } + let best_block = get_best_block(rpc).await?; + let mut keys = client + .storage() + .at(best_block) + .fetch_raw_keys(root_key.clone()) + .await?; + + let mut contract_accounts = Vec::new(); + while let Some(result) = keys.next().await { + let key = result?; + let contract_account = parse_contract_account_address(&key, root_key.len())?; + contract_accounts.push(contract_account); } - /// Returns the hex encoded hashed `blake2_128_concat` representation of the storage - /// key. - pub fn hashed_to_hex(&self) -> String { - use blake2::digest::{ - consts::U16, - Digest as _, - }; - - let mut blake2_128 = blake2::Blake2b::::new(); - blake2_128.update(&self.raw); - let result = blake2_128.finalize(); - - let concat = result - .as_slice() - .iter() - .chain(self.raw.iter()) - .cloned() - .collect::>(); - - hex::encode(concat) - } + Ok(contract_accounts) } /// A struct used in the storage reads to access account info. From e9492b2865437c751e675fd75d2591deca1ce126 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 20 Nov 2023 15:21:05 +0000 Subject: [PATCH 11/34] Move to separate contract_storage.rs mod. --- crates/cargo-contract/src/cmd/info.rs | 5 +- crates/cargo-contract/src/cmd/mod.rs | 6 +- crates/cargo-contract/src/cmd/storage.rs | 18 +-- crates/extrinsics/src/contract_info.rs | 46 +++++-- crates/extrinsics/src/contract_storage.rs | 156 ++++++++++++++++++++++ crates/extrinsics/src/lib.rs | 11 +- crates/metadata/src/lib.rs | 2 +- 7 files changed, 216 insertions(+), 28 deletions(-) create mode 100644 crates/extrinsics/src/contract_storage.rs diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index 492b621a3..ef4e2a82b 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -33,6 +33,7 @@ use contract_extrinsics::{ CodeHash, ContractInfo, ErrorVariant, + TrieId, }; use std::{ fmt::Debug, @@ -146,7 +147,7 @@ impl InfoCommand { #[derive(serde::Serialize)] pub struct ExtendedContractInfo { - pub trie_id: String, + pub trie_id: TrieId, pub code_hash: CodeHash, pub storage_items: u32, pub storage_items_deposit: Balance, @@ -161,7 +162,7 @@ impl ExtendedContractInfo { None => "Unknown".to_string(), }; ExtendedContractInfo { - trie_id: contract_info.trie_id().to_string(), + trie_id: contract_info.trie_id().clone(), code_hash: *contract_info.code_hash(), storage_items: contract_info.storage_items(), storage_items_deposit: contract_info.storage_items_deposit(), diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 02e8934b2..712f0e3cb 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -225,7 +225,11 @@ pub fn print_gas_required_success(gas: Weight) { /// Display contract information in a formatted way pub fn basic_display_format_extended_contract_info(info: &ExtendedContractInfo) { - name_value_println!("TrieId", format!("{}", info.trie_id), MAX_KEY_COL_WIDTH); + name_value_println!( + "TrieId", + format!("{}", info.trie_id.to_hex()), + MAX_KEY_COL_WIDTH + ); name_value_println!( "Code Hash", format!("{:?}", info.code_hash), diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 9cf87c070..91ac208e3 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -21,8 +21,8 @@ use anyhow::{ }; use contract_extrinsics::{ ContractArtifacts, - ContractInfoRpc, ContractStorageKey, + ContractStorageRpc, ErrorVariant, }; use std::{ @@ -61,7 +61,7 @@ pub struct StorageCommand { impl StorageCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { - let rpc = ContractInfoRpc::new(&self.url).await?; + let rpc = ContractStorageRpc::new(&self.url).await?; // todo: to be used for metadata of storage entries let _contract_artifacts = ContractArtifacts::from_manifest_or_file( @@ -69,20 +69,14 @@ impl StorageCommand { self.file.as_ref(), )?; - let contract_info = - rpc.fetch_contract_info(&self.contract) - .await? - .ok_or(anyhow!( - "No contract information was found for account id {}", - self.contract - ))?; + let contract_info = rpc.fetch_contract_info(&self.contract).await?; - let child_storage_key = contract_info.prefixed_storage_key(); + let trie_id = contract_info.trie_id(); let root_key = ContractStorageKey::new([0u8, 0, 0, 0]); // todo: fetch all storage keys and map to metadata? let storage_keys = rpc - .fetch_storage_keys_paged(&child_storage_key, None, 100, None, None) + .fetch_storage_keys_paged(trie_id, None, 100, None, None) .await?; for storage_key in storage_keys { @@ -90,7 +84,7 @@ impl StorageCommand { } let root_storage = rpc - .fetch_contract_storage(&child_storage_key, &root_key, None) + .fetch_contract_storage(trie_id, &root_key, None) .await? .ok_or(anyhow!( "No contract storage was found for account id {}", diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 6294e5fd9..044f1433a 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -14,11 +14,6 @@ // You should have received a copy of the GNU General Public License // along with cargo-contract. If not, see . -use anyhow::{ - anyhow, - Result, -}; - use super::{ get_best_block, runtime_api::api, @@ -27,6 +22,11 @@ use super::{ CodeHash, DefaultConfig, }; +use anyhow::{ + anyhow, + Result, +}; +use contract_metadata::byte_str::serialize_as_byte_str; use scale::Decode; use std::option::Option; @@ -146,7 +146,7 @@ impl ContractInfoRaw { }; ContractInfo { - trie_id: hex::encode(&self.contract_info.trie_id.0), + trie_id: self.contract_info.trie_id.0.into(), code_hash: self.contract_info.code_hash, storage_items: self.contract_info.storage_items, storage_items_deposit: self.contract_info.storage_item_deposit, @@ -163,7 +163,7 @@ impl ContractInfoRaw { #[derive(Debug, PartialEq, serde::Serialize)] pub struct ContractInfo { - trie_id: String, + trie_id: TrieId, code_hash: CodeHash, storage_items: u32, storage_items_deposit: Balance, @@ -177,7 +177,7 @@ impl ContractInfo { } /// Return the trie_id of the contract. - pub fn trie_id(&self) -> &str { + pub fn trie_id(&self) -> &TrieId { &self.trie_id } @@ -202,6 +202,32 @@ impl ContractInfo { } } +/// A contract's child trie id. +#[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] +pub struct TrieId { + #[serde(serialize_with = "serialize_as_byte_str")] + raw: Vec, +} + +impl From> for TrieId { + fn from(raw: Vec) -> Self { + Self { raw } + } +} + +impl AsRef<[u8]> for TrieId { + fn as_ref(&self) -> &[u8] { + &self.raw + } +} + +impl TrieId { + /// Encode the trie id as hex string. + pub fn to_hex(&self) -> String { + hex::encode(&self.raw) + } +} + /// Fetch the contract wasm code from the storage using the provided client and code hash. pub async fn fetch_wasm_code( client: &Client, @@ -393,7 +419,7 @@ mod tests { assert_eq!( contract_info, ContractInfo { - trie_id: hex::encode(contract_info_v11.trie_id.0), + trie_id: contract_info_v11.trie_id.0.into(), code_hash: contract_info_v11.code_hash, storage_items: contract_info_v11.storage_items, storage_items_deposit: contract_info_v11.storage_item_deposit, @@ -458,7 +484,7 @@ mod tests { assert_eq!( contract_info, ContractInfo { - trie_id: hex::encode(contract_info_v15.trie_id.0), + trie_id: contract_info_v15.trie_id.0.into(), code_hash: contract_info_v15.code_hash, storage_items: contract_info_v15.storage_items, storage_items_deposit: contract_info_v15.storage_item_deposit, diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs new file mode 100644 index 000000000..7d5e3605e --- /dev/null +++ b/crates/extrinsics/src/contract_storage.rs @@ -0,0 +1,156 @@ +// Copyright 2018-2023 Parity Technologies (UK) Ltd. +// This file is part of cargo-contract. +// +// cargo-contract is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// cargo-contract is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with cargo-contract. If not, see . + +use anyhow::Result; + +use super::{ + fetch_contract_info, + url_to_string, + Client, + ContractInfo, + DefaultConfig, + TrieId, +}; + +use sp_core::storage::ChildInfo; +use std::option::Option; +use subxt::{ + backend::{ + legacy::{ + rpc_methods::Bytes, + LegacyRpcMethods, + }, + rpc::{ + rpc_params, + RpcClient, + }, + }, + utils::AccountId32, + Config, + OnlineClient, +}; + +/// Methods for querying contracts over RPC. +pub struct ContractStorageRpc { + rpc_client: RpcClient, + rpc_methods: LegacyRpcMethods, + client: Client, +} + +impl ContractStorageRpc { + /// Create a new instance of the ContractsRpc. + pub async fn new(url: &url::Url) -> Result { + let rpc_client = RpcClient::from_url(url_to_string(&url)).await?; + let client = + OnlineClient::::from_rpc_client(rpc_client.clone()).await?; + let rpc_methods = LegacyRpcMethods::::new(rpc_client.clone()); + + Ok(Self { + rpc_client, + rpc_methods, + client, + }) + } + + /// Fetch the contract info to access the trie id for querying storage. + pub async fn fetch_contract_info( + &self, + contract: &AccountId32, + ) -> Result { + fetch_contract_info(contract, &self.rpc_methods, &self.client).await + } + + /// Fetch the contract storage at the given key. + /// + /// For more information about how storage keys are calculated see: https://use.ink/datastructures/storage-in-metadata + pub async fn fetch_contract_storage( + &self, + trie_id: &TrieId, + key: &ContractStorageKey, + block_hash: Option<::Hash>, + ) -> Result>> { + let child_storage_key = + ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); + let key_hex = key.hashed_to_hex(); + tracing::debug!("fetch_contract_storage: child_storage_key: {child_storage_key:?} for key: {key_hex:?}"); + let params = rpc_params![child_storage_key, key_hex, block_hash]; + let data: Option = self + .rpc_client + .request("childstate_getStorage", params) + .await?; + Ok(data.map(|b| b.0)) + } + + pub async fn fetch_storage_keys_paged( + &self, + trie_id: &TrieId, + prefix: Option<&[u8]>, + count: u32, + start_key: Option<&[u8]>, + block_hash: Option<::Hash>, + ) -> Result>> { + let child_storage_key = + ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); + let prefix_hex = prefix.map(|p| format!("0x{}", hex::encode(p))); + let start_key_hex = start_key.map(|p| format!("0x{}", hex::encode(p))); + let params = rpc_params![ + child_storage_key, + prefix_hex, + count, + start_key_hex, + block_hash + ]; + let data: Vec = self + .rpc_client + .request("childstate_getKeysPaged", params) + .await?; + Ok(data.into_iter().map(|b| b.0).collect()) + } +} + +/// Represents a 32 bit storage key within a contract's storage. +pub struct ContractStorageKey { + raw: [u8; 4], +} + +impl ContractStorageKey { + /// Create a new instance of the ContractStorageKey. + pub fn new(raw: [u8; 4]) -> Self { + Self { raw } + } + + /// Returns the hex encoded hashed `blake2_128_concat` representation of the storage + /// key. + pub fn hashed_to_hex(&self) -> String { + use blake2::digest::{ + consts::U16, + Digest as _, + }; + + let mut blake2_128 = blake2::Blake2b::::new(); + blake2_128.update(&self.raw); + let result = blake2_128.finalize(); + + let concat = result + .as_slice() + .iter() + .chain(self.raw.iter()) + .cloned() + .collect::>(); + + hex::encode(concat) + } +} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 9cd50227c..1c58549a2 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -18,6 +18,7 @@ mod balance; mod call; mod contract_artifacts; mod contract_info; +mod contract_storage; mod env_check; mod error; mod events; @@ -64,11 +65,17 @@ pub use call::{ }; pub use contract_artifacts::ContractArtifacts; pub use contract_info::{ + fetch_all_contracts, + fetch_contract_info, + fetch_wasm_code, ContractInfo, - ContractInfoRpc, - ContractStorageKey, + TrieId, }; use contract_metadata::ContractMetadata; +pub use contract_storage::{ + ContractStorageKey, + ContractStorageRpc, +}; pub use contract_transcode::ContractMessageTranscoder; pub use error::{ ErrorVariant, diff --git a/crates/metadata/src/lib.rs b/crates/metadata/src/lib.rs index e0eba5f59..4b5c3b0c6 100644 --- a/crates/metadata/src/lib.rs +++ b/crates/metadata/src/lib.rs @@ -66,7 +66,7 @@ #![deny(unused_crate_dependencies)] -mod byte_str; +pub mod byte_str; pub mod compatibility; use anyhow::{ From 4eeb308e9fb8cc38da5ca5433b3a2dbc60ce6114 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 20 Nov 2023 18:27:07 +0000 Subject: [PATCH 12/34] WIP getting keys from contract metadata storage layout --- crates/cargo-contract/src/cmd/storage.rs | 8 +-- crates/extrinsics/src/contract_storage.rs | 62 ++++++++++++++++++----- crates/extrinsics/src/lib.rs | 1 + 3 files changed, 54 insertions(+), 17 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 91ac208e3..3a84c8e29 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -22,6 +22,7 @@ use anyhow::{ use contract_extrinsics::{ ContractArtifacts, ContractStorageKey, + ContractStorageLayout, ContractStorageRpc, ErrorVariant, }; @@ -63,8 +64,7 @@ impl StorageCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { let rpc = ContractStorageRpc::new(&self.url).await?; - // todo: to be used for metadata of storage entries - let _contract_artifacts = ContractArtifacts::from_manifest_or_file( + let contract_artifacts = ContractArtifacts::from_manifest_or_file( self.manifest_path.as_ref(), self.file.as_ref(), )?; @@ -72,7 +72,9 @@ impl StorageCommand { let contract_info = rpc.fetch_contract_info(&self.contract).await?; let trie_id = contract_info.trie_id(); - let root_key = ContractStorageKey::new([0u8, 0, 0, 0]); + let storage_layout = + ContractStorageLayout::try_from(contract_artifacts.metadata())?; + let root_key = storage_layout.root_key(); // todo: fetch all storage keys and map to metadata? let storage_keys = rpc diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 7d5e3605e..b21b7da5e 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -15,18 +15,14 @@ // along with cargo-contract. If not, see . use anyhow::Result; - -use super::{ - fetch_contract_info, - url_to_string, - Client, - ContractInfo, - DefaultConfig, - TrieId, +use ink_metadata::{ + layout::{ + Layout, + LayoutKey, + }, + InkProject, }; - use sp_core::storage::ChildInfo; -use std::option::Option; use subxt::{ backend::{ legacy::{ @@ -43,6 +39,34 @@ use subxt::{ OnlineClient, }; +use super::{ + fetch_contract_info, + url_to_string, + Client, + ContractInfo, + DefaultConfig, + TrieId, +}; + +pub struct ContractStorageLayout { + metadata: InkProject, + root_key: ContractStorageKey, +} + +impl ContractStorageLayout { + pub fn new(metadata: InkProject) -> Result { + if let Layout::Root(root) = metadata.layout() { + let root_key = ContractStorageKey::from(root.root_key()); + Ok(Self { metadata, root_key }) + } else { + Err(anyhow::anyhow!("No root layout found in metadata")) + } + } + pub fn root_key(&self) -> &ContractStorageKey { + &self.root_key + } +} + /// Methods for querying contracts over RPC. pub struct ContractStorageRpc { rpc_client: RpcClient, @@ -123,15 +147,25 @@ impl ContractStorageRpc { /// Represents a 32 bit storage key within a contract's storage. pub struct ContractStorageKey { - raw: [u8; 4], + raw: u32, +} + +impl From<&LayoutKey> for ContractStorageKey { + fn from(key: &LayoutKey) -> Self { + Self { raw: *key.key() } + } } impl ContractStorageKey { /// Create a new instance of the ContractStorageKey. - pub fn new(raw: [u8; 4]) -> Self { + pub fn new(raw: u32) -> Self { Self { raw } } + pub fn bytes(&self) -> [u8; 4] { + self.raw.to_be_bytes() + } + /// Returns the hex encoded hashed `blake2_128_concat` representation of the storage /// key. pub fn hashed_to_hex(&self) -> String { @@ -141,13 +175,13 @@ impl ContractStorageKey { }; let mut blake2_128 = blake2::Blake2b::::new(); - blake2_128.update(&self.raw); + blake2_128.update(&self.bytes()); let result = blake2_128.finalize(); let concat = result .as_slice() .iter() - .chain(self.raw.iter()) + .chain(self.bytes().iter()) .cloned() .collect::>(); diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 1c58549a2..9d5328f44 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -74,6 +74,7 @@ pub use contract_info::{ use contract_metadata::ContractMetadata; pub use contract_storage::{ ContractStorageKey, + ContractStorageLayout, ContractStorageRpc, }; pub use contract_transcode::ContractMessageTranscoder; From 45c45d45f5c153813010c6ae29bebe5ca64e36a3 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Tue, 21 Nov 2023 09:20:40 +0000 Subject: [PATCH 13/34] Get root storage key from metadata --- crates/cargo-contract/src/cmd/storage.rs | 2 +- crates/extrinsics/src/contract_storage.rs | 12 ++++++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 3a84c8e29..e5bce544f 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -73,7 +73,7 @@ impl StorageCommand { let trie_id = contract_info.trie_id(); let storage_layout = - ContractStorageLayout::try_from(contract_artifacts.metadata())?; + ContractStorageLayout::try_from(contract_artifacts.metadata()?)?; let root_key = storage_layout.root_key(); // todo: fetch all storage keys and map to metadata? diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index b21b7da5e..24caf3c6e 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -67,6 +67,18 @@ impl ContractStorageLayout { } } +impl TryFrom for ContractStorageLayout { + type Error = anyhow::Error; + + fn try_from( + metadata: contract_metadata::ContractMetadata, + ) -> Result { + let ink_project = + serde_json::from_value(serde_json::Value::Object(metadata.abi))?; + Self::new(ink_project) + } +} + /// Methods for querying contracts over RPC. pub struct ContractStorageRpc { rpc_client: RpcClient, From 5a5dbfd805cdebdc4fa4a88abfda6df6c016b1f5 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Tue, 21 Nov 2023 14:31:01 +0000 Subject: [PATCH 14/34] Make it generic --- crates/cargo-contract/src/cmd/storage.rs | 57 +++-------- crates/extrinsics/src/contract_artifacts.rs | 15 +++ crates/extrinsics/src/contract_info.rs | 75 +++++++++----- crates/extrinsics/src/contract_storage.rs | 105 ++++++++++++++------ crates/extrinsics/src/lib.rs | 6 +- 5 files changed, 154 insertions(+), 104 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index e5bce544f..189ad1961 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -15,13 +15,9 @@ // along with cargo-contract. If not, see . use super::DefaultConfig; -use anyhow::{ - anyhow, - Result, -}; +use anyhow::Result; use contract_extrinsics::{ ContractArtifacts, - ContractStorageKey, ContractStorageLayout, ContractStorageRpc, ErrorVariant, @@ -62,43 +58,28 @@ pub struct StorageCommand { impl StorageCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { - let rpc = ContractStorageRpc::new(&self.url).await?; + let rpc = ContractStorageRpc::::new(&self.url).await?; let contract_artifacts = ContractArtifacts::from_manifest_or_file( self.manifest_path.as_ref(), self.file.as_ref(), )?; - let contract_info = rpc.fetch_contract_info(&self.contract).await?; - - let trie_id = contract_info.trie_id(); + let ink_metadata = contract_artifacts.ink_project_metadata()?; let storage_layout = - ContractStorageLayout::try_from(contract_artifacts.metadata()?)?; - let root_key = storage_layout.root_key(); + ContractStorageLayout::::new(ink_metadata, rpc); // todo: fetch all storage keys and map to metadata? - let storage_keys = rpc - .fetch_storage_keys_paged(trie_id, None, 100, None, None) - .await?; - - for storage_key in storage_keys { - println!("storage key: {}", hex::encode(storage_key)); - } - - let root_storage = rpc - .fetch_contract_storage(trie_id, &root_key, None) - .await? - .ok_or(anyhow!( - "No contract storage was found for account id {}", - self.contract - ))?; - - let root_cell = ContractStorageCell { - key: root_key.hashed_to_hex(), - value: hex::encode(root_storage), - }; + // let storage_keys = rpc + // .fetch_storage_keys_paged(trie_id, None, 100, None, None) + // .await?; + // + // for storage_key in storage_keys { + // println!("storage key: {}", hex::encode(storage_key)); + // } - let contract_storage = ContractStorage { root: root_cell }; + let contract_storage = + storage_layout.load_contract_storage(&self.contract).await?; println!( "{json}", @@ -108,15 +89,3 @@ impl StorageCommand { Ok(()) } } - -#[derive(serde::Serialize)] -struct ContractStorage { - root: ContractStorageCell, -} - -#[derive(serde::Serialize)] - -struct ContractStorageCell { - key: String, - value: String, -} diff --git a/crates/extrinsics/src/contract_artifacts.rs b/crates/extrinsics/src/contract_artifacts.rs index 5ef4f2212..74f89a933 100644 --- a/crates/extrinsics/src/contract_artifacts.rs +++ b/crates/extrinsics/src/contract_artifacts.rs @@ -25,6 +25,7 @@ use anyhow::{ Result, }; use colored::Colorize; +use ink_metadata::InkProject; use std::path::{ Path, PathBuf, @@ -142,6 +143,20 @@ impl ContractArtifacts { }) } + /// Get the deserialized [`InkProject`] metadata. + /// + /// ## Errors + /// - No contract metadata could be found. + /// - Invalid contract metadata. + pub fn ink_project_metadata(&self) -> Result { + let metadata = self.metadata()?; + let ink_project = serde_json::from_value(serde_json::Value::Object(metadata.abi)) + .context( + "Failed to deserialize ink project metadata from contract metadata", + )?; + Ok(ink_project) + } + /// Get the code hash from the contract metadata. pub fn code_hash(&self) -> Result<[u8; 32]> { let metadata = self.metadata()?; diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 044f1433a..7061c7a02 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -27,26 +27,40 @@ use anyhow::{ Result, }; use contract_metadata::byte_str::serialize_as_byte_str; +use std::fmt::Display; use scale::Decode; use std::option::Option; use subxt::{ - backend::legacy::LegacyRpcMethods, + backend::{ + legacy::LegacyRpcMethods, + BlockRef, + }, dynamic::DecodedValueThunk, + error::DecodeError, ext::{ - scale_decode::DecodeAsType, + scale_decode::{ + DecodeAsType, + IntoVisitor, + Visitor, + }, scale_value::Value, }, storage::dynamic, utils::AccountId32, + Config, }; /// Return the account data for an account ID. -async fn get_account_balance( - account: &AccountId32, - rpc: &LegacyRpcMethods, +async fn get_account_balance( + account: &C::AccountId, + rpc: &LegacyRpcMethods, client: &Client, -) -> Result { +) -> Result +where + C::AccountId: AsRef<[u8]>, + BlockRef: From, +{ let storage_query = subxt::dynamic::storage("System", "Account", vec![Value::from_bytes(account)]); let best_block = get_best_block(rpc).await?; @@ -63,11 +77,16 @@ async fn get_account_balance( } /// Fetch the contract info from the storage using the provided client. -pub async fn fetch_contract_info( - contract: &AccountId32, - rpc: &LegacyRpcMethods, +pub async fn fetch_contract_info( + contract: &C::AccountId, + rpc: &LegacyRpcMethods, client: &Client, -) -> Result { +) -> Result +where + C::AccountId: AsRef<[u8]> + Display + IntoVisitor, + DecodeError: From<<::Visitor as Visitor>::Error>, + BlockRef: From, +{ let best_block = get_best_block(rpc).await?; let contract_info_address = dynamic( @@ -87,7 +106,8 @@ pub async fn fetch_contract_info( ) })?; - let contract_info_raw = ContractInfoRaw::new(contract.clone(), contract_info_value)?; + let contract_info_raw = + ContractInfoRaw::::new(contract.clone(), contract_info_value)?; let deposit_account = contract_info_raw.get_deposit_account(); let deposit_account_data = get_account_balance(deposit_account, rpc, client).await?; @@ -96,17 +116,22 @@ pub async fn fetch_contract_info( /// Struct representing contract info, supporting deposit on either the main or secondary /// account. -struct ContractInfoRaw { - deposit_account: AccountId32, +struct ContractInfoRaw { + deposit_account: C::AccountId, contract_info: ContractInfoOf, deposit_on_main_account: bool, } -impl ContractInfoRaw { +impl ContractInfoRaw +where + C: Config, + C::AccountId: IntoVisitor, + DecodeError: From<<::Visitor as Visitor>::Error>, +{ /// Create a new instance of `ContractInfoRaw` based on the provided contract and /// contract info value. Determines whether it's a main or secondary account deposit. pub fn new( - contract_account: AccountId32, + contract_account: C::AccountId, contract_info_value: DecodedValueThunk, ) -> Result { let contract_info = contract_info_value.as_type::()?; @@ -133,7 +158,7 @@ impl ContractInfoRaw { } } - pub fn get_deposit_account(&self) -> &AccountId32 { + pub fn get_deposit_account(&self) -> &C::AccountId { &self.deposit_account } @@ -155,8 +180,8 @@ impl ContractInfoRaw { } /// Decode the deposit account from the contract info - fn get_deposit_account_id(contract_info: &DecodedValueThunk) -> Result { - let account = contract_info.as_type::()?; + fn get_deposit_account_id(contract_info: &DecodedValueThunk) -> Result { + let account = contract_info.as_type::>()?; Ok(account.deposit_account) } } @@ -323,8 +348,8 @@ struct ContractInfoOf { /// A struct used in storage reads to access the deposit account from contract info. #[derive(Debug, DecodeAsType)] #[decode_as_type(crate_path = "subxt::ext::scale_decode")] -struct DepositAccount { - deposit_account: AccountId32, +struct DepositAccount { + deposit_account: AccountId, } #[cfg(test)] @@ -408,8 +433,9 @@ mod tests { .expect("the contract info must be decoded"); let contract = AccountId32([0u8; 32]); - let contract_info_raw = ContractInfoRaw::new(contract, contract_info_thunk) - .expect("the conatract info raw must be created"); + let contract_info_raw = + ContractInfoRaw::::new(contract, contract_info_thunk) + .expect("the conatract info raw must be created"); let account_data = AccountData { free: 1, reserved: 10, @@ -473,8 +499,9 @@ mod tests { .expect("the contract info must be decoded"); let contract = AccountId32([0u8; 32]); - let contract_info_raw = ContractInfoRaw::new(contract, contract_info_thunk) - .expect("the conatract info raw must be created"); + let contract_info_raw = + ContractInfoRaw::::new(contract, contract_info_thunk) + .expect("the conatract info raw must be created"); let account_data = AccountData { free: 1, reserved: 10, diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 24caf3c6e..8ea605ae7 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -19,10 +19,13 @@ use ink_metadata::{ layout::{ Layout, LayoutKey, + RootLayout, }, InkProject, }; +use scale_info::form::PortableForm; use sp_core::storage::ChildInfo; +use std::fmt::Display; use subxt::{ backend::{ legacy::{ @@ -33,8 +36,13 @@ use subxt::{ rpc_params, RpcClient, }, + BlockRef, + }, + error::DecodeError, + ext::scale_decode::{ + IntoVisitor, + Visitor, }, - utils::AccountId32, Config, OnlineClient, }; @@ -48,51 +56,82 @@ use super::{ TrieId, }; -pub struct ContractStorageLayout { - metadata: InkProject, - root_key: ContractStorageKey, +#[derive(serde::Serialize)] +pub struct ContractStorage { + root: ContractStorageCell, } -impl ContractStorageLayout { - pub fn new(metadata: InkProject) -> Result { - if let Layout::Root(root) = metadata.layout() { - let root_key = ContractStorageKey::from(root.root_key()); - Ok(Self { metadata, root_key }) - } else { - Err(anyhow::anyhow!("No root layout found in metadata")) - } - } - pub fn root_key(&self) -> &ContractStorageKey { - &self.root_key - } +#[derive(serde::Serialize)] + +pub struct ContractStorageCell { + key: String, + value: String, } -impl TryFrom for ContractStorageLayout { - type Error = anyhow::Error; +pub struct ContractStorageLayout { + metadata: InkProject, + rpc: ContractStorageRpc, +} + +impl ContractStorageLayout +where + C::AccountId: AsRef<[u8]> + Display + IntoVisitor, + DecodeError: From<<::Visitor as Visitor>::Error>, + BlockRef: From, +{ + pub fn new(metadata: InkProject, rpc: ContractStorageRpc) -> Self { + Self { metadata, rpc } + } - fn try_from( - metadata: contract_metadata::ContractMetadata, - ) -> Result { - let ink_project = - serde_json::from_value(serde_json::Value::Object(metadata.abi))?; - Self::new(ink_project) + pub async fn load_contract_storage( + &self, + contract_account: &C::AccountId, + ) -> Result { + let root_layout = if let Layout::Root(root_layout) = self.metadata.layout() { + Ok(root_layout) + } else { + Err(anyhow::anyhow!("No root layout found in metadata")) + }?; + + let root_key = ContractStorageKey::from(root_layout.root_key()); + let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; + let trie_id = contract_info.trie_id(); + + let root_storage = self + .rpc + .fetch_contract_storage(trie_id, &root_key, None) + .await? + .ok_or(anyhow::anyhow!( + "No contract storage was found for account id {}", + contract_account + ))?; + let root_cell = ContractStorageCell { + key: root_key.hashed_to_hex(), + value: hex::encode(root_storage), + }; + let contract_storage = ContractStorage { root: root_cell }; + Ok(contract_storage) } } /// Methods for querying contracts over RPC. -pub struct ContractStorageRpc { +pub struct ContractStorageRpc { rpc_client: RpcClient, - rpc_methods: LegacyRpcMethods, + rpc_methods: LegacyRpcMethods, client: Client, } -impl ContractStorageRpc { +impl ContractStorageRpc +where + C::AccountId: AsRef<[u8]> + Display + IntoVisitor, + DecodeError: From<<::Visitor as Visitor>::Error>, + BlockRef: From, +{ /// Create a new instance of the ContractsRpc. pub async fn new(url: &url::Url) -> Result { let rpc_client = RpcClient::from_url(url_to_string(&url)).await?; - let client = - OnlineClient::::from_rpc_client(rpc_client.clone()).await?; - let rpc_methods = LegacyRpcMethods::::new(rpc_client.clone()); + let client = OnlineClient::from_rpc_client(rpc_client.clone()).await?; + let rpc_methods = LegacyRpcMethods::new(rpc_client.clone()); Ok(Self { rpc_client, @@ -104,7 +143,7 @@ impl ContractStorageRpc { /// Fetch the contract info to access the trie id for querying storage. pub async fn fetch_contract_info( &self, - contract: &AccountId32, + contract: &C::AccountId, ) -> Result { fetch_contract_info(contract, &self.rpc_methods, &self.client).await } @@ -116,7 +155,7 @@ impl ContractStorageRpc { &self, trie_id: &TrieId, key: &ContractStorageKey, - block_hash: Option<::Hash>, + block_hash: Option, ) -> Result>> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); @@ -136,7 +175,7 @@ impl ContractStorageRpc { prefix: Option<&[u8]>, count: u32, start_key: Option<&[u8]>, - block_hash: Option<::Hash>, + block_hash: Option, ) -> Result>> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 9d5328f44..55b75b865 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -211,9 +211,9 @@ pub fn parse_code_hash(input: &str) -> Result<::Hash> { } /// Fetch the hash of the *best* block (included but not guaranteed to be finalized). -async fn get_best_block( - rpc: &LegacyRpcMethods, -) -> core::result::Result<::Hash, subxt::Error> { +async fn get_best_block( + rpc: &LegacyRpcMethods, +) -> core::result::Result { rpc.chain_get_block_hash(None) .await? .ok_or(subxt::Error::Other("Best block not found".into())) From 0d254a014336b164c9af35f1410aa00f066d78d6 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Tue, 21 Nov 2023 18:02:49 +0000 Subject: [PATCH 15/34] Recurse --- Cargo.lock | 12 +++ crates/extrinsics/Cargo.toml | 1 + crates/extrinsics/src/contract_storage.rs | 118 ++++++++++++++++------ 3 files changed, 98 insertions(+), 33 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a3d06d273..01a700c0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,6 +338,17 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "async-recursion" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.38", +] + [[package]] name = "async-task" version = "4.4.0" @@ -1044,6 +1055,7 @@ version = "4.0.0-alpha" dependencies = [ "anyhow", "assert_cmd", + "async-recursion", "blake2", "clap", "colored", diff --git a/crates/extrinsics/Cargo.toml b/crates/extrinsics/Cargo.toml index 9c9df17cb..471decd05 100644 --- a/crates/extrinsics/Cargo.toml +++ b/crates/extrinsics/Cargo.toml @@ -20,6 +20,7 @@ contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } contract-transcode = { version = "4.0.0-alpha", path = "../transcode" } anyhow = "1.0.75" +async-recursion = "1.0.5" blake2 = { version = "0.10.6", default-features = false } clap = { version = "4.4.8", features = ["derive", "env"] } futures = { version = "0.3.29", default-features = false, features = ["std"] } diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 8ea605ae7..586d66833 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -15,15 +15,17 @@ // along with cargo-contract. If not, see . use anyhow::Result; +use async_recursion::async_recursion; +use contract_metadata::byte_str; use ink_metadata::{ layout::{ Layout, LayoutKey, - RootLayout, }, InkProject, }; use scale_info::form::PortableForm; +use serde::Serialize; use sp_core::storage::ChildInfo; use std::fmt::Display; use subxt::{ @@ -56,18 +58,6 @@ use super::{ TrieId, }; -#[derive(serde::Serialize)] -pub struct ContractStorage { - root: ContractStorageCell, -} - -#[derive(serde::Serialize)] - -pub struct ContractStorageCell { - key: String, - value: String, -} - pub struct ContractStorageLayout { metadata: InkProject, rpc: ContractStorageRpc, @@ -87,31 +77,86 @@ where &self, contract_account: &C::AccountId, ) -> Result { - let root_layout = if let Layout::Root(root_layout) = self.metadata.layout() { - Ok(root_layout) - } else { - Err(anyhow::anyhow!("No root layout found in metadata")) - }?; - - let root_key = ContractStorageKey::from(root_layout.root_key()); let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; let trie_id = contract_info.trie_id(); - let root_storage = self - .rpc - .fetch_contract_storage(trie_id, &root_key, None) - .await? - .ok_or(anyhow::anyhow!( - "No contract storage was found for account id {}", - contract_account - ))?; - let root_cell = ContractStorageCell { - key: root_key.hashed_to_hex(), - value: hex::encode(root_storage), - }; - let contract_storage = ContractStorage { root: root_cell }; + let cells = self.load_storage_cells(trie_id, self.metadata.layout()).await?; + + let contract_storage = ContractStorage { cells }; Ok(contract_storage) } + + #[async_recursion] + async fn load_storage_cells(&self, trie_id: &TrieId, layout: &Layout) -> Result> { + match layout { + Layout::Leaf(leaf) => { + let key = ContractStorageKey::from(leaf.key()); + let value = self + .rpc + .fetch_contract_storage(trie_id, &key, None) + .await?; + Ok(vec![ContractStorageCell::new(key, value)]) + }, + Layout::Root(root) => { + let root_key = ContractStorageKey::from(root.root_key()); + let root_storage = self + .rpc + .fetch_contract_storage(trie_id, &root_key, None) + .await?; + let cell = ContractStorageCell::new(root_key, root_storage); + let mut cells = self.load_storage_cells(trie_id, root.layout()).await?; + vec![cell].append(&mut cells); + Ok(cells) + } + Layout::Hash(_) => { unimplemented!("Hash layout not currently constructed for ink! contracts") }, + Layout::Array(_array) => { + todo!("struct") + // let key = ContractStorageKey::from(array.key()); + // let value = self + // .rpc + // .fetch_contract_storage(trie_id, &key, None) + // .await?; + }, + Layout::Struct(_) => todo!("struct"), + Layout::Enum(_) => todo!("enum"), + } + } +} + +#[derive(Serialize)] +pub struct ContractStorage { + cells: Vec, +} + +#[derive(Serialize)] + +pub struct ContractStorageCell { + key: ContractStorageKey, + value: Option, +} + +impl ContractStorageCell { + pub fn new(key: ContractStorageKey, value: Option>) -> Self { + Self { key, value: value.map(Into::into) } + } +} + +#[derive(Serialize)] +pub struct ContractStorageValue { + #[serde(serialize_with = "byte_str::serialize_as_byte_str")] + bytes: Vec, +} + +impl From> for ContractStorageValue { + fn from(bytes: Vec) -> Self { + Self { bytes } + } +} + +impl AsRef<[u8]> for ContractStorageValue { + fn as_ref(&self) -> &[u8] { + &self.bytes + } } /// Methods for querying contracts over RPC. @@ -197,10 +242,17 @@ where } /// Represents a 32 bit storage key within a contract's storage. +#[derive(Serialize)] pub struct ContractStorageKey { raw: u32, } +impl Display for ContractStorageKey { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!(f, "{}", self.raw) + } +} + impl From<&LayoutKey> for ContractStorageKey { fn from(key: &LayoutKey) -> Self { Self { raw: *key.key() } From 6e78b364845e17832b87d81baff161b7e017bfa2 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Tue, 21 Nov 2023 21:54:31 +0000 Subject: [PATCH 16/34] WIP fetching all keys for contract --- crates/extrinsics/src/contract_storage.rs | 90 +++++++++++++++++------ 1 file changed, 68 insertions(+), 22 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 586d66833..67d814dab 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -80,44 +80,69 @@ where let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; let trie_id = contract_info.trie_id(); - let cells = self.load_storage_cells(trie_id, self.metadata.layout()).await?; + let mut cells = Vec::new(); + self + .load_storage_cells(trie_id, self.metadata.layout(), &mut cells) + .await?; let contract_storage = ContractStorage { cells }; Ok(contract_storage) } #[async_recursion] - async fn load_storage_cells(&self, trie_id: &TrieId, layout: &Layout) -> Result> { + async fn load_storage_cells( + &self, + trie_id: &TrieId, + layout: &Layout, + cells_acc: &mut Vec, + ) -> Result<()> { match layout { - Layout::Leaf(leaf) => { - let key = ContractStorageKey::from(leaf.key()); - let value = self - .rpc - .fetch_contract_storage(trie_id, &key, None) - .await?; - Ok(vec![ContractStorageCell::new(key, value)]) - }, + Layout::Leaf(_leaf) => Ok(()), Layout::Root(root) => { let root_key = ContractStorageKey::from(root.root_key()); - let root_storage = self + let prefix = root_key.bytes(); + println!("root key: {}, prefix {}", hex::encode(root_key.bytes()), hex::encode(&prefix)); + let storage_keys = self .rpc - .fetch_contract_storage(trie_id, &root_key, None) + // .fetch_storage_keys_paged(trie_id, Some(prefix.as_slice()), 1000, None, None) + .fetch_storage_keys_paged(trie_id, None, 1000, None, None) .await?; - let cell = ContractStorageCell::new(root_key, root_storage); - let mut cells = self.load_storage_cells(trie_id, root.layout()).await?; - vec![cell].append(&mut cells); - Ok(cells) + let storage_values = self + .rpc + .fetch_storage_entries(trie_id, &storage_keys, None) + .await?; + assert_eq!( + storage_keys.len(), + storage_values.len(), + "storage keys and values must be the same length" + ); + let mut cells = storage_keys + .into_iter() + .zip(storage_values.into_iter()) + .map(|(key, value)| ContractStorageCell::new(key, value)) + .collect(); + cells_acc.append(&mut cells); + + self.load_storage_cells(trie_id, root.layout(), cells_acc).await?; + Ok(()) + } + Layout::Hash(_) => { + unimplemented!("Hash layout not currently constructed for ink! contracts") } - Layout::Hash(_) => { unimplemented!("Hash layout not currently constructed for ink! contracts") }, Layout::Array(_array) => { - todo!("struct") + todo!("array") // let key = ContractStorageKey::from(array.key()); // let value = self // .rpc // .fetch_contract_storage(trie_id, &key, None) // .await?; + } + Layout::Struct(struct_layout) => { + for field in struct_layout.fields() { + self.load_storage_cells(trie_id, field.layout(), cells_acc).await?; + } + Ok(()) }, - Layout::Struct(_) => todo!("struct"), Layout::Enum(_) => todo!("enum"), } } @@ -131,13 +156,17 @@ pub struct ContractStorage { #[derive(Serialize)] pub struct ContractStorageCell { - key: ContractStorageKey, + #[serde(serialize_with = "byte_str::serialize_as_byte_str")] + key: Vec, value: Option, } impl ContractStorageCell { - pub fn new(key: ContractStorageKey, value: Option>) -> Self { - Self { key, value: value.map(Into::into) } + pub fn new(key: Vec, value: Option>) -> Self { + Self { + key, + value: value.map(Into::into), + } } } @@ -239,6 +268,23 @@ where .await?; Ok(data.into_iter().map(|b| b.0).collect()) } + + pub async fn fetch_storage_entries( + &self, + trie_id: &TrieId, + keys: &[Vec], + block_hash: Option, + ) -> Result>>> { + let child_storage_key = + ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); + let keys_hex: Vec<_> = keys.iter().map(|key| format!("0x{}", hex::encode(key))).collect(); + let params = rpc_params![child_storage_key, keys_hex, block_hash]; + let data: Vec> = self + .rpc_client + .request("childstate_getStorageEntries", params) + .await?; + Ok(data.into_iter().map(|o| o.map(|b| b.0)).collect()) + } } /// Represents a 32 bit storage key within a contract's storage. From 245d0083fcbec77a4ae8066e730167da87b40975 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 10:18:28 +0000 Subject: [PATCH 17/34] Use prefix --- crates/extrinsics/src/contract_storage.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 67d814dab..f5b3eb784 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -100,12 +100,10 @@ where Layout::Leaf(_leaf) => Ok(()), Layout::Root(root) => { let root_key = ContractStorageKey::from(root.root_key()); - let prefix = root_key.bytes(); - println!("root key: {}, prefix {}", hex::encode(root_key.bytes()), hex::encode(&prefix)); + println!("root_key: {}", root_key.hashed_to_hex()); let storage_keys = self .rpc - // .fetch_storage_keys_paged(trie_id, Some(prefix.as_slice()), 1000, None, None) - .fetch_storage_keys_paged(trie_id, None, 1000, None, None) + .fetch_storage_keys_paged(trie_id, Some(root_key), 1000, None, None) .await?; let storage_values = self .rpc @@ -246,14 +244,14 @@ where pub async fn fetch_storage_keys_paged( &self, trie_id: &TrieId, - prefix: Option<&[u8]>, + prefix: Option, count: u32, start_key: Option<&[u8]>, block_hash: Option, ) -> Result>> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); - let prefix_hex = prefix.map(|p| format!("0x{}", hex::encode(p))); + let prefix_hex = prefix.map(|p| p.hashed_to_hex()); let start_key_hex = start_key.map(|p| format!("0x{}", hex::encode(p))); let params = rpc_params![ child_storage_key, From 636379f898a240c6517a4f0d749a70c26d6222a1 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 12:04:27 +0000 Subject: [PATCH 18/34] Fetch all storage entries raw --- crates/extrinsics/src/contract_storage.rs | 158 +++++++++++----------- 1 file changed, 79 insertions(+), 79 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index f5b3eb784..9df31e10e 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -16,7 +16,6 @@ use anyhow::Result; use async_recursion::async_recursion; -use contract_metadata::byte_str; use ink_metadata::{ layout::{ Layout, @@ -80,70 +79,67 @@ where let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; let trie_id = contract_info.trie_id(); - let mut cells = Vec::new(); - self - .load_storage_cells(trie_id, self.metadata.layout(), &mut cells) + let storage_keys = self + .rpc + .fetch_storage_keys_paged(trie_id, None, 1000, None, None) // todo loop pages .await?; + let storage_values = self + .rpc + .fetch_storage_entries(trie_id, &storage_keys, None) + .await?; + assert_eq!( + storage_keys.len(), + storage_values.len(), + "storage keys and values must be the same length" + ); + let mut cells = storage_keys + .into_iter() + .zip(storage_values.into_iter()) + .map(|(key, value)| ContractStorageCell::new(key, value)) + .collect(); let contract_storage = ContractStorage { cells }; Ok(contract_storage) } - #[async_recursion] - async fn load_storage_cells( - &self, - trie_id: &TrieId, - layout: &Layout, - cells_acc: &mut Vec, - ) -> Result<()> { - match layout { - Layout::Leaf(_leaf) => Ok(()), - Layout::Root(root) => { - let root_key = ContractStorageKey::from(root.root_key()); - println!("root_key: {}", root_key.hashed_to_hex()); - let storage_keys = self - .rpc - .fetch_storage_keys_paged(trie_id, Some(root_key), 1000, None, None) - .await?; - let storage_values = self - .rpc - .fetch_storage_entries(trie_id, &storage_keys, None) - .await?; - assert_eq!( - storage_keys.len(), - storage_values.len(), - "storage keys and values must be the same length" - ); - let mut cells = storage_keys - .into_iter() - .zip(storage_values.into_iter()) - .map(|(key, value)| ContractStorageCell::new(key, value)) - .collect(); - cells_acc.append(&mut cells); - - self.load_storage_cells(trie_id, root.layout(), cells_acc).await?; - Ok(()) - } - Layout::Hash(_) => { - unimplemented!("Hash layout not currently constructed for ink! contracts") - } - Layout::Array(_array) => { - todo!("array") - // let key = ContractStorageKey::from(array.key()); - // let value = self - // .rpc - // .fetch_contract_storage(trie_id, &key, None) - // .await?; - } - Layout::Struct(struct_layout) => { - for field in struct_layout.fields() { - self.load_storage_cells(trie_id, field.layout(), cells_acc).await?; - } - Ok(()) - }, - Layout::Enum(_) => todo!("enum"), - } - } + // #[async_recursion] + // async fn load_storage_cells( + // &self, + // trie_id: &TrieId, + // layout: &Layout, + // cells_acc: &mut Vec, + // ) -> Result<()> { + // match layout { + // Layout::Leaf(_leaf) => Ok(()), + // Layout::Root(root) => { + // let root_key = ContractStorageKey::from(root.root_key()); + // + // + // cells_acc.append(&mut cells); + // + // self.load_storage_cells(trie_id, root.layout(), cells_acc).await?; + // Ok(()) + // } + // Layout::Hash(_) => { + // unimplemented!("Hash layout not currently constructed for ink! + // contracts") } + // Layout::Array(_array) => { + // todo!("array") + // // let key = ContractStorageKey::from(array.key()); + // // let value = self + // // .rpc + // // .fetch_contract_storage(trie_id, &key, None) + // // .await?; + // } + // Layout::Struct(struct_layout) => { + // for field in struct_layout.fields() { + // self.load_storage_cells(trie_id, field.layout(), cells_acc).await?; + // } + // Ok(()) + // }, + // Layout::Enum(_) => todo!("enum"), + // } + // } } #[derive(Serialize)] @@ -154,29 +150,26 @@ pub struct ContractStorage { #[derive(Serialize)] pub struct ContractStorageCell { - #[serde(serialize_with = "byte_str::serialize_as_byte_str")] - key: Vec, - value: Option, + key: Bytes, + value: Option, } impl ContractStorageCell { - pub fn new(key: Vec, value: Option>) -> Self { - Self { - key, - value: value.map(Into::into), - } + pub fn new(key: Bytes, value: Option) -> Self { + Self { key, value } } } #[derive(Serialize)] pub struct ContractStorageValue { - #[serde(serialize_with = "byte_str::serialize_as_byte_str")] - bytes: Vec, + bytes: Bytes, } impl From> for ContractStorageValue { fn from(bytes: Vec) -> Self { - Self { bytes } + Self { + bytes: bytes.into(), + } } } @@ -228,7 +221,7 @@ where trie_id: &TrieId, key: &ContractStorageKey, block_hash: Option, - ) -> Result>> { + ) -> Result> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); let key_hex = key.hashed_to_hex(); @@ -238,7 +231,7 @@ where .rpc_client .request("childstate_getStorage", params) .await?; - Ok(data.map(|b| b.0)) + Ok(data) } pub async fn fetch_storage_keys_paged( @@ -248,7 +241,7 @@ where count: u32, start_key: Option<&[u8]>, block_hash: Option, - ) -> Result>> { + ) -> Result> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); let prefix_hex = prefix.map(|p| p.hashed_to_hex()); @@ -264,24 +257,23 @@ where .rpc_client .request("childstate_getKeysPaged", params) .await?; - Ok(data.into_iter().map(|b| b.0).collect()) + Ok(data) } pub async fn fetch_storage_entries( &self, trie_id: &TrieId, - keys: &[Vec], + keys: &[Bytes], block_hash: Option, - ) -> Result>>> { + ) -> Result>> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); - let keys_hex: Vec<_> = keys.iter().map(|key| format!("0x{}", hex::encode(key))).collect(); - let params = rpc_params![child_storage_key, keys_hex, block_hash]; + let params = rpc_params![child_storage_key, keys, block_hash]; let data: Vec> = self .rpc_client .request("childstate_getStorageEntries", params) .await?; - Ok(data.into_iter().map(|o| o.map(|b| b.0)).collect()) + Ok(data) } } @@ -335,3 +327,11 @@ impl ContractStorageKey { hex::encode(concat) } } + +#[cfg(test)] +mod tests { + #[test] + fn storage_key_is_part_of_root() { + todo!("test deet") + } +} From 961e52bf7be28a695868ad23025b1fca527b96c1 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 12:30:13 +0000 Subject: [PATCH 19/34] BTreeMap --- crates/extrinsics/src/contract_storage.rs | 50 +++++------------------ 1 file changed, 10 insertions(+), 40 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 9df31e10e..3d1205e06 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -26,7 +26,10 @@ use ink_metadata::{ use scale_info::form::PortableForm; use serde::Serialize; use sp_core::storage::ChildInfo; -use std::fmt::Display; +use std::{ + collections::BTreeMap, + fmt::Display, +}; use subxt::{ backend::{ legacy::{ @@ -75,7 +78,7 @@ where pub async fn load_contract_storage( &self, contract_account: &C::AccountId, - ) -> Result { + ) -> Result { let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; let trie_id = contract_info.trie_id(); @@ -92,13 +95,13 @@ where storage_values.len(), "storage keys and values must be the same length" ); - let mut cells = storage_keys + let storage = storage_keys .into_iter() .zip(storage_values.into_iter()) - .map(|(key, value)| ContractStorageCell::new(key, value)) + .filter_map(|(key, value)| value.map(|v| (key, v))) .collect(); - let contract_storage = ContractStorage { cells }; + let contract_storage = ContractStorageData(storage); Ok(contract_storage) } @@ -142,42 +145,9 @@ where // } } +/// Represents the raw key/value storage for the contract. #[derive(Serialize)] -pub struct ContractStorage { - cells: Vec, -} - -#[derive(Serialize)] - -pub struct ContractStorageCell { - key: Bytes, - value: Option, -} - -impl ContractStorageCell { - pub fn new(key: Bytes, value: Option) -> Self { - Self { key, value } - } -} - -#[derive(Serialize)] -pub struct ContractStorageValue { - bytes: Bytes, -} - -impl From> for ContractStorageValue { - fn from(bytes: Vec) -> Self { - Self { - bytes: bytes.into(), - } - } -} - -impl AsRef<[u8]> for ContractStorageValue { - fn as_ref(&self) -> &[u8] { - &self.bytes - } -} +pub struct ContractStorageData(BTreeMap); /// Methods for querying contracts over RPC. pub struct ContractStorageRpc { From 70d8458d7383749fdec4389d89c2fb84cde56bd9 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 16:02:12 +0000 Subject: [PATCH 20/34] Display enriched contract storage --- Cargo.lock | 12 -- crates/cargo-contract/src/cmd/storage.rs | 10 +- crates/extrinsics/Cargo.toml | 1 - crates/extrinsics/src/contract_storage.rs | 157 ++++++++++++++-------- crates/extrinsics/src/lib.rs | 2 +- 5 files changed, 108 insertions(+), 74 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 01a700c0f..a3d06d273 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -338,17 +338,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "async-recursion" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fd55a5ba1179988837d24ab4c7cc8ed6efdeff578ede0416b4225a5fca35bd0" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.38", -] - [[package]] name = "async-task" version = "4.4.0" @@ -1055,7 +1044,6 @@ version = "4.0.0-alpha" dependencies = [ "anyhow", "assert_cmd", - "async-recursion", "blake2", "clap", "colored", diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 189ad1961..ed6d3c8f5 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -18,7 +18,7 @@ use super::DefaultConfig; use anyhow::Result; use contract_extrinsics::{ ContractArtifacts, - ContractStorageLayout, + ContractStorage, ContractStorageRpc, ErrorVariant, }; @@ -66,8 +66,7 @@ impl StorageCommand { )?; let ink_metadata = contract_artifacts.ink_project_metadata()?; - let storage_layout = - ContractStorageLayout::::new(ink_metadata, rpc); + let storage_layout = ContractStorage::::new(ink_metadata, rpc); // todo: fetch all storage keys and map to metadata? // let storage_keys = rpc @@ -78,8 +77,9 @@ impl StorageCommand { // println!("storage key: {}", hex::encode(storage_key)); // } - let contract_storage = - storage_layout.load_contract_storage(&self.contract).await?; + let contract_storage = storage_layout + .load_contract_storage_data(&self.contract) + .await?; println!( "{json}", diff --git a/crates/extrinsics/Cargo.toml b/crates/extrinsics/Cargo.toml index 471decd05..9c9df17cb 100644 --- a/crates/extrinsics/Cargo.toml +++ b/crates/extrinsics/Cargo.toml @@ -20,7 +20,6 @@ contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } contract-transcode = { version = "4.0.0-alpha", path = "../transcode" } anyhow = "1.0.75" -async-recursion = "1.0.5" blake2 = { version = "0.10.6", default-features = false } clap = { version = "4.4.8", features = ["derive", "env"] } futures = { version = "0.3.29", default-features = false, features = ["std"] } diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 3d1205e06..a71fd55eb 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -15,7 +15,6 @@ // along with cargo-contract. If not, see . use anyhow::Result; -use async_recursion::async_recursion; use ink_metadata::{ layout::{ Layout, @@ -24,7 +23,9 @@ use ink_metadata::{ InkProject, }; use scale_info::form::PortableForm; -use serde::Serialize; +use serde::{ + Serialize, +}; use sp_core::storage::ChildInfo; use std::{ collections::BTreeMap, @@ -60,12 +61,12 @@ use super::{ TrieId, }; -pub struct ContractStorageLayout { +pub struct ContractStorage { metadata: InkProject, rpc: ContractStorageRpc, } -impl ContractStorageLayout +impl ContractStorage where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, @@ -75,7 +76,8 @@ where Self { metadata, rpc } } - pub async fn load_contract_storage( + /// Load the raw key/value storage for a given contract. + pub async fn load_contract_storage_data( &self, contract_account: &C::AccountId, ) -> Result { @@ -105,50 +107,104 @@ where Ok(contract_storage) } - // #[async_recursion] - // async fn load_storage_cells( - // &self, - // trie_id: &TrieId, - // layout: &Layout, - // cells_acc: &mut Vec, - // ) -> Result<()> { - // match layout { - // Layout::Leaf(_leaf) => Ok(()), - // Layout::Root(root) => { - // let root_key = ContractStorageKey::from(root.root_key()); - // - // - // cells_acc.append(&mut cells); - // - // self.load_storage_cells(trie_id, root.layout(), cells_acc).await?; - // Ok(()) - // } - // Layout::Hash(_) => { - // unimplemented!("Hash layout not currently constructed for ink! - // contracts") } - // Layout::Array(_array) => { - // todo!("array") - // // let key = ContractStorageKey::from(array.key()); - // // let value = self - // // .rpc - // // .fetch_contract_storage(trie_id, &key, None) - // // .await?; - // } - // Layout::Struct(struct_layout) => { - // for field in struct_layout.fields() { - // self.load_storage_cells(trie_id, field.layout(), cells_acc).await?; - // } - // Ok(()) - // }, - // Layout::Enum(_) => todo!("enum"), - // } - // } + pub async fn load_contract_storage_with_layout( + &self, + contract_account: &C::AccountId, + ) -> Result { + let data = self.load_contract_storage_data(contract_account).await?; + let layout = ContractStorageLayout::new(data, self.metadata.layout()); + Ok(layout) + } } /// Represents the raw key/value storage for the contract. #[derive(Serialize)] pub struct ContractStorageData(BTreeMap); +#[derive(Serialize)] +pub struct ContractStorageLayout { + cells: Vec, +} + +impl ContractStorageLayout { + pub fn new(data: ContractStorageData, layout: &Layout) -> Self { + let mut cells = Vec::new(); + Self::layout_cells("root".to_string(), &data, layout, &mut cells); + Self { cells } + } + + fn layout_cells( + label: String, + data: &ContractStorageData, + layout: &Layout, + cells_acc: &mut Vec, + ) { + match layout { + Layout::Root(root) => { + let mut cells = data + .0 + .iter() + .filter_map(|(k, v)| { + assert!(k.0.len() >= 20, "key must be at least 20 bytes"); + let root_key = { + let mut key = [0u8; 4]; + key.copy_from_slice(&k.0[16..20]); + u32::from_be_bytes(key) + }; + let mapping_key = if k.0.len() > 20 { + Some(Bytes::from(k.0[20..].to_vec())) + } else { + None + }; + + if root_key != *root.root_key().key() { + None + } else { + Some(ContractStorageCell { + key: k.clone(), + value: v.clone(), + root_key, + mapping_key, + label: label.clone(), + }) + } + }) + .collect(); + + cells_acc.append(&mut cells); + } + Layout::Struct(struct_layout) => { + for field in struct_layout.fields() { + let label = field.name().to_string(); + Self::layout_cells(label, data, field.layout(), cells_acc); + } + } + Layout::Enum(enum_layout) => { + for (variant, struct_layout) in enum_layout.variants() { + for field in struct_layout.fields() { + let label = format!("{}::{}", enum_layout.name(), variant.value()); + Self::layout_cells(label, data, field.layout(), cells_acc); + } + } + } + Layout::Array(_) => { + todo!("Figure out what to do with an array layout") + } + Layout::Hash(_) => unimplemented!("Layout::Hash is not currently be constructed"), + Layout::Leaf(_) => {} + } + } +} + +#[derive(Serialize)] +pub struct ContractStorageCell { + key: Bytes, + value: Bytes, + root_key: u32, + mapping_key: Option, + label: String, +} + /// Methods for querying contracts over RPC. pub struct ContractStorageRpc { rpc_client: RpcClient, @@ -195,7 +251,6 @@ where let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); let key_hex = key.hashed_to_hex(); - tracing::debug!("fetch_contract_storage: child_storage_key: {child_storage_key:?} for key: {key_hex:?}"); let params = rpc_params![child_storage_key, key_hex, block_hash]; let data: Option = self .rpc_client @@ -207,15 +262,15 @@ where pub async fn fetch_storage_keys_paged( &self, trie_id: &TrieId, - prefix: Option, + prefix: Option<&[u8]>, count: u32, start_key: Option<&[u8]>, block_hash: Option, ) -> Result> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); - let prefix_hex = prefix.map(|p| p.hashed_to_hex()); - let start_key_hex = start_key.map(|p| format!("0x{}", hex::encode(p))); + let prefix_hex = prefix.map(|p| format!("0x{}", hex::encode(p))); + let start_key_hex = start_key.map(|k| format!("0x{}", hex::encode(k))); let params = rpc_params![ child_storage_key, prefix_hex, @@ -297,11 +352,3 @@ impl ContractStorageKey { hex::encode(concat) } } - -#[cfg(test)] -mod tests { - #[test] - fn storage_key_is_part_of_root() { - todo!("test deet") - } -} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 55b75b865..0d1e25ea8 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -73,8 +73,8 @@ pub use contract_info::{ }; use contract_metadata::ContractMetadata; pub use contract_storage::{ + ContractStorage, ContractStorageKey, - ContractStorageLayout, ContractStorageRpc, }; pub use contract_transcode::ContractMessageTranscoder; From df7b093ff8f6fc489bd1bf6c842a9c7b4a0bcd07 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 16:05:17 +0000 Subject: [PATCH 21/34] Remove unused stuff --- crates/extrinsics/src/contract_storage.rs | 72 +++-------------------- crates/extrinsics/src/lib.rs | 1 - 2 files changed, 9 insertions(+), 64 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index a71fd55eb..679a67f68 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -16,16 +16,11 @@ use anyhow::Result; use ink_metadata::{ - layout::{ - Layout, - LayoutKey, - }, + layout::Layout, InkProject, }; use scale_info::form::PortableForm; -use serde::{ - Serialize, -}; +use serde::Serialize; use sp_core::storage::ChildInfo; use std::{ collections::BTreeMap, @@ -182,7 +177,8 @@ impl ContractStorageLayout { Layout::Enum(enum_layout) => { for (variant, struct_layout) in enum_layout.variants() { for field in struct_layout.fields() { - let label = format!("{}::{}", enum_layout.name(), variant.value()); + let label = + format!("{}::{}", enum_layout.name(), variant.value()); Self::layout_cells(label, data, field.layout(), cells_acc); } } @@ -190,7 +186,9 @@ impl ContractStorageLayout { Layout::Array(_) => { todo!("Figure out what to do with an array layout") } - Layout::Hash(_) => unimplemented!("Layout::Hash is not currently be constructed"), + Layout::Hash(_) => { + unimplemented!("Layout::Hash is not currently be constructed") + } Layout::Leaf(_) => {} } } @@ -245,13 +243,12 @@ where pub async fn fetch_contract_storage( &self, trie_id: &TrieId, - key: &ContractStorageKey, + key: &Bytes, block_hash: Option, ) -> Result> { let child_storage_key = ChildInfo::new_default(trie_id.as_ref()).into_prefixed_storage_key(); - let key_hex = key.hashed_to_hex(); - let params = rpc_params![child_storage_key, key_hex, block_hash]; + let params = rpc_params![child_storage_key, key, block_hash]; let data: Option = self .rpc_client .request("childstate_getStorage", params) @@ -301,54 +298,3 @@ where Ok(data) } } - -/// Represents a 32 bit storage key within a contract's storage. -#[derive(Serialize)] -pub struct ContractStorageKey { - raw: u32, -} - -impl Display for ContractStorageKey { - fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - write!(f, "{}", self.raw) - } -} - -impl From<&LayoutKey> for ContractStorageKey { - fn from(key: &LayoutKey) -> Self { - Self { raw: *key.key() } - } -} - -impl ContractStorageKey { - /// Create a new instance of the ContractStorageKey. - pub fn new(raw: u32) -> Self { - Self { raw } - } - - pub fn bytes(&self) -> [u8; 4] { - self.raw.to_be_bytes() - } - - /// Returns the hex encoded hashed `blake2_128_concat` representation of the storage - /// key. - pub fn hashed_to_hex(&self) -> String { - use blake2::digest::{ - consts::U16, - Digest as _, - }; - - let mut blake2_128 = blake2::Blake2b::::new(); - blake2_128.update(&self.bytes()); - let result = blake2_128.finalize(); - - let concat = result - .as_slice() - .iter() - .chain(self.bytes().iter()) - .cloned() - .collect::>(); - - hex::encode(concat) - } -} diff --git a/crates/extrinsics/src/lib.rs b/crates/extrinsics/src/lib.rs index 0d1e25ea8..760bc6a71 100644 --- a/crates/extrinsics/src/lib.rs +++ b/crates/extrinsics/src/lib.rs @@ -74,7 +74,6 @@ pub use contract_info::{ use contract_metadata::ContractMetadata; pub use contract_storage::{ ContractStorage, - ContractStorageKey, ContractStorageRpc, }; pub use contract_transcode::ContractMessageTranscoder; From cc0d96105556f703b1a4cb30ac6b06199709afcd Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 16:58:44 +0000 Subject: [PATCH 22/34] WIP query root keys --- crates/cargo-contract/src/cmd/storage.rs | 11 +-- crates/extrinsics/src/contract_storage.rs | 82 ++++++++++++----------- 2 files changed, 45 insertions(+), 48 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index ed6d3c8f5..592868b5b 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -68,17 +68,8 @@ impl StorageCommand { let ink_metadata = contract_artifacts.ink_project_metadata()?; let storage_layout = ContractStorage::::new(ink_metadata, rpc); - // todo: fetch all storage keys and map to metadata? - // let storage_keys = rpc - // .fetch_storage_keys_paged(trie_id, None, 100, None, None) - // .await?; - // - // for storage_key in storage_keys { - // println!("storage key: {}", hex::encode(storage_key)); - // } - let contract_storage = storage_layout - .load_contract_storage_data(&self.contract) + .load_contract_storage_with_layout(&self.contract) .await?; println!( diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 679a67f68..252d5e6ca 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -15,6 +15,7 @@ // along with cargo-contract. If not, see . use anyhow::Result; +use contract_metadata::byte_str; use ink_metadata::{ layout::Layout, InkProject, @@ -123,55 +124,59 @@ pub struct ContractStorageLayout { impl ContractStorageLayout { pub fn new(data: ContractStorageData, layout: &Layout) -> Self { - let mut cells = Vec::new(); - Self::layout_cells("root".to_string(), &data, layout, &mut cells); + let mut root_keys = Vec::new(); + Self::collect_roots("root".to_string(), layout, &mut root_keys); + + let cells = data + .0 + .iter() + .filter_map(|(k, v)| { + assert!(k.0.len() >= 20, "key must be at least 20 bytes"); + let root_key = { + let mut key = [0u8; 4]; + key.copy_from_slice(&k.0[16..20]); + }; + let mapping_key = if k.0.len() > 20 { + Some(Bytes::from(k.0[20..].to_vec())) + } else { + None + }; + + let root = root_keys.iter().find(|(_, key)| key.0 == &root_key.to_string()).unwrap(); + + if root_key != *root.root_key().key() { + None + } else { + Some(ContractStorageCell { + key: k.clone(), + value: v.clone(), + root_key, + mapping_key, + label: label.clone(), + }) + } + }) + .collect(); + Self { cells } } - fn layout_cells( + fn collect_root_keys( label: String, - data: &ContractStorageData, layout: &Layout, - cells_acc: &mut Vec, + root_keys: &mut Vec<(String, Bytes)>, ) { match layout { Layout::Root(root) => { - let mut cells = data - .0 - .iter() - .filter_map(|(k, v)| { - assert!(k.0.len() >= 20, "key must be at least 20 bytes"); - let root_key = { - let mut key = [0u8; 4]; - key.copy_from_slice(&k.0[16..20]); - u32::from_be_bytes(key) - }; - let mapping_key = if k.0.len() > 20 { - Some(Bytes::from(k.0[20..].to_vec())) - } else { - None - }; - - if root_key != *root.root_key().key() { - None - } else { - Some(ContractStorageCell { - key: k.clone(), - value: v.clone(), - root_key, - mapping_key, - label: label.clone(), - }) - } - }) - .collect(); - cells_acc.append(&mut cells); + root_keys.append(&mut cells); + Self::collect_roots(label, root.layout(), root_keys) } Layout::Struct(struct_layout) => { for field in struct_layout.fields() { let label = field.name().to_string(); - Self::layout_cells(label, data, field.layout(), cells_acc); + println!("field: {}", label); + Self::collect_roots(label, field.layout(), root_keys); } } Layout::Enum(enum_layout) => { @@ -179,7 +184,7 @@ impl ContractStorageLayout { for field in struct_layout.fields() { let label = format!("{}::{}", enum_layout.name(), variant.value()); - Self::layout_cells(label, data, field.layout(), cells_acc); + Self::collect_roots(label, field.layout(), root_keys); } } } @@ -198,7 +203,8 @@ impl ContractStorageLayout { pub struct ContractStorageCell { key: Bytes, value: Bytes, - root_key: u32, + #[serde(serialize_with = "byte_str::serialize_as_byte_str")] + root_key: [u8; 4], mapping_key: Option, label: String, } From 95be3bf8c7ff1c2fe319d04eb5b5e00f9c6e3604 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 18:03:56 +0000 Subject: [PATCH 23/34] Query root keys --- crates/extrinsics/src/contract_storage.rs | 41 ++++++++++------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 252d5e6ca..3a2c29400 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -125,36 +125,31 @@ pub struct ContractStorageLayout { impl ContractStorageLayout { pub fn new(data: ContractStorageData, layout: &Layout) -> Self { let mut root_keys = Vec::new(); - Self::collect_roots("root".to_string(), layout, &mut root_keys); + Self::collect_root_keys("root".to_string(), layout, &mut root_keys); let cells = data .0 .iter() .filter_map(|(k, v)| { assert!(k.0.len() >= 20, "key must be at least 20 bytes"); - let root_key = { - let mut key = [0u8; 4]; - key.copy_from_slice(&k.0[16..20]); - }; + let mut root_key = [0u8; 4]; + root_key.copy_from_slice(&k.0[16..20]); + let mapping_key = if k.0.len() > 20 { Some(Bytes::from(k.0[20..].to_vec())) } else { None }; - let root = root_keys.iter().find(|(_, key)| key.0 == &root_key.to_string()).unwrap(); + let (label, _) = root_keys.iter().find(|(_, key)| key.to_be_bytes() == root_key)?; - if root_key != *root.root_key().key() { - None - } else { - Some(ContractStorageCell { - key: k.clone(), - value: v.clone(), - root_key, - mapping_key, - label: label.clone(), - }) - } + Some(ContractStorageCell { + key: k.clone(), + value: v.clone(), + root_key, + mapping_key, + label: label.clone(), + }) }) .collect(); @@ -164,19 +159,17 @@ impl ContractStorageLayout { fn collect_root_keys( label: String, layout: &Layout, - root_keys: &mut Vec<(String, Bytes)>, + root_keys: &mut Vec<(String, u32)>, ) { match layout { Layout::Root(root) => { - - root_keys.append(&mut cells); - Self::collect_roots(label, root.layout(), root_keys) + root_keys.push((label.clone(), *root.root_key().key())); + Self::collect_root_keys(label, root.layout(), root_keys) } Layout::Struct(struct_layout) => { for field in struct_layout.fields() { let label = field.name().to_string(); - println!("field: {}", label); - Self::collect_roots(label, field.layout(), root_keys); + Self::collect_root_keys(label, field.layout(), root_keys); } } Layout::Enum(enum_layout) => { @@ -184,7 +177,7 @@ impl ContractStorageLayout { for field in struct_layout.fields() { let label = format!("{}::{}", enum_layout.name(), variant.value()); - Self::collect_roots(label, field.layout(), root_keys); + Self::collect_root_keys(label, field.layout(), root_keys); } } } From fa2c00056568513c7c783c5348e982a7d22befd7 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 18:57:04 +0000 Subject: [PATCH 24/34] Warnings and don't serialize if none --- crates/extrinsics/src/contract_storage.rs | 43 +++++++++++++++-------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 3a2c29400..e117e9f90 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -15,7 +15,6 @@ // along with cargo-contract. If not, see . use anyhow::Result; -use contract_metadata::byte_str; use ink_metadata::{ layout::Layout, InkProject, @@ -131,17 +130,8 @@ impl ContractStorageLayout { .0 .iter() .filter_map(|(k, v)| { - assert!(k.0.len() >= 20, "key must be at least 20 bytes"); - let mut root_key = [0u8; 4]; - root_key.copy_from_slice(&k.0[16..20]); - - let mapping_key = if k.0.len() > 20 { - Some(Bytes::from(k.0[20..].to_vec())) - } else { - None - }; - - let (label, _) = root_keys.iter().find(|(_, key)| key.to_be_bytes() == root_key)?; + let (root_key, mapping_key) = Self::key_parts(k); + let (label, _) = root_keys.iter().find(|(_, key)| *key == root_key)?; Some(ContractStorageCell { key: k.clone(), @@ -190,16 +180,39 @@ impl ContractStorageLayout { Layout::Leaf(_) => {} } } + + /// Split the key up + /// + /// 0x6a3fa479de3b1efe271333d8974501c8e7dc23266dd9bfa5543a94aad824cfb29396d200926d28223c57df8954cf0dc16812ea47 + /// |--------------------------------|---------|-------------------------------------------------------------| + /// blake2_128 of raw key root key mapping key + fn key_parts(key: &Bytes) -> (u32, Option) { + assert!(key.0.len() >= 20, "key must be at least 20 bytes"); + let mut root_key_bytes = [0u8; 4]; + root_key_bytes.copy_from_slice(&key.0[16..20]); + + // keys are SCALE encoded (little endian), so the root key + let root_key = ::decode(&mut &root_key_bytes[..]) + .expect("root key is 4 bytes, it always decodes successfully to a u32; qed"); + + let mapping_key = if key.0.len() > 20 { + Some(Bytes::from(key.0[20..].to_vec())) + } else { + None + }; + + (root_key, mapping_key) + } } #[derive(Serialize)] pub struct ContractStorageCell { key: Bytes, value: Bytes, - #[serde(serialize_with = "byte_str::serialize_as_byte_str")] - root_key: [u8; 4], - mapping_key: Option, label: String, + root_key: u32, + #[serde(skip_serializing_if = "Option::is_none")] + mapping_key: Option, } /// Methods for querying contracts over RPC. From ce0fd3ad9e9c6e75b21c24c657467622df542077 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Wed, 22 Nov 2023 18:57:53 +0000 Subject: [PATCH 25/34] Clippy --- crates/extrinsics/src/contract_storage.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index e117e9f90..7e4dd0a25 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -76,7 +76,7 @@ where &self, contract_account: &C::AccountId, ) -> Result { - let contract_info = self.rpc.fetch_contract_info(&contract_account).await?; + let contract_info = self.rpc.fetch_contract_info(contract_account).await?; let trie_id = contract_info.trie_id(); let storage_keys = self @@ -230,7 +230,7 @@ where { /// Create a new instance of the ContractsRpc. pub async fn new(url: &url::Url) -> Result { - let rpc_client = RpcClient::from_url(url_to_string(&url)).await?; + let rpc_client = RpcClient::from_url(url_to_string(url)).await?; let client = OnlineClient::from_rpc_client(rpc_client.clone()).await?; let rpc_methods = LegacyRpcMethods::new(rpc_client.clone()); From f3d0a74197bf13d5b2c4fa22d447461708308537 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 23 Nov 2023 09:59:46 +0000 Subject: [PATCH 26/34] Use Display for TrieId --- crates/cargo-contract/src/cmd/mod.rs | 6 +----- crates/extrinsics/src/contract_info.rs | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 712f0e3cb..7795dc9f7 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -225,11 +225,7 @@ pub fn print_gas_required_success(gas: Weight) { /// Display contract information in a formatted way pub fn basic_display_format_extended_contract_info(info: &ExtendedContractInfo) { - name_value_println!( - "TrieId", - format!("{}", info.trie_id.to_hex()), - MAX_KEY_COL_WIDTH - ); + name_value_println!("TrieId", info.trie_id, MAX_KEY_COL_WIDTH); name_value_println!( "Code Hash", format!("{:?}", info.code_hash), diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 3eaff38fb..32181d75f 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -26,7 +26,10 @@ use anyhow::{ Result, }; use contract_metadata::byte_str::serialize_as_byte_str; -use std::fmt::Display; +use std::fmt::{ + Display, + Formatter, +}; use scale::Decode; use std::option::Option; @@ -229,10 +232,17 @@ impl ContractInfo { /// A contract's child trie id. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] pub struct TrieId { - #[serde(serialize_with = "serialize_as_byte_str")] + #[serde(flatten, serialize_with = "serialize_as_byte_str")] raw: Vec, } +impl TrieId { + /// Encode the trie id as hex string. + pub fn to_hex(&self) -> String { + format!("0x{}", hex::encode(&self.raw)) + } +} + impl From> for TrieId { fn from(raw: Vec) -> Self { Self { raw } @@ -245,10 +255,9 @@ impl AsRef<[u8]> for TrieId { } } -impl TrieId { - /// Encode the trie id as hex string. - pub fn to_hex(&self) -> String { - hex::encode(&self.raw) +impl Display for TrieId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.to_hex()) } } From de9fd7d821dd882357c358975966a7070991c7c4 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 23 Nov 2023 10:32:46 +0000 Subject: [PATCH 27/34] Display raw storage --- crates/cargo-contract/src/cmd/storage.rs | 56 ++++++++++++++++++----- crates/extrinsics/src/contract_storage.rs | 8 ++-- 2 files changed, 48 insertions(+), 16 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 592868b5b..10a7c187f 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -16,6 +16,7 @@ use super::DefaultConfig; use anyhow::Result; +use colored::Colorize; use contract_extrinsics::{ ContractArtifacts, ContractStorage, @@ -39,6 +40,9 @@ pub struct StorageCommand { required_unless_present = "all" )] contract: ::AccountId, + /// Fetch the "raw" storage keys and values for the contract. + #[clap(long)] + raw: bool, /// Path to a contract build artifact file: a raw `.wasm` file, a `.contract` bundle, /// or a `.json` metadata file. #[clap(value_parser, conflicts_with = "manifest_path")] @@ -59,24 +63,52 @@ pub struct StorageCommand { impl StorageCommand { pub async fn run(&self) -> Result<(), ErrorVariant> { let rpc = ContractStorageRpc::::new(&self.url).await?; + let storage_layout = ContractStorage::::new(rpc); + + if self.raw { + let storage_data = storage_layout + .load_contract_storage_data(&self.contract) + .await?; + println!( + "{json}", + json = serde_json::to_string_pretty(&storage_data)? + ); + return Ok(()) + } let contract_artifacts = ContractArtifacts::from_manifest_or_file( self.manifest_path.as_ref(), self.file.as_ref(), - )?; - - let ink_metadata = contract_artifacts.ink_project_metadata()?; - let storage_layout = ContractStorage::::new(ink_metadata, rpc); - - let contract_storage = storage_layout - .load_contract_storage_with_layout(&self.contract) - .await?; - - println!( - "{json}", - json = serde_json::to_string_pretty(&contract_storage)? ); + match contract_artifacts { + Ok(contract_artifacts) => { + let ink_metadata = contract_artifacts.ink_project_metadata()?; + let contract_storage = storage_layout + .load_contract_storage_with_layout(&ink_metadata, &self.contract) + .await?; + println!( + "{json}", + json = serde_json::to_string_pretty(&contract_storage)? + ); + } + Err(_) => { + eprintln!( + "{} {}", + "Info:".cyan().bold(), + "Displaying raw storage: no valid contract metadata artifacts found" + ); + let storage_data = storage_layout + .load_contract_storage_data(&self.contract) + .await?; + println!( + "{json}", + json = serde_json::to_string_pretty(&storage_data)? + ); + return Ok(()) + } + } + Ok(()) } } diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 7e4dd0a25..312dff055 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -57,7 +57,6 @@ use super::{ }; pub struct ContractStorage { - metadata: InkProject, rpc: ContractStorageRpc, } @@ -67,8 +66,8 @@ where DecodeError: From<<::Visitor as Visitor>::Error>, BlockRef: From, { - pub fn new(metadata: InkProject, rpc: ContractStorageRpc) -> Self { - Self { metadata, rpc } + pub fn new(rpc: ContractStorageRpc) -> Self { + Self { rpc } } /// Load the raw key/value storage for a given contract. @@ -104,10 +103,11 @@ where pub async fn load_contract_storage_with_layout( &self, + metadata: &InkProject, contract_account: &C::AccountId, ) -> Result { let data = self.load_contract_storage_data(contract_account).await?; - let layout = ContractStorageLayout::new(data, self.metadata.layout()); + let layout = ContractStorageLayout::new(data, metadata.layout()); Ok(layout) } } From b588e381dd925906a52923e13347f7a5a770ebe1 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 23 Nov 2023 10:50:40 +0000 Subject: [PATCH 28/34] Flatten TrieId properly --- crates/extrinsics/src/contract_info.rs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 32181d75f..068a77bbf 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -231,27 +231,24 @@ impl ContractInfo { /// A contract's child trie id. #[derive(Clone, Debug, Eq, PartialEq, serde::Serialize)] -pub struct TrieId { - #[serde(flatten, serialize_with = "serialize_as_byte_str")] - raw: Vec, -} +pub struct TrieId(#[serde(serialize_with = "serialize_as_byte_str")] Vec); impl TrieId { /// Encode the trie id as hex string. pub fn to_hex(&self) -> String { - format!("0x{}", hex::encode(&self.raw)) + format!("0x{}", hex::encode(&self.0)) } } impl From> for TrieId { fn from(raw: Vec) -> Self { - Self { raw } + Self(raw) } } impl AsRef<[u8]> for TrieId { fn as_ref(&self) -> &[u8] { - &self.raw + &self.0 } } From d4968312b0a521b2e69d81e63c035bb78d39ee68 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Thu, 23 Nov 2023 11:06:22 +0000 Subject: [PATCH 29/34] Clippy --- crates/cargo-contract/src/cmd/storage.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 10a7c187f..6438f2466 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -94,9 +94,8 @@ impl StorageCommand { } Err(_) => { eprintln!( - "{} {}", + "{} Displaying raw storage: no valid contract metadata artifacts found", "Info:".cyan().bold(), - "Displaying raw storage: no valid contract metadata artifacts found" ); let storage_data = storage_layout .load_contract_storage_data(&self.contract) From 1f885667cefc3a897b7b6aef842c9bb0cd4c8724 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 24 Nov 2023 12:33:01 +0000 Subject: [PATCH 30/34] CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1af9c38d..8a0dcdb90 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add workspace support -[#1358](https://github.com/paritytech/cargo-contract/pull/1358) - Add `Storage Total Deposit` to `info` command output - [#1347](https://github.com/paritytech/cargo-contract/pull/1347) - Add dynamic types support - [#1399](https://github.com/paritytech/cargo-contract/pull/1399) +- Basic storage inspection command - [#1395](https://github.com/paritytech/cargo-contract/pull/1395) ### Fixed - Do not allow to execute calls on immutable contract messages - [#1397](https://github.com/paritytech/cargo-contract/pull/1397) From a74d78706116e93cdfd567076e8935138f1c1ade Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Fri, 24 Nov 2023 12:35:52 +0000 Subject: [PATCH 31/34] README.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 31bca03a1..8c33e772c 100644 --- a/README.md +++ b/README.md @@ -149,6 +149,10 @@ Remove a contract from a `pallet-contracts` enabled chain. See [extrinsics](crat Fetch and display contract information of a contract on chain. See [info](docs/info.md). +##### `cargo contract storage` + +Fetch and display the storage of a contract on chain. + ## Publishing In order to publish a new version of `cargo-contract`: From a7870f7b53aab002c0f8b134fad978bc9a4dd8f2 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 27 Nov 2023 09:59:55 +0000 Subject: [PATCH 32/34] Make `Client` generic --- crates/extrinsics/src/contract_info.rs | 12 ++++-------- crates/extrinsics/src/contract_storage.rs | 8 +++----- 2 files changed, 7 insertions(+), 13 deletions(-) diff --git a/crates/extrinsics/src/contract_info.rs b/crates/extrinsics/src/contract_info.rs index 068a77bbf..73f4f91d0 100644 --- a/crates/extrinsics/src/contract_info.rs +++ b/crates/extrinsics/src/contract_info.rs @@ -34,10 +34,7 @@ use std::fmt::{ use scale::Decode; use std::option::Option; use subxt::{ - backend::{ - legacy::LegacyRpcMethods, - BlockRef, - }, + backend::legacy::LegacyRpcMethods, dynamic::DecodedValueThunk, error::DecodeError, ext::{ @@ -51,17 +48,17 @@ use subxt::{ storage::dynamic, utils::AccountId32, Config, + OnlineClient, }; /// Return the account data for an account ID. async fn get_account_balance( account: &C::AccountId, rpc: &LegacyRpcMethods, - client: &Client, + client: &OnlineClient, ) -> Result where C::AccountId: AsRef<[u8]>, - BlockRef: From, { let storage_query = subxt::dynamic::storage("System", "Account", vec![Value::from_bytes(account)]); @@ -82,12 +79,11 @@ where pub async fn fetch_contract_info( contract: &C::AccountId, rpc: &LegacyRpcMethods, - client: &Client, + client: &OnlineClient, ) -> Result where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, - BlockRef: From, { let best_block = get_best_block(rpc).await?; diff --git a/crates/extrinsics/src/contract_storage.rs b/crates/extrinsics/src/contract_storage.rs index 312dff055..fae4abc59 100644 --- a/crates/extrinsics/src/contract_storage.rs +++ b/crates/extrinsics/src/contract_storage.rs @@ -36,7 +36,6 @@ use subxt::{ rpc_params, RpcClient, }, - BlockRef, }, error::DecodeError, ext::scale_decode::{ @@ -50,7 +49,6 @@ use subxt::{ use super::{ fetch_contract_info, url_to_string, - Client, ContractInfo, DefaultConfig, TrieId, @@ -64,7 +62,7 @@ impl ContractStorage where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, - BlockRef: From, + // BlockRef: From, { pub fn new(rpc: ContractStorageRpc) -> Self { Self { rpc } @@ -219,14 +217,14 @@ pub struct ContractStorageCell { pub struct ContractStorageRpc { rpc_client: RpcClient, rpc_methods: LegacyRpcMethods, - client: Client, + client: OnlineClient, } impl ContractStorageRpc where C::AccountId: AsRef<[u8]> + Display + IntoVisitor, DecodeError: From<<::Visitor as Visitor>::Error>, - BlockRef: From, + // BlockRef: From, { /// Create a new instance of the ContractsRpc. pub async fn new(url: &url::Url) -> Result { From 505542568b9e56e69578c1a626118659c43be4a9 Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 27 Nov 2023 10:01:45 +0000 Subject: [PATCH 33/34] Don't need `required_unless_present = "all"` --- crates/cargo-contract/src/cmd/storage.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index 6438f2466..dab4f3cd6 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -37,7 +37,6 @@ pub struct StorageCommand { name = "contract", long, env = "CONTRACT", - required_unless_present = "all" )] contract: ::AccountId, /// Fetch the "raw" storage keys and values for the contract. From 4558255944ff0d39c3471b4a056971a4973bd12c Mon Sep 17 00:00:00 2001 From: Andrew Jones Date: Mon, 27 Nov 2023 10:02:53 +0000 Subject: [PATCH 34/34] Fmt --- crates/cargo-contract/src/cmd/storage.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/crates/cargo-contract/src/cmd/storage.rs b/crates/cargo-contract/src/cmd/storage.rs index dab4f3cd6..ba2ac22e2 100644 --- a/crates/cargo-contract/src/cmd/storage.rs +++ b/crates/cargo-contract/src/cmd/storage.rs @@ -33,11 +33,7 @@ use subxt::Config; #[clap(name = "storage", about = "Inspect contract storage")] pub struct StorageCommand { /// The address of the contract to inspect storage of. - #[clap( - name = "contract", - long, - env = "CONTRACT", - )] + #[clap(name = "contract", long, env = "CONTRACT")] contract: ::AccountId, /// Fetch the "raw" storage keys and values for the contract. #[clap(long)]