Skip to content

Commit

Permalink
Add determine contract language support
Browse files Browse the repository at this point in the history
  • Loading branch information
smiasojed committed Sep 12, 2023
1 parent 5173489 commit 8898876
Show file tree
Hide file tree
Showing 9 changed files with 247 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Add `verify` command - [#1306](https://github.com/paritytech/cargo-contract/pull/1306)
- 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)
- Add contract language detection feature for `info` command - [#1319](https://github.com/paritytech/cargo-contract/pull/1329)

## [4.0.0-alpha]

Expand Down
11 changes: 11 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

13 changes: 13 additions & 0 deletions crates/analyze/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
[package]
name = "contract-analyze"
version = "0.1.0"
authors = ["Parity Technologies <admin@parity.io>"]
edition = "2021"

[dependencies]
contract-metadata = { version = "4.0.0-alpha", path = "../metadata" }
parity-wasm = { version = "0.45.0", features = ["bulk"] }
anyhow = "1.0.75"

[dev-dependencies]
wabt = "0.10.0"
1 change: 1 addition & 0 deletions crates/analyze/LICENSE
6 changes: 6 additions & 0 deletions crates/analyze/README.md
Original file line number Diff line number Diff line change
@@ -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).
153 changes: 153 additions & 0 deletions crates/analyze/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
// 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 <http://www.gnu.org/licenses/>.
#![deny(unused_crate_dependencies)]
use anyhow::{
anyhow,
bail,
Result,
};
pub use contract_metadata::Language;
use parity_wasm::elements::Module;

/// 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.
///
/// If multiple language patterns are found in the code, the function returns an error.
pub fn determine_language(code: &[u8]) -> Result<Language> {
let module: Module = parity_wasm::deserialize_buffer(code)?;
let import_section = module.import_section();
let start_section = module.start_section();
let mut custom_sections = module.custom_sections().map(|e| e.name()).peekable();

let import_section_first = import_section
.ok_or(anyhow!("Missing required import section"))?
.entries()
.first()
.map(|e| e.field())
.ok_or(anyhow!("Missing required import section"))?;

if import_section_first != "memory"
&& start_section.is_none()
&& custom_sections.peek().is_none()
{
return Ok(Language::Ink)
} else if import_section_first == "memory"
&& start_section.is_none()
&& custom_sections.any(|e| e == "name")
{
return Ok(Language::Solidity)
} else if import_section_first != "memory"
&& start_section.is_some()
&& custom_sections.any(|e| e == "sourceMappingURL")
{
return Ok(Language::AssemblyScript)
}

bail!("Language unsupported or unrecognized")
}

#[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)))
(import "seal" "foo" (func (;5;) (type 0)))
(import "env" "memory" (func (;5;) (type 0)))
(func (;5;) (type 0))
)"#;
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" (func (;5;) (type 0)))
(func (;5;) (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("name".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 (;5;) (type 0)))
(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");
// 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"
);
}
}
1 change: 1 addition & 0 deletions crates/cargo-contract/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.2", features = ["derive", "env"] }
Expand Down
57 changes: 47 additions & 10 deletions crates/cargo-contract/src/cmd/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,18 +15,22 @@
// along with cargo-contract. If not, see <http://www.gnu.org/licenses/>.

use super::{
basic_display_format_contract_info,
basic_display_format_extended_contract_info,
display_all_contracts,
DefaultConfig,
};
use anyhow::{
anyhow,
Result,
};
use contract_analyze::determine_language;
use contract_extrinsics::{
fetch_all_contracts,
fetch_contract_info,
fetch_wasm_code,
Balance,
CodeHash,
ContractInfo,
ErrorVariant,
};
use std::{
Expand Down Expand Up @@ -114,15 +118,14 @@ impl InfoCommand {
contract
))?;

let wasm_code = fetch_wasm_code(&client, 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, 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))
Expand All @@ -134,11 +137,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,
}
}
}
21 changes: 14 additions & 7 deletions crates/cargo-contract/src/cmd/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ pub(crate) use self::{
},
call::CallCommand,
decode::DecodeCommand,
info::InfoCommand,
info::{
ExtendedContractInfo,
InfoCommand,
},
instantiate::InstantiateCommand,
remove::RemoveCommand,
upload::UploadCommand,
Expand All @@ -58,7 +61,6 @@ pub(crate) use contract_extrinsics::ErrorVariant;
use contract_extrinsics::{
Balance,
BalanceVariant,
ContractInfo,
};
use pallet_contracts_primitives::ContractResult;
use std::io::{
Expand Down Expand Up @@ -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
);
}
Expand Down

0 comments on commit 8898876

Please sign in to comment.