diff --git a/CHANGELOG.md b/CHANGELOG.md index bc4896c60..0df25de31 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added +- Add schema generation and verification - [#1404](https://github.com/paritytech/cargo-contract/pull/1404) - Compare `Environment` types against the node - [#1377](https://github.com/paritytech/cargo-contract/pull/1377) - Adds workflow for publishing docker images for the verifiable builds - [#1267](https://github.com/paritytech/cargo-contract/pull/1267) - Detect `INK_STATIC_BUFFER_SIZE` env var - [#1310](https://github.com/paritytech/cargo-contract/pull/1310) diff --git a/Cargo.lock b/Cargo.lock index 8018ed2e9..11a5778c1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,6 +91,7 @@ dependencies = [ "cfg-if", "getrandom 0.2.10", "once_cell", + "serde", "version_check", ] @@ -434,6 +435,21 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + [[package]] name = "bitcoin_hashes" version = "0.11.0" @@ -699,6 +715,12 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "bytecount" +version = "0.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1e5f035d16fc623ae5f74981db80a439803888314e3a555fd6f04acd51a3205" + [[package]] name = "byteorder" version = "1.4.3" @@ -735,10 +757,13 @@ dependencies = [ "contract-transcode", "current_platform", "hex", + "ink_metadata", + "jsonschema", "pallet-contracts-primitives", "predicates", "primitive-types", "regex", + "schemars", "semver", "serde", "serde_json", @@ -1590,6 +1615,15 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" +[[package]] +name = "encoding_rs" +version = "0.8.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +dependencies = [ + "cfg-if", +] + [[package]] name = "environmental" version = "1.1.4" @@ -1650,6 +1684,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" +[[package]] +name = "fancy-regex" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b95f7c0680e4142284cf8b22c14a476e87d61b004a3a0861872b32ef7ead40a2" +dependencies = [ + "bit-set", + "regex", +] + [[package]] name = "fastrand" version = "1.9.0" @@ -1707,6 +1751,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fraction" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3027ae1df8d41b4bed2241c8fdad4acc1e7af60c8e17743534b545e77182d678" +dependencies = [ + "lazy_static", + "num", +] + [[package]] name = "frame-metadata" version = "15.1.0" @@ -1884,8 +1938,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be4136b2a15dd319360be1c07d9933517ccf0be8f16bf62a3bee4f0d618df427" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -2467,6 +2523,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ipnet" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" + [[package]] name = "is-terminal" version = "0.4.9" @@ -2478,6 +2540,15 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "iso8601" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "924e5d73ea28f59011fec52a0d12185d496a9b075d360657aed2a5707f701153" +dependencies = [ + "nom", +] + [[package]] name = "itertools" version = "0.10.5" @@ -2623,6 +2694,36 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonschema" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a071f4f7efc9a9118dfb627a0a94ef247986e1ab8606a4c806ae2b3aa3b6978" +dependencies = [ + "ahash 0.8.3", + "anyhow", + "base64 0.21.3", + "bytecount", + "clap", + "fancy-regex", + "fraction", + "getrandom 0.2.10", + "iso8601", + "itoa", + "memchr", + "num-cmp", + "once_cell", + "parking_lot", + "percent-encoding", + "regex", + "reqwest", + "serde", + "serde_json", + "time", + "url", + "uuid", +] + [[package]] name = "keccak" version = "0.1.4" @@ -2851,6 +2952,12 @@ dependencies = [ "zeroize", ] +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -2935,6 +3042,20 @@ dependencies = [ "winapi", ] +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + [[package]] name = "num-bigint" version = "0.4.4" @@ -2946,6 +3067,21 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-cmp" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa" + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + [[package]] name = "num-format" version = "0.4.4" @@ -2966,6 +3102,17 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + [[package]] name = "num-rational" version = "0.4.1" @@ -3574,6 +3721,41 @@ dependencies = [ "bytecheck", ] +[[package]] +name = "reqwest" +version = "0.11.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "046cd98826c46c2ac8ddecae268eb5c2e58628688a5fc7a2643704a73faba95b" +dependencies = [ + "base64 0.21.3", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "ipnet", + "js-sys", + "log", + "mime", + "once_cell", + "percent-encoding", + "pin-project-lite", + "serde", + "serde_json", + "serde_urlencoded", + "system-configuration", + "tokio", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + [[package]] name = "ring" version = "0.16.20" @@ -5129,6 +5311,27 @@ dependencies = [ "unicode-xid", ] +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "tap" version = "1.0.1" @@ -5799,6 +6002,18 @@ dependencies = [ "wasm-bindgen-shared", ] +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c02dbc21516f9f1f04f187958890d7e6026df8d16540b7ad9492bc34a67cea03" +dependencies = [ + "cfg-if", + "js-sys", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "wasm-bindgen-macro" version = "0.2.87" @@ -6255,6 +6470,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/crates/cargo-contract/Cargo.toml b/crates/cargo-contract/Cargo.toml index 4d70d9fb6..8f265315f 100644 --- a/crates/cargo-contract/Cargo.toml +++ b/crates/cargo-contract/Cargo.toml @@ -35,7 +35,9 @@ serde_json = "1.0.108" serde = { version = "1.0.192", default-features = false, features = ["derive"] } url = { version = "2.4.1", features = ["serde"] } semver = "1.0" - +jsonschema = "0.17" +schemars = "0.8" +ink_metadata = "5.0.0-alpha" # dependencies for extrinsics (deploying and calling a contract) diff --git a/crates/cargo-contract/src/cmd/mod.rs b/crates/cargo-contract/src/cmd/mod.rs index 4b7f2d738..87918cfb4 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 schema; pub mod upload; pub mod verify; @@ -37,6 +38,10 @@ pub(crate) use self::{ }, instantiate::InstantiateCommand, remove::RemoveCommand, + schema::{ + GenerateSchemaCommand, + VerifySchemaCommand, + }, upload::UploadCommand, verify::VerifyCommand, }; diff --git a/crates/cargo-contract/src/cmd/schema.rs b/crates/cargo-contract/src/cmd/schema.rs new file mode 100644 index 000000000..06597774e --- /dev/null +++ b/crates/cargo-contract/src/cmd/schema.rs @@ -0,0 +1,172 @@ +use std::{ + fs::File, + path::PathBuf, +}; + +use anyhow::{ + anyhow, + Context, + Result, +}; +use colored::Colorize; +use contract_build::{ + Verbosity, + VerbosityFlags, +}; +use jsonschema::JSONSchema; +use schemars::schema_for; + +#[derive(Debug, Clone, Default, clap::ValueEnum)] +#[clap(name = "metadata")] +enum Metadata { + /// Represents the outer schema format of the contract + #[clap(name = "outer")] + #[default] + Outer, + /// Represents the inner schema format of the contract. + /// Contains specification of the ink! contract. + #[clap(name = "inner")] + Inner, +} + +/// Checks if a contract in the given workspace matches that of a reference contract. +#[derive(Debug, clap::Args)] +pub struct GenerateSchemaCommand { + /// What type of metadata to generate. + #[clap(long, value_enum, default_value = "outer")] + metadata: Metadata, +} + +impl GenerateSchemaCommand { + pub fn run(&self) -> Result { + let schema = match self.metadata { + Metadata::Outer => schema_for!(ink_metadata::InkProject), + Metadata::Inner => schema_for!(ink_metadata::ConstructorSpec), + }; + let pretty_string = serde_json::to_string_pretty(&schema)?; + + Ok(pretty_string) + } +} + +/// Verifies the metadata of the given contract against the schema file. +#[derive(Debug, clap::Args)] +pub struct VerifySchemaCommand { + /// The path to metadata + #[clap(long, value_parser)] + schema: PathBuf, + /// The .contract path to verify the metadata + #[clap(name = "bundle", long, value_parser)] + contract_bundle: Option, + /// What type of metadata to verify. + #[clap(long, conflicts_with = "bundle", value_parser)] + metadata: Option, + /// Denotes if output should be printed to stdout. + #[clap(flatten)] + verbosity: VerbosityFlags, + /// Output the result in JSON format + #[clap(long, conflicts_with = "verbose")] + output_json: bool, +} + +impl VerifySchemaCommand { + pub fn run(&self) -> Result { + let verbosity: Verbosity = TryFrom::<&VerbosityFlags>::try_from(&self.verbosity)?; + + let mut metadata = serde_json::Value::Null; + let mut metadata_source = String::new(); + + // 1a. Extract given metadata from .contract bundle + if let Some(path) = &self.contract_bundle { + let file = File::open(path) + .context(format!("Failed to open contract bundle {}", path.display()))?; + + let mut contract_metadata: contract_metadata::ContractMetadata = + serde_json::from_reader(&file).context(format!( + "Failed to deserialize contract bundle {}", + path.display() + ))?; + contract_metadata.remove_source_wasm_attribute(); + + metadata = serde_json::value::to_value(contract_metadata)?; + metadata_source = path.display().to_string(); + } + + // 1b. Read metadata file + if let Some(path) = &self.metadata { + let file = File::open(path) + .context(format!("Failed to open metadata file {}", path.display()))?; + + let contract_metadata: contract_metadata::ContractMetadata = + serde_json::from_reader(&file).context(format!( + "Failed to deserialize metadata file {}", + path.display() + ))?; + + metadata = serde_json::value::to_value(contract_metadata)?; + metadata_source = path.display().to_string(); + } + + // 2. Open schema file + let path = &self.schema; + let file = File::open(path) + .context(format!("Failed to open schema file {}", path.display()))?; + + let schema: serde_json::Value = serde_json::from_reader(&file).context( + format!("Failed to deserialize schema file {}", path.display()), + )?; + + // 3. Build validator + + // We have to use let-else here, otherwise `&schema` is required to be static + let Ok(validator) = JSONSchema::compile(&schema) else { + anyhow::bail!("Failed to compile schema to validation tree") + }; + + // 3. Validate and display error if any + validator.validate(&metadata).map_err(|errors| { + let error_msg = errors.fold( + String::from("Error during schema validation:\n"), + |acc, e| format!("{}\n{}", acc, e), + ); + anyhow!(error_msg) + })?; + + Ok(SchemaVerificationResult { + is_verified: true, + metadata_source, + schema: self.schema.display().to_string(), + output_json: self.output_json, + verbosity, + }) + } +} + +/// The result of verification process +#[derive(serde::Serialize, serde::Deserialize)] +pub struct SchemaVerificationResult { + pub is_verified: bool, + pub metadata_source: String, + pub schema: String, + #[serde(skip_serializing, skip_deserializing)] + pub output_json: bool, + #[serde(skip_serializing, skip_deserializing)] + pub verbosity: Verbosity, +} + +impl SchemaVerificationResult { + /// Display the result in a fancy format + pub fn display(&self) -> String { + format!( + "\n{} {} against schema {}", + "Successfully verified metadata in".bright_green().bold(), + format!("`{}`", &self.metadata_source).bold(), + format!("`{}`!", &self.schema).bold() + ) + } + + /// Display the build results in a pretty formatted JSON string. + pub fn serialize_json(&self) -> Result { + Ok(serde_json::to_string_pretty(self)?) + } +} diff --git a/crates/cargo-contract/src/main.rs b/crates/cargo-contract/src/main.rs index 9d81f213b..1561c3b38 100644 --- a/crates/cargo-contract/src/main.rs +++ b/crates/cargo-contract/src/main.rs @@ -24,11 +24,13 @@ use self::cmd::{ CheckCommand, DecodeCommand, ErrorVariant, + GenerateSchemaCommand, InfoCommand, InstantiateCommand, RemoveCommand, UploadCommand, VerifyCommand, + VerifySchemaCommand, }; use anyhow::{ anyhow, @@ -142,6 +144,12 @@ enum Command { /// workspace. #[clap(name = "verify")] Verify(VerifyCommand), + /// Generates schema from the current metadata specification. + #[clap(name = "generate-schema")] + GenerateSchema(GenerateSchemaCommand), + /// Verify schema from the current metadata specification. + #[clap(name = "verify-schema")] + VerifySchema(VerifySchemaCommand), } fn main() { @@ -223,6 +231,21 @@ fn exec(cmd: Command) -> Result<()> { Command::Verify(verify) => { let result = verify.run().map_err(format_err)?; + if result.output_json { + println!("{}", result.serialize_json()?) + } else if result.verbosity.is_verbose() { + println!("{}", result.display()) + } + Ok(()) + } + Command::GenerateSchema(generate) => { + let result = generate.run().map_err(format_err)?; + println!("{}", result); + Ok(()) + } + Command::VerifySchema(verify) => { + let result = verify.run().map_err(format_err)?; + if result.output_json { println!("{}", result.serialize_json()?) } else if result.verbosity.is_verbose() {