diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index 0f9ad0545..85349403b 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -85,7 +85,27 @@ jobs: - name: Run integration tests env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: cargo nextest run --locked --no-default-features --features "contract,integration-tests" --test contract --no-fail-fast --nocapture --status-level all + run: | + if [[ "${{ matrix.os }}" == "macos-latest" ]]; then + cargo nextest run \ + --locked \ + --no-default-features \ + --features "contract,integration-tests" \ + --test contract \ + --no-fail-fast \ + --nocapture \ + --status-level all \ + -- --skip verifiable_contract_lifecycle + else + cargo nextest run \ + --locked \ + --no-default-features \ + --features "contract,integration-tests" \ + --test contract \ + --no-fail-fast \ + --nocapture \ + --status-level all + fi chain-integration-tests: strategy: diff --git a/Cargo.lock b/Cargo.lock index 9961bc297..1f23a75b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2098,8 +2098,7 @@ checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6" [[package]] name = "contract-build" version = "6.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "66ae0d8673738134e7e63d672685287486dc2054a570081f9291b8b26fbd989d" +source = "git+https://github.com/use-ink/cargo-contract?rev=ceb7b89#ceb7b89f807b0e446dce68c007511d5dbc171277" dependencies = [ "alloy-json-abi", "anyhow", @@ -2142,8 +2141,7 @@ dependencies = [ [[package]] name = "contract-extrinsics" version = "6.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c19e4b6d9f2effa08a76ddfe317ac1370298bb25e2b938aa15122eabb9dd26c5" +source = "git+https://github.com/use-ink/cargo-contract?rev=ceb7b89#ceb7b89f807b0e446dce68c007511d5dbc171277" dependencies = [ "anyhow", "colored", @@ -2173,8 +2171,7 @@ dependencies = [ [[package]] name = "contract-metadata" version = "6.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8401a04d171e2867eaf270755b4dbd852a260541dbeb0f5085b4b63ec63be88" +source = "git+https://github.com/use-ink/cargo-contract?rev=ceb7b89#ceb7b89f807b0e446dce68c007511d5dbc171277" dependencies = [ "anyhow", "impl-serde", @@ -2187,8 +2184,7 @@ dependencies = [ [[package]] name = "contract-transcode" version = "6.0.0-beta.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4ddd5e0f972d2358bea1db6469de2d5c5a70143b1a98d433c207e86981a0417" +source = "git+https://github.com/use-ink/cargo-contract?rev=ceb7b89#ceb7b89f807b0e446dce68c007511d5dbc171277" dependencies = [ "anyhow", "base58", diff --git a/Cargo.toml b/Cargo.toml index ef4872d61..071e0d7a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,9 +57,9 @@ sp-weights = { version = "33.0.0", default-features = false } scale = { package = "parity-scale-codec", version = "3.7.5", features = ["derive"] } scale-info = { version = "2.11.6", default-features = false, features = ["derive"] } scale-value = { version = "0.18.0", default-features = false, features = ["from-string", "parser-ss58"] } -contract-build = { version = "6.0.0-beta.1", default-features = false } -contract-extrinsics = { version = "6.0.0-beta.1", default-features = false } -contract-transcode = { version = "6.0.0-beta.1", default-features = false } +contract-build = { git = "https://github.com/use-ink/cargo-contract", rev = "ceb7b89", default-features = false } +contract-extrinsics = { git = "https://github.com/use-ink/cargo-contract", rev = "ceb7b89", default-features = false } +contract-transcode = { git = "https://github.com/use-ink/cargo-contract", rev = "ceb7b89", default-features = false } heck = { version = "0.5.0", default-features = false } # parachains diff --git a/crates/pop-cli/src/commands/build/contract.rs b/crates/pop-cli/src/commands/build/contract.rs index d11a8e679..5fe4d8347 100644 --- a/crates/pop-cli/src/commands/build/contract.rs +++ b/crates/pop-cli/src/commands/build/contract.rs @@ -1,17 +1,21 @@ // SPDX-License-Identifier: GPL-3.0 +use super::{BuildArgs, Profile}; use crate::cli; -use pop_contracts::{MetadataSpec, Verbosity, build_smart_contract}; +use pop_contracts::{BuildMode, ImageVariant, MetadataSpec, Verbosity, build_smart_contract}; use std::path::PathBuf; /// Configuration for building a smart contract. pub struct BuildContract { /// Path of the contract project. pub(crate) path: PathBuf, - /// Build profile: `true` for release mode, `false` for debug mode. - pub(crate) release: bool, + /// Build profile: `Release` for release mode, `Debug` for debug mode, `Verifiable` for + /// deterministic, release mode. + pub(crate) build_mode: BuildMode, /// Which specification to use for contract metadata. pub(crate) metadata: Option, + /// A custom image for a verifiable build + pub(crate) image: Option, } impl BuildContract { @@ -27,10 +31,107 @@ impl BuildContract { fn build(self, cli: &mut impl cli::traits::Cli) -> anyhow::Result<&'static str> { cli.intro("Building your contract")?; // Build contract. - let build_result = - build_smart_contract(&self.path, self.release, Verbosity::Default, self.metadata)?; + let build_result = build_smart_contract( + &self.path, + self.build_mode, + Verbosity::Default, + self.metadata, + self.image, + )?; cli.success(build_result.display())?; cli.outro("Build completed successfully!")?; Ok("contract") } } + +/// Resolve the `BuildMode` to use in a contract build depending on the specified args +/// +/// # Arguments +/// * `args` - The `BuildArgs` needed to resolve the `BuildMode` +pub(super) fn resolve_build_mode(args: &BuildArgs) -> BuildMode { + match (&args.profile, args.verifiable) { + (Some(Profile::Release), false) | (Some(Profile::Production), false) => BuildMode::Release, + (None, true) => BuildMode::Verifiable, + (None, false) if args.release => BuildMode::Release, + // Fallback to debug mode + _ => BuildMode::Debug, + } +} + +pub(super) fn resolve_image(args: &BuildArgs) -> anyhow::Result> { + match (&args.image, args.verifiable) { + (Some(image), true) => Ok(Some(ImageVariant::Custom(image.clone()))), + (None, true) => Ok(Some(ImageVariant::Default)), + (None, false) => Ok(None), + (Some(_), false) => + Err(anyhow::anyhow!("Custom images can only be used in verifiable builds")), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn resolve_build_mode_works() { + // Profile::Release + verifiable=false -> Release + assert_eq!( + resolve_build_mode(&BuildArgs { + profile: Some(Profile::Release), + ..Default::default() + }), + BuildMode::Release + ); + // Profile::Production + verifiable=false -> Release + assert_eq!( + resolve_build_mode(&BuildArgs { + profile: Some(Profile::Production), + ..Default::default() + }), + BuildMode::Release + ); + // No profile + verifiable=true -> Verifiable + assert_eq!( + resolve_build_mode(&BuildArgs { verifiable: true, ..Default::default() }), + BuildMode::Verifiable + ); + // No profile + verifiable=false + release=true -> Release + assert_eq!( + resolve_build_mode(&BuildArgs { release: true, ..Default::default() }), + BuildMode::Release + ); + // Profile::Debug + verifiable=false -> Debug + assert_eq!( + resolve_build_mode(&BuildArgs { profile: Some(Profile::Debug), ..Default::default() }), + BuildMode::Debug + ); + // No profile + verifiable=false + release=false -> Debug + assert_eq!(resolve_build_mode(&BuildArgs::default()), BuildMode::Debug); + } + + #[test] + fn resolve_image_works() { + // Custom image + verifiable=true -> Custom image + assert!(matches!(resolve_image(&BuildArgs { + image: Some("my-image:latest".to_string()), + verifiable: true, + ..Default::default() + }), Ok(Some(ImageVariant::Custom(custom))) if custom == "my-image:latest" + )); + // No image + verifiable=true -> Default image + assert!(matches!( + resolve_image(&BuildArgs { verifiable: true, ..Default::default() }), + Ok(Some(ImageVariant::Default)) + )); + // No image + verifiable=false -> None + assert!(matches!(resolve_image(&BuildArgs::default()), Ok(None))); + // Custom image + verifiable=false -> Error + let err = resolve_image(&BuildArgs { + image: Some("my-image:latest".to_string()), + verifiable: false, + ..Default::default() + }) + .unwrap_err(); + assert_eq!(err.to_string(), "Custom images can only be used in verifiable builds"); + } +} diff --git a/crates/pop-cli/src/commands/build/mod.rs b/crates/pop-cli/src/commands/build/mod.rs index 690aa0fbc..52e7ca001 100644 --- a/crates/pop-cli/src/commands/build/mod.rs +++ b/crates/pop-cli/src/commands/build/mod.rs @@ -47,7 +47,7 @@ pub(crate) struct BuildArgs { #[cfg(feature = "chain")] pub command: Option, /// Directory path with flag for your project [default: current directory] - #[arg(long)] + #[arg(long, visible_alias = "manifest-path")] pub(crate) path: Option, /// Directory path without flag for your project [default: current directory] #[arg(value_name = "PATH", index = 1, conflicts_with = "path")] @@ -84,10 +84,15 @@ pub(crate) struct BuildArgs { #[clap(long, help_heading = CONTRACT_HELP_HEADER)] #[cfg(feature = "contract")] pub(crate) metadata: Option, - /// Whether to build in a way that the contract is verifiable - ///#[clap(long, help_heading = CONTRACT_HELP_HEADER)] - ///#[cfg(feature = "contract")] - ///pub(crate) verifiable: bool + /// Whether to build in a way that the contract is verifiable. For verifiable contracts, use + /// --manifest-path instead of --path to directly point to your contracts Cargo.toml + #[clap(long, conflicts_with_all = ["release", "profile"], help_heading = CONTRACT_HELP_HEADER)] + #[cfg(feature = "contract")] + pub(crate) verifiable: bool, + /// Custom image for verifiable builds + #[clap(long, requires = "verifiable", help_heading = CONTRACT_HELP_HEADER)] + #[cfg(feature = "contract")] + pub(crate) image: Option, } /// Subcommand for building chain artifacts. @@ -120,12 +125,10 @@ impl Command { #[cfg(feature = "contract")] if pop_contracts::is_supported(&project_path)? { - // All commands originating from root command are valid - let release = match &args.profile { - Some(profile) => (*profile).into(), - None => args.release, - }; - BuildContract { path: project_path, release, metadata: args.metadata }.execute()?; + let build_mode = contract::resolve_build_mode(args); + let image = contract::resolve_image(args)?; + BuildContract { path: project_path, build_mode, metadata: args.metadata, image } + .execute()?; return Ok(()); } @@ -361,6 +364,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, }, project_path, &mut cli, @@ -429,6 +436,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; Ok(()) @@ -474,6 +485,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; // Test 2: Execute with production profile @@ -496,6 +511,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; // Test 3: Execute with custom features @@ -515,6 +534,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; } @@ -538,6 +561,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; // Test 5: Execute with path_pos instead of path @@ -560,6 +587,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; // Test 6: Execute with benchmark and try_runtime flags @@ -579,6 +610,10 @@ mod tests { only_runtime: false, #[cfg(feature = "contract")] metadata: None, + #[cfg(feature = "contract")] + verifiable: false, + #[cfg(feature = "contract")] + image: None, })?; } diff --git a/crates/pop-cli/src/commands/call/contract.rs b/crates/pop-cli/src/commands/call/contract.rs index 10bb27ab3..1e6b26f6c 100644 --- a/crates/pop-cli/src/commands/call/contract.rs +++ b/crates/pop-cli/src/commands/call/contract.rs @@ -19,8 +19,8 @@ use clap::Args; use cliclack::spinner; use pop_common::{DefaultConfig, Keypair, parse_h160_account}; use pop_contracts::{ - CallExec, CallOpts, ContractCallable, ContractFunction, ContractStorage, DefaultEnvironment, - Verbosity, Weight, build_smart_contract, call_smart_contract, + BuildMode, CallExec, CallOpts, ContractCallable, ContractFunction, ContractStorage, + DefaultEnvironment, Verbosity, Weight, build_smart_contract, call_smart_contract, call_smart_contract_from_signed_payload, dry_run_gas_estimate_call, fetch_contract_storage, get_call_payload, get_contract_storage_info, get_messages, set_up_call, }; @@ -202,7 +202,13 @@ impl CallContractCommand { cli.warning("NOTE: contract has not yet been built.")?; let spinner = spinner(); spinner.start("Building contract in RELEASE mode..."); - let result = match build_smart_contract(&project_path, true, Verbosity::Quiet, None) { + let result = match build_smart_contract( + &project_path, + BuildMode::Release, + Verbosity::Quiet, + None, + None, + ) { Ok(result) => result, Err(e) => { return Err(anyhow!(format!( diff --git a/crates/pop-cli/src/commands/up/contract.rs b/crates/pop-cli/src/commands/up/contract.rs index cb2a5ec96..dafd2ff41 100644 --- a/crates/pop-cli/src/commands/up/contract.rs +++ b/crates/pop-cli/src/commands/up/contract.rs @@ -21,7 +21,7 @@ use clap::Args; use cliclack::spinner; use console::Emoji; use pop_contracts::{ - FunctionType, UpOpts, Verbosity, Weight, build_smart_contract, + BuildMode, FunctionType, UpOpts, Verbosity, Weight, build_smart_contract, dry_run_gas_estimate_instantiate, dry_run_upload, extract_function, get_contract_code, get_instantiate_payload, get_upload_payload, instantiate_contract_signed, instantiate_smart_contract, is_chain_alive, run_eth_rpc_node, run_ink_node, set_up_deployment, @@ -160,7 +160,13 @@ impl UpContractCommand { } let spinner = spinner(); spinner.start("Building contract in RELEASE mode..."); - let result = match build_smart_contract(&self.path, true, Verbosity::Quiet, None) { + let result = match build_smart_contract( + &self.path, + BuildMode::Release, + Verbosity::Quiet, + None, + None, + ) { Ok(result) => result, Err(e) => { Cli.outro_cancel(format!("🚫 An error occurred building your contract: {e}\nUse `pop build` to retry with build output."))?; diff --git a/crates/pop-cli/tests/contract.rs b/crates/pop-cli/tests/contract.rs index a749f7416..fc2fb4a33 100644 --- a/crates/pop-cli/tests/contract.rs +++ b/crates/pop-cli/tests/contract.rs @@ -12,6 +12,7 @@ use pop_contracts::{ set_up_call, set_up_deployment, }; use serde::{Deserialize, Serialize}; +use serde_json::Value; use std::{path::Path, time::Duration}; use strum::VariantArray; use subxt::{ @@ -272,6 +273,41 @@ async fn contract_lifecycle() -> Result<()> { Ok(()) } +#[tokio::test] +async fn verifiable_contract_lifecycle() -> Result<()> { + // TODO: Incomplete test, we'll be adding more steps as the development of the feature + // progresses + let temp = tempfile::tempdir()?; + let temp_dir = temp.path(); + //let temp_dir = Path::new("./"); //For testing locally + // pop new contract test_contract (default) + let mut command = pop(temp_dir, ["new", "contract", "test_contract", "--template", "standard"]); + assert!(command.spawn()?.wait().await?.success()); + let contract_dir = temp_dir.join("test_contract"); + assert!(contract_dir.exists()); + + // pop build --verifiable + command = + pop(&temp_dir, ["build", "--manifest-path", "./test_contract/Cargo.toml", "--verifiable"]); + assert!(command.spawn()?.wait().await?.success()); + + let ink_target_path = contract_dir.join("target").join("ink"); + assert!(ink_target_path.join("test_contract.contract").exists()); + assert!(ink_target_path.join("test_contract.polkavm").exists()); + let metadata_path = ink_target_path.join("test_contract.json"); + assert!(metadata_path.exists()); + let metadata_contents: Value = serde_json::from_str(&std::fs::read_to_string(&metadata_path)?)?; + // Verifiable builds include the used image (useink/contracts-verifiable:{tag} if not custom + // specified) in the metadata so that they can be exactly reproduced + let image_key = metadata_contents.get("image"); + match image_key { + Some(Value::String(value)) if value.starts_with("useink/contracts-verifiable") => (), + _ => return Err(anyhow::anyhow!("Verifiable build doesn't include the expected image")), + } + + Ok(()) +} + async fn generate_all_the_templates(temp_dir: &Path) -> Result<()> { for template in Contract::VARIANTS { let contract_name = format!("test_contract_{}", template).replace("-", "_"); diff --git a/crates/pop-contracts/README.md b/crates/pop-contracts/README.md index e9ad86841..2d1cd0632 100644 --- a/crates/pop-contracts/README.md +++ b/crates/pop-contracts/README.md @@ -20,11 +20,11 @@ Build an existing Smart Contract: ```rust,no_run use pop_contracts::build_smart_contract; use std::path::Path; -pub use contract_build::{Verbosity, MetadataSpec}; +pub use contract_build::{Verbosity, MetadataSpec, BuildMode}; let contract_path = Path::new("./"); -let build_release = true; // `true` for release mode, `false` for debug mode. -let result = build_smart_contract(&contract_path, build_release, Verbosity::Default, Some(MetadataSpec::Ink)); // You can build your contract with Solidity metadata using `Some(Metadata::Ink)` +let build_mode = BuildMode::Release; // `Release` for release mode, `Debug` for debug mode, `Verifiable` for verifiable contract. +let result = build_smart_contract(&contract_path, build_mode, Verbosity::Default, Some(MetadataSpec::Ink), None); // You can build your contract with Solidity metadata using `Some(MetadataSpec::Solidity)` ``` diff --git a/crates/pop-contracts/src/build.rs b/crates/pop-contracts/src/build.rs index f59a62808..1bc8e9fc8 100644 --- a/crates/pop-contracts/src/build.rs +++ b/crates/pop-contracts/src/build.rs @@ -1,8 +1,8 @@ // SPDX-License-Identifier: GPL-3.0 use crate::{errors::Error, utils::get_manifest_path}; -use contract_build::{BuildMode, BuildResult, ExecuteArgs, execute}; -pub use contract_build::{MetadataSpec, Verbosity}; +pub use contract_build::{BuildMode, ImageVariant, MetadataSpec, Verbosity}; +use contract_build::{BuildResult, ExecuteArgs, execute}; use std::path::Path; /// Build the smart contract located at the specified `path` in `build_release` mode. @@ -15,21 +15,26 @@ use std::path::Path; /// * `metadata_spec` - Optionally specify the contract metadata format/version. pub fn build_smart_contract( path: &Path, - release: bool, + build_mode: BuildMode, verbosity: Verbosity, metadata_spec: Option, + image: Option, ) -> anyhow::Result { let manifest_path = get_manifest_path(path)?; - let build_mode = match release { - true => BuildMode::Release, - false => BuildMode::Debug, - }; - - let args = + let mut args = ExecuteArgs { manifest_path, build_mode, verbosity, metadata_spec, ..Default::default() }; + + if let Some(image) = image { + args.image = image; + } + // Execute the build and log the output of the build - execute(args) + match build_mode { + // For verifiable contracts, execute calls docker_build (https://github.com/use-ink/cargo-contract/blob/master/crates/build/src/lib.rs#L595) which launches a blocking tokio runtime to handle the async operations (https://github.com/use-ink/cargo-contract/blob/master/crates/build/src/docker.rs#L135). The issue is that pop is itself a tokio runtime, launching another blocking one isn't allowed by tokio. So for verifiable contracts we need to first block the main pop tokio runtime before calling execute + BuildMode::Verifiable => tokio::task::block_in_place(|| execute(args)), + _ => execute(args), + } } /// Determines whether the manifest at the supplied path is a supported smart contract project. diff --git a/crates/pop-contracts/src/lib.rs b/crates/pop-contracts/src/lib.rs index 9b145276a..54f1ee226 100644 --- a/crates/pop-contracts/src/lib.rs +++ b/crates/pop-contracts/src/lib.rs @@ -13,7 +13,7 @@ mod testing; mod up; mod utils; -pub use build::{Verbosity, build_smart_contract, is_supported}; +pub use build::{BuildMode, ImageVariant, Verbosity, build_smart_contract, is_supported}; pub use call::{ CallOpts, call_smart_contract, call_smart_contract_from_signed_payload, dry_run_call, dry_run_gas_estimate_call, get_call_payload, set_up_call, diff --git a/crates/pop-contracts/src/utils/mod.rs b/crates/pop-contracts/src/utils/mod.rs index cd4ba6c81..79021255b 100644 --- a/crates/pop-contracts/src/utils/mod.rs +++ b/crates/pop-contracts/src/utils/mod.rs @@ -15,11 +15,14 @@ pub mod metadata; /// Retrieves the manifest path for a contract project. /// /// # Arguments -/// * `path` - A path to the project directory. +/// * `path` - A path to the project directory or directly to a Cargo.toml file. pub fn get_manifest_path(path: &Path) -> Result { - let full_path = PathBuf::from(path.to_string_lossy().to_string()) - .join("Cargo.toml") - .canonicalize()?; + let full_path = if path.ends_with("Cargo.toml") { + path.to_path_buf() + } else { + PathBuf::from(path.to_string_lossy().to_string()).join("Cargo.toml") + } + .canonicalize()?; ManifestPath::try_from(Some(full_path)) .map_err(|e| Error::ManifestPath(format!("Failed to get manifest path: {e}"))) } @@ -74,9 +77,27 @@ mod tests { } #[test] - fn test_get_manifest_path() -> Result<(), Error> { + fn get_manifest_path_from_directory() -> Result<(), Error> { + let temp_dir = setup_test_environment()?; + let contract_path = temp_dir.path().join("test_contract"); + let manifest_path = get_manifest_path(&contract_path)?; + assert_eq!( + manifest_path.as_ref().canonicalize().unwrap(), + contract_path.join("Cargo.toml").canonicalize().unwrap() + ); + Ok(()) + } + + #[test] + fn get_manifest_path_from_cargo_toml() -> Result<(), Error> { let temp_dir = setup_test_environment()?; - get_manifest_path(&temp_dir.path().join("test_contract"))?; + let contract_path = temp_dir.path().join("test_contract"); + let manifest_path = + get_manifest_path(&temp_dir.path().join("test_contract").join("Cargo.toml"))?; + assert_eq!( + manifest_path.as_ref().canonicalize().unwrap(), + contract_path.join("Cargo.toml").canonicalize().unwrap() + ); Ok(()) }