diff --git a/CHANGELOG.md b/CHANGELOG.md index d9f35a222..9a26fc755 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `--binary` flag for `info` command - [#1311](https://github.com/paritytech/cargo-contract/pull/1311/) - Add `--all` flag for `info` command - [#1319](https://github.com/paritytech/cargo-contract/pull/1319) - Fix for a Url to String conversion in `info` command - [#1330](https://github.com/paritytech/cargo-contract/pull/1330) +- Add contract language detection feature for `info` command - [#1329](https://github.com/paritytech/cargo-contract/pull/1329) - Add warning message when using incompatible contract's ink! version - [#1334](https://github.com/paritytech/cargo-contract/pull/1334) - Bump `subxt` to `0.32.0` - [#1352](https://github.com/paritytech/cargo-contract/pull/1352) - Remove check for compatible `scale` and `scale-info` versions - [#1370](https://github.com/paritytech/cargo-contract/pull/1370) diff --git a/Cargo.lock b/Cargo.lock index b48cb9a69..11422145c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -749,6 +749,7 @@ dependencies = [ "assert_cmd", "clap", "colored", + "contract-analyze", "contract-build", "contract-extrinsics", "contract-metadata", @@ -977,6 +978,16 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "21a53c0a4d288377e7415b53dcfc3c04da5cdc2cc95c8d5ac178b58f0b861ad6" +[[package]] +name = "contract-analyze" +version = "0.1.0" +dependencies = [ + "anyhow", + "contract-metadata", + "parity-wasm", + "wabt", +] + [[package]] name = "contract-build" version = "4.0.0-alpha" diff --git a/crates/analyze/Cargo.toml b/crates/analyze/Cargo.toml new file mode 100644 index 000000000..cd637ad5a --- /dev/null +++ b/crates/analyze/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "contract-analyze" +version = "0.1.0" +authors = ["Parity Technologies "] +edition = "2021" + +[dependencies] +contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } +parity-wasm = { version = "0.45.0", features = ["sign_ext"] } +anyhow = "1.0.75" + +[dev-dependencies] +wabt = "0.10.0" diff --git a/crates/analyze/LICENSE b/crates/analyze/LICENSE new file mode 120000 index 000000000..30cff7403 --- /dev/null +++ b/crates/analyze/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/analyze/README.md b/crates/analyze/README.md new file mode 100644 index 000000000..9a1e2aef2 --- /dev/null +++ b/crates/analyze/README.md @@ -0,0 +1,6 @@ +# Contract Analyze + +Contains heuristic for determining source language for smart contract. + +Currently part of [`cargo-contract`](https://github.com/paritytech/cargo-contract), the build tool for smart + contracts written in [ink!](https://github.com/paritytech/ink). diff --git a/crates/analyze/src/lib.rs b/crates/analyze/src/lib.rs new file mode 100644 index 000000000..8e776bcf1 --- /dev/null +++ b/crates/analyze/src/lib.rs @@ -0,0 +1,315 @@ +// 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 . +#![deny(unused_crate_dependencies)] +use anyhow::{ + anyhow, + bail, + Result, +}; +pub use contract_metadata::Language; +use parity_wasm::elements::{ + External, + FuncBody, + FunctionType, + ImportSection, + Instruction, + Module, + Type, + TypeSection, + ValueType, +}; + +/// Detects the programming language of a smart contract from its WebAssembly (Wasm) +/// binary code. +/// +/// This function accepts a Wasm code as input and employs a set of heuristics to identify +/// the contract's source language. It currently supports detection for Ink!, Solidity, +/// and AssemblyScript languages. +pub fn determine_language(code: &[u8]) -> Result { + let wasm_module: Module = parity_wasm::deserialize_buffer(code)?; + let module = wasm_module.clone().parse_names().unwrap_or(wasm_module); + let start_section = module.start_section(); + + if start_section.is_none() && has_custom_section(&module, "producers") { + return Ok(Language::Solidity) + } else if start_section.is_some() && has_custom_section(&module, "sourceMappingURL") { + return Ok(Language::AssemblyScript) + } else if start_section.is_none() + && (is_ink_function_present(&module) || has_function_name(&module, "ink_env")) + { + return Ok(Language::Ink) + } + + bail!("Language unsupported or unrecognized") +} + +/// Checks if a ink! function is present. +fn is_ink_function_present(module: &Module) -> bool { + let import_section = module + .import_section() + .expect("Import setction shall be present"); + + // Signature for 'deny_payment' ink! function. + let ink_func_deny_payment_sig = + Type::Function(FunctionType::new(vec![], vec![ValueType::I32])); + // Signature for 'transferred_value' ink! function. + let ink_func_transferred_value_sig = + Type::Function(FunctionType::new(vec![ValueType::I32], vec![])); + + // The deny_payment and transferred_value functions internally call the + // value_transferred function. Getting its index from import section. + let value_transferred_index = + // For ink! >=4 + get_function_import_index(import_section, "value_transferred").or( + // For ink! ^3 + get_function_import_index(import_section, "seal_value_transferred"), + ); + + let mut functions: Vec<&FuncBody> = Vec::new(); + let function_signatures = + vec![&ink_func_deny_payment_sig, &ink_func_transferred_value_sig]; + + for signature in function_signatures { + if let Ok(mut func) = filter_function_by_type(module, signature) { + functions.append(&mut func); + } + } + + if let Ok(index) = value_transferred_index { + functions.iter().any(|&body| { + body.code().elements().iter().any(|instruction| { + // Matches the 'value_transferred' function. + matches!(instruction, &Instruction::Call(i) if i as usize == index) + }) + }) + } else { + false + } +} + +/// Check if any function in the 'name' section contains the specified name. +fn has_function_name(module: &Module, name: &str) -> bool { + // The contract compiled in debug mode includes function names in the name section. + module + .names_section() + .map(|section| { + if let Some(functions) = section.functions() { + functions + .names() + .iter() + .any(|(_, func)| func.contains(name)) + } else { + false + } + }) + .unwrap_or(false) +} + +/// Check if custom section is present. +fn has_custom_section(module: &Module, section_name: &str) -> bool { + module + .custom_sections() + .any(|section| section.name() == section_name) +} + +/// Get the function index from the import section. +fn get_function_import_index( + import_section: &ImportSection, + field: &str, +) -> Result { + import_section + .entries() + .iter() + .filter(|&entry| matches!(entry.external(), External::Function(_))) + .position(|e| e.field() == field) + .ok_or(anyhow!("Missing required import for: {}", field)) +} + +/// Get the function type index from the type section. +fn get_function_type_index( + type_section: &TypeSection, + function_type: &Type, +) -> Result { + type_section + .types() + .iter() + .position(|e| e == function_type) + .ok_or(anyhow!("Requested function type not found")) +} + +/// Search for a functions in a WebAssembly (Wasm) module that matches a given function +/// type. +/// +/// If one or more functions matching the specified type are found, this function returns +/// their bodies in a vector; otherwise, it returns an error. +fn filter_function_by_type<'a>( + module: &'a Module, + function_type: &Type, +) -> Result> { + let type_section = module + .type_section() + .ok_or(anyhow!("Missing required type section"))?; + let func_type_index = get_function_type_index(type_section, function_type)?; + + module + .function_section() + .ok_or(anyhow!("Missing required function section"))? + .entries() + .iter() + .enumerate() + .filter(|(_, elem)| elem.type_ref() == func_type_index as u32) + .map(|(index, _)| { + module + .code_section() + .ok_or(anyhow!("Missing required code section"))? + .bodies() + .get(index) + .ok_or(anyhow!("Requested function not found code section")) + }) + .collect::>>() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn failes_with_unsupported_language() { + let contract = r#" + (module + (type $none_=>_none (func)) + (type (;0;) (func (param i32 i32 i32))) + (import "env" "memory" (func (;5;) (type 0))) + (start $~start) + (func $~start (type $none_=>_none)) + (func (;5;) (type 0)) + ) + "#; + let code = wabt::wat2wasm(contract).expect("invalid wabt"); + let lang = determine_language(&code); + assert!(lang.is_err()); + assert_eq!( + lang.unwrap_err().to_string(), + "Language unsupported or unrecognized" + ); + } + + #[test] + fn determines_ink_language() { + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (type (;1;) (func (result i32))) + (type (;2;) (func (param i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (import "seal0" "value_transferred" (func (;1;) (type 2))) + (import "env" "memory" (memory (;0;) 2 16)) + (func (;2;) (type 2)) + (func (;3;) (type 1) (result i32) + (local i32 i64 i64) + global.get 0 + i32.const 32 + i32.sub + local.tee 0 + global.set 0 + local.get 0 + i64.const 0 + i64.store offset=8 + local.get 0 + i64.const 0 + i64.store + local.get 0 + i32.const 16 + i32.store offset=28 + local.get 0 + local.get 0 + i32.const 28 + i32.add + call 1 + local.get 0 + i64.load offset=8 + local.set 1 + local.get 0 + i64.load + local.set 2 + local.get 0 + i32.const 32 + i32.add + global.set 0 + i32.const 5 + i32.const 4 + local.get 1 + local.get 2 + i64.or + i64.eqz + select + ) + (global (;0;) (mut i32) (i32.const 65536)) + )"#; + let code = wabt::wat2wasm(contract).expect("invalid wabt"); + let lang = determine_language(&code); + assert!( + matches!(lang, Ok(Language::Ink)), + "Failed to detect Ink! language" + ); + } + + #[test] + fn determines_solidity_language() { + let contract = r#" + (module + (type (;0;) (func (param i32 i32 i32))) + (import "env" "memory" (memory (;0;) 16 16)) + (func (;0;) (type 0)) + ) + "#; + let code = wabt::wat2wasm(contract).expect("invalid wabt"); + // Custom sections are not supported in wabt format, injecting using parity_wasm + let mut module: Module = parity_wasm::deserialize_buffer(&code).unwrap(); + module.set_custom_section("producers".to_string(), Vec::new()); + let code = module.into_bytes().unwrap(); + let lang = determine_language(&code); + assert!( + matches!(lang, Ok(Language::Solidity)), + "Failed to detect Solidity language" + ); + } + + #[test] + fn determines_assembly_script_language() { + let contract = r#" + (module + (type $none_=>_none (func)) + (type (;0;) (func (param i32 i32 i32))) + (import "seal" "foo" (func (;0;) (type 0))) + (import "env" "memory" (memory $0 2 16)) + (start $~start) + (func $~start (type $none_=>_none)) + (func (;1;) (type 0)) + ) + "#; + let code = wabt::wat2wasm(contract).expect("invalid wabt"); + // Custom sections are not supported in wabt format, injecting using parity_wasm + let mut module: Module = parity_wasm::deserialize_buffer(&code).unwrap(); + module.set_custom_section("sourceMappingURL".to_string(), Vec::new()); + let code = module.into_bytes().unwrap(); + let lang = determine_language(&code); + assert!( + matches!(lang, Ok(Language::AssemblyScript)), + "Failed to detect AssemblyScript language" + ); + } +} diff --git a/crates/cargo-contract/Cargo.toml b/crates/cargo-contract/Cargo.toml index c6cefe66f..8dc58f881 100644 --- a/crates/cargo-contract/Cargo.toml +++ b/crates/cargo-contract/Cargo.toml @@ -22,6 +22,7 @@ contract-build = { version = "4.0.0-alpha", path = "../build" } contract-extrinsics = { version = "4.0.0-alpha", path = "../extrinsics" } contract-transcode = { version = "4.0.0-alpha", path = "../transcode" } contract-metadata = { version = "4.0.0-alpha", path = "../metadata" } +contract-analyze = { version = "0.1.0", path = "../analyze" } anyhow = "1.0.75" clap = { version = "4.4.6", features = ["derive", "env"] } diff --git a/crates/cargo-contract/src/cmd/info.rs b/crates/cargo-contract/src/cmd/info.rs index 40623fa51..5d165d878 100644 --- a/crates/cargo-contract/src/cmd/info.rs +++ b/crates/cargo-contract/src/cmd/info.rs @@ -15,7 +15,7 @@ // along with cargo-contract. If not, see . use super::{ - basic_display_format_contract_info, + basic_display_format_extended_contract_info, display_all_contracts, DefaultConfig, }; @@ -23,11 +23,15 @@ use anyhow::{ anyhow, Result, }; +use contract_analyze::determine_language; use contract_extrinsics::{ fetch_all_contracts, fetch_contract_info, fetch_wasm_code, url_to_string, + Balance, + CodeHash, + ContractInfo, ErrorVariant, }; use std::{ @@ -108,15 +112,14 @@ impl InfoCommand { 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 { - 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 - ))?; - if self.output_json { let wasm = serde_json::json!({ "wasm": format!("0x{}", hex::encode(wasm_code)) @@ -128,11 +131,45 @@ impl InfoCommand { .expect("Writing to stdout failed") } } else if self.output_json { - println!("{}", info_to_json.to_json()?) + println!( + "{}", + serde_json::to_string_pretty(&ExtendedContractInfo::new( + info_to_json, + &wasm_code + ))? + ) } else { - basic_display_format_contract_info(&info_to_json) + basic_display_format_extended_contract_info(&ExtendedContractInfo::new( + info_to_json, + &wasm_code, + )) } Ok(()) } } } + +#[derive(serde::Serialize)] +pub struct ExtendedContractInfo { + pub trie_id: String, + pub code_hash: CodeHash, + pub storage_items: u32, + pub storage_item_deposit: Balance, + pub source_language: String, +} + +impl ExtendedContractInfo { + pub fn new(contract_info: ContractInfo, code: &[u8]) -> Self { + let language = match determine_language(code).ok() { + Some(lang) => lang.to_string(), + None => "Unknown".to_string(), + }; + ExtendedContractInfo { + trie_id: contract_info.trie_id().to_string(), + code_hash: *contract_info.code_hash(), + storage_items: contract_info.storage_items(), + storage_item_deposit: contract_info.storage_item_deposit(), + source_language: language, + } + } +} diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 6fcac4275..aa1078f95 100644 --- a/crates/cargo-contract/src/cmd/mod.rs +++ b/crates/cargo-contract/src/cmd/mod.rs @@ -31,7 +31,10 @@ pub(crate) use self::{ }, call::CallCommand, decode::DecodeCommand, - info::InfoCommand, + info::{ + ExtendedContractInfo, + InfoCommand, + }, instantiate::InstantiateCommand, remove::RemoveCommand, upload::UploadCommand, @@ -58,7 +61,6 @@ pub(crate) use contract_extrinsics::ErrorVariant; use contract_extrinsics::{ Balance, BalanceVariant, - ContractInfo, }; use pallet_contracts_primitives::ContractResult; use std::io::{ @@ -215,21 +217,26 @@ pub fn print_gas_required_success(gas: Weight) { } /// Display contract information in a formatted way -pub fn basic_display_format_contract_info(info: &ContractInfo) { - name_value_println!("TrieId", format!("{}", info.trie_id()), MAX_KEY_COL_WIDTH); +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!( "Code Hash", - format!("{:?}", info.code_hash()), + format!("{:?}", info.code_hash), MAX_KEY_COL_WIDTH ); name_value_println!( "Storage Items", - format!("{:?}", info.storage_items()), + format!("{:?}", info.storage_items), MAX_KEY_COL_WIDTH ); name_value_println!( "Storage Deposit", - format!("{:?}", info.storage_item_deposit()), + format!("{:?}", info.storage_item_deposit), + MAX_KEY_COL_WIDTH + ); + name_value_println!( + "Source Language", + format!("{}", info.source_language), MAX_KEY_COL_WIDTH ); }