Skip to content

Commit

Permalink
Contracts: use compiled rust tests (paritytech#2347)
Browse files Browse the repository at this point in the history
see paritytech#2189

This PR does the following:
- Bring the user api functions into a new pallet-contracts-uapi (They
are currently defined in ink!
[here])(https://github.com/paritytech/ink/blob/master/crates/env/src/engine/on_chain/ext.rs)
- Add older api versions and unstable to the user api trait.
- Remove pallet-contracts-primitives and bring the types it defined in
uapi / pallet-contracts
- Add the infrastructure to build fixtures from Rust files and test it
works by replacing `dummy.wat` and `call.wat`
- Move all the doc from wasm/runtime.rs to pallet-contracts-uapi.

This will be done in a follow up:
- convert the rest of the test from .wat to rust
- bring risc-v uapi up to date with wasm
- finalize the uapi host fns, making sure everything is codegen from the
source host fns in pallet-contracts

---------

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
  • Loading branch information
pgherveou and athei authored Nov 29, 2023
1 parent 8b0f267 commit 78559a0
Show file tree
Hide file tree
Showing 34 changed files with 2,447 additions and 943 deletions.
2 changes: 0 additions & 2 deletions substrate/bin/node/runtime/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ pallet-broker = { path = "../../../frame/broker", default-features = false}
pallet-child-bounties = { path = "../../../frame/child-bounties", default-features = false}
pallet-collective = { path = "../../../frame/collective", default-features = false}
pallet-contracts = { path = "../../../frame/contracts", default-features = false}
pallet-contracts-primitives = { path = "../../../frame/contracts/primitives", default-features = false}
pallet-conviction-voting = { path = "../../../frame/conviction-voting", default-features = false}
pallet-core-fellowship = { path = "../../../frame/core-fellowship", default-features = false}
pallet-democracy = { path = "../../../frame/democracy", default-features = false}
Expand Down Expand Up @@ -171,7 +170,6 @@ std = [
"pallet-broker/std",
"pallet-child-bounties/std",
"pallet-collective/std",
"pallet-contracts-primitives/std",
"pallet-contracts/std",
"pallet-conviction-voting/std",
"pallet-core-fellowship/std",
Expand Down
10 changes: 5 additions & 5 deletions substrate/bin/node/runtime/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2497,7 +2497,7 @@ impl_runtime_apis! {
gas_limit: Option<Weight>,
storage_deposit_limit: Option<Balance>,
input_data: Vec<u8>,
) -> pallet_contracts_primitives::ContractExecResult<Balance, EventRecord> {
) -> pallet_contracts::ContractExecResult<Balance, EventRecord> {
let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block);
Contracts::bare_call(
origin,
Expand All @@ -2517,10 +2517,10 @@ impl_runtime_apis! {
value: Balance,
gas_limit: Option<Weight>,
storage_deposit_limit: Option<Balance>,
code: pallet_contracts_primitives::Code<Hash>,
code: pallet_contracts::Code<Hash>,
data: Vec<u8>,
salt: Vec<u8>,
) -> pallet_contracts_primitives::ContractInstantiateResult<AccountId, Balance, EventRecord>
) -> pallet_contracts::ContractInstantiateResult<AccountId, Balance, EventRecord>
{
let gas_limit = gas_limit.unwrap_or(RuntimeBlockWeights::get().max_block);
Contracts::bare_instantiate(
Expand All @@ -2541,7 +2541,7 @@ impl_runtime_apis! {
code: Vec<u8>,
storage_deposit_limit: Option<Balance>,
determinism: pallet_contracts::Determinism,
) -> pallet_contracts_primitives::CodeUploadResult<Hash, Balance>
) -> pallet_contracts::CodeUploadResult<Hash, Balance>
{
Contracts::bare_upload_code(
origin,
Expand All @@ -2554,7 +2554,7 @@ impl_runtime_apis! {
fn get_storage(
address: AccountId,
key: Vec<u8>,
) -> pallet_contracts_primitives::GetStorageResult {
) -> pallet_contracts::GetStorageResult {
Contracts::get_storage(
address,
key
Expand Down
4 changes: 1 addition & 3 deletions substrate/frame/contracts/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ frame-benchmarking = { path = "../benchmarking", default-features = false, optio
frame-support = { path = "../support", default-features = false}
frame-system = { path = "../system", default-features = false}
pallet-balances = { path = "../balances", default-features = false , optional = true}
pallet-contracts-primitives = { path = "primitives", default-features = false}
pallet-contracts-uapi = { path = "uapi" }
pallet-contracts-proc-macro = { path = "proc-macro" }
sp-api = { path = "../../primitives/api", default-features = false}
sp-core = { path = "../../primitives/core", default-features = false}
Expand Down Expand Up @@ -83,8 +83,6 @@ std = [
"frame-system/std",
"log/std",
"pallet-balances?/std",
"pallet-contracts-fixtures/std",
"pallet-contracts-primitives/std",
"pallet-contracts-proc-macro/full",
"pallet-insecure-randomness-collective-flip/std",
"pallet-proxy/std",
Expand Down
1 change: 0 additions & 1 deletion substrate/frame/contracts/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,5 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
version - 1,
)?;

println!("cargo:rerun-if-changed=src/migration");
Ok(())
}
16 changes: 11 additions & 5 deletions substrate/frame/contracts/fixtures/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@ description = "Fixtures for testing contracts pallet."

[dependencies]
wat = "1"
frame-system = { path = "../../system", default-features = false}
sp-runtime = { path = "../../../primitives/runtime", default-features = false}
frame-system = { path = "../../system" }
sp-runtime = { path = "../../../primitives/runtime" }
anyhow = "1.0.0"

[build-dependencies]
parity-wasm = "0.45.0"
tempfile = "3.8.1"
toml = "0.8.8"
twox-hash = "1.6.3"
anyhow = "1.0.0"
cfg-if = { version = "1.0", default-features = false }

[features]
default = [ "std" ]
std = [ "frame-system/std", "sp-runtime/std" ]

277 changes: 277 additions & 0 deletions substrate/frame/contracts/fixtures/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,277 @@
// This file is part of Substrate.

// Copyright (C) Parity Technologies (UK) Ltd.
// SPDX-License-Identifier: Apache-2.0

// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

//! Compile contracts to wasm and RISC-V binaries.
use anyhow::Result;
use parity_wasm::elements::{deserialize_file, serialize_to_file, Internal};
use std::{
env, fs,
hash::Hasher,
path::{Path, PathBuf},
process::Command,
};
use twox_hash::XxHash32;

/// Read the file at `path` and return its hash as a hex string.
fn file_hash(path: &Path) -> String {
let data = fs::read(path).expect("file exists; qed");
let mut hasher = XxHash32::default();
hasher.write(&data);
hasher.write(include_bytes!("build.rs"));
let hash = hasher.finish();
format!("{:x}", hash)
}

/// A contract entry.
struct Entry {
/// The path to the contract source file.
path: PathBuf,
/// The hash of the contract source file.
hash: String,
}

impl Entry {
/// Create a new contract entry from the given path.
fn new(path: PathBuf) -> Self {
let hash = file_hash(&path);
Self { path, hash }
}

/// Return the path to the contract source file.
fn path(&self) -> &str {
self.path.to_str().expect("path is valid unicode; qed")
}

/// Return the name of the contract.
fn name(&self) -> &str {
self.path
.file_stem()
.expect("file exits; qed")
.to_str()
.expect("name is valid unicode; qed")
}

/// Return the name of the output wasm file.
fn out_wasm_filename(&self) -> String {
format!("{}.wasm", self.name())
}
}

/// Collect all contract entries from the given source directory.
/// Contracts that have already been compiled are filtered out.
fn collect_entries(contracts_dir: &Path, out_dir: &Path) -> Vec<Entry> {
fs::read_dir(&contracts_dir)
.expect("src dir exists; qed")
.filter_map(|file| {
let path = file.expect("file exists; qed").path();
if path.extension().map_or(true, |ext| ext != "rs") {
return None;
}

let entry = Entry::new(path);
if out_dir.join(&entry.hash).exists() {
None
} else {
Some(entry)
}
})
.collect::<Vec<_>>()
}

/// Create a `Cargo.toml` to compile the given contract entries.
fn create_cargo_toml<'a>(
fixtures_dir: &Path,
entries: impl Iterator<Item = &'a Entry>,
output_dir: &Path,
) -> Result<()> {
let uapi_path = fixtures_dir.join("../uapi").canonicalize()?;
let common_path = fixtures_dir.join("./contracts/common").canonicalize()?;
let mut cargo_toml: toml::Value = toml::from_str(&format!(
"
[package]
name = 'contracts'
version = '0.1.0'
edition = '2021'
# Binary targets are injected below.
[[bin]]
[dependencies]
uapi = {{ package = 'pallet-contracts-uapi', default-features = false, path = {uapi_path:?}}}
common = {{ package = 'pallet-contracts-fixtures-common', path = {common_path:?}}}
[profile.release]
opt-level = 3
lto = true
codegen-units = 1
"
))?;

let binaries = entries
.map(|entry| {
let name = entry.name();
let path = entry.path();
toml::Value::Table(toml::toml! {
name = name
path = path
})
})
.collect::<Vec<_>>();

cargo_toml["bin"] = toml::Value::Array(binaries);
let cargo_toml = toml::to_string_pretty(&cargo_toml)?;
fs::write(output_dir.join("Cargo.toml"), cargo_toml).map_err(Into::into)
}

/// Invoke `cargo fmt` to check that fixtures files are formatted.
fn invoke_cargo_fmt<'a>(
config_path: &Path,
files: impl Iterator<Item = &'a Path>,
contract_dir: &Path,
) -> Result<()> {
// If rustfmt is not installed, skip the check.
if !Command::new("rustup")
.args(&["run", "nightly", "rustfmt", "--version"])
.output()
.expect("failed to execute process")
.status
.success()
{
return Ok(())
}

let fmt_res = Command::new("rustup")
.args(&["run", "nightly", "rustfmt", "--check", "--config-path"])
.arg(config_path)
.args(files)
.output()
.expect("failed to execute process");

if fmt_res.status.success() {
return Ok(())
}

let stdout = String::from_utf8_lossy(&fmt_res.stdout);
let stderr = String::from_utf8_lossy(&fmt_res.stderr);
eprintln!("{}\n{}", stdout, stderr);
eprintln!(
"Fixtures files are not formatted.\nPlease run `rustup run nightly rustfmt --config-path {} {}/*.rs`",
config_path.display(),
contract_dir.display()
);
anyhow::bail!("Fixtures files are not formatted")
}

/// Invoke `cargo build` to compile the contracts.
fn invoke_build(current_dir: &Path) -> Result<()> {
let encoded_rustflags = [
"-Clink-arg=-zstack-size=65536",
"-Clink-arg=--import-memory",
"-Clinker-plugin-lto",
"-Ctarget-cpu=mvp",
"-Dwarnings",
]
.join("\x1f");

let build_res = Command::new(env::var("CARGO")?)
.current_dir(current_dir)
.env("CARGO_ENCODED_RUSTFLAGS", encoded_rustflags)
.args(&["build", "--release", "--target=wasm32-unknown-unknown"])
.output()
.expect("failed to execute process");

if build_res.status.success() {
return Ok(())
}

let stderr = String::from_utf8_lossy(&build_res.stderr);
eprintln!("{}", stderr);
anyhow::bail!("Failed to build contracts");
}

/// Post-process the compiled wasm contracts.
fn post_process_wasm(input_path: &Path, output_path: &Path) -> Result<()> {
let mut module = deserialize_file(input_path)?;
if let Some(section) = module.export_section_mut() {
section.entries_mut().retain(|entry| {
matches!(entry.internal(), Internal::Function(_)) &&
(entry.field() == "call" || entry.field() == "deploy")
});
}

serialize_to_file(output_path, module).map_err(Into::into)
}

/// Write the compiled contracts to the given output directory.
fn write_output(build_dir: &Path, out_dir: &Path, entries: Vec<Entry>) -> Result<()> {
for entry in entries {
let wasm_output = entry.out_wasm_filename();
post_process_wasm(
&build_dir.join("target/wasm32-unknown-unknown/release").join(&wasm_output),
&out_dir.join(&wasm_output),
)?;
fs::write(out_dir.join(&entry.hash), "")?;
}

Ok(())
}

/// Returns the root path of the wasm workspace.
fn find_workspace_root(current_dir: &Path) -> Option<PathBuf> {
let mut current_dir = current_dir.to_path_buf();

while current_dir.parent().is_some() {
if current_dir.join("Cargo.toml").exists() {
let cargo_toml_contents =
std::fs::read_to_string(current_dir.join("Cargo.toml")).ok()?;
if cargo_toml_contents.contains("[workspace]") {
return Some(current_dir);
}
}

current_dir.pop();
}

None
}

fn main() -> Result<()> {
let fixtures_dir: PathBuf = env::var("CARGO_MANIFEST_DIR")?.into();
let contracts_dir = fixtures_dir.join("contracts");
let out_dir: PathBuf = env::var("OUT_DIR")?.into();
let workspace_root = find_workspace_root(&fixtures_dir).expect("workspace root exists; qed");

let entries = collect_entries(&contracts_dir, &out_dir);
if entries.is_empty() {
return Ok(());
}

let tmp_dir = tempfile::tempdir()?;
let tmp_dir_path = tmp_dir.path();

create_cargo_toml(&fixtures_dir, entries.iter(), tmp_dir.path())?;
invoke_cargo_fmt(
&workspace_root.join(".rustfmt.toml"),
entries.iter().map(|entry| &entry.path as _),
&contracts_dir,
)?;

invoke_build(tmp_dir_path)?;
write_output(tmp_dir_path, &out_dir, entries)?;

Ok(())
}
Loading

0 comments on commit 78559a0

Please sign in to comment.