diff --git a/CHANGELOG.md b/CHANGELOG.md index a1abc85d29..8f0982dfec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,6 +35,8 @@ and this project adheres to ([#2120]) - cosmwasm-derive: Add `state_version` attribute for `migrate` entrypoints ([#2124]) +- cosmwasm-vm: Read the state version from Wasm modules and return them as part + of `AnalyzeReport` ([#2129]) [#1983]: https://github.com/CosmWasm/cosmwasm/pull/1983 [#2057]: https://github.com/CosmWasm/cosmwasm/pull/2057 @@ -47,6 +49,7 @@ and this project adheres to [#2107]: https://github.com/CosmWasm/cosmwasm/pull/2107 [#2120]: https://github.com/CosmWasm/cosmwasm/pull/2120 [#2124]: https://github.com/CosmWasm/cosmwasm/pull/2124 +[#2129]: https://github.com/CosmWasm/cosmwasm/pull/2129 ### Changed diff --git a/Cargo.lock b/Cargo.lock index e1a4b08833..6dc054f946 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -544,6 +544,7 @@ dependencies = [ "thiserror", "time", "tracing", + "wasm-encoder 0.205.0", "wasmer", "wasmer-middlewares", "wat", @@ -2491,6 +2492,15 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.205.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90e95b3563d164f33c1cfb0a7efbd5940c37710019be10cd09f800fdec8b0e5c" +dependencies = [ + "leb128", +] + [[package]] name = "wasmer" version = "4.2.6" @@ -2672,7 +2682,7 @@ dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.27.0", ] [[package]] diff --git a/packages/vm/Cargo.toml b/packages/vm/Cargo.toml index f140107243..50860ed16e 100644 --- a/packages/vm/Cargo.toml +++ b/packages/vm/Cargo.toml @@ -79,6 +79,7 @@ hex-literal = "0.4.1" rand = "0.8" tempfile = "3.1.0" wat = "1.0" +wasm-encoder = "0.205.0" clap = "4" leb128 = "0.2" target-lexicon = "0.12" diff --git a/packages/vm/src/cache.rs b/packages/vm/src/cache.rs index 2810e83683..7a6e41a838 100644 --- a/packages/vm/src/cache.rs +++ b/packages/vm/src/cache.rs @@ -124,6 +124,7 @@ pub struct Cache { } #[derive(PartialEq, Eq, Debug)] +#[non_exhaustive] pub struct AnalysisReport { /// `true` if and only if all [`REQUIRED_IBC_EXPORTS`] exist as exported functions. /// This does not guarantee they are functional or even have the correct signatures. @@ -132,6 +133,8 @@ pub struct AnalysisReport { pub entrypoints: BTreeSet, /// The set of capabilities the contract requires. pub required_capabilities: BTreeSet, + /// The contract state version exported set by the contract developer + pub contract_state_version: Option, } impl Cache @@ -320,6 +323,7 @@ where required_capabilities: required_capabilities_from_module(&module) .into_iter() .collect(), + contract_state_version: module.contract_state_version, }) } @@ -582,8 +586,10 @@ mod tests { use crate::capabilities::capabilities_from_csv; use crate::testing::{mock_backend, mock_env, mock_info, MockApi, MockQuerier, MockStorage}; use cosmwasm_std::{coins, Empty}; + use std::borrow::Cow; use std::fs::{create_dir_all, remove_dir_all}; use tempfile::TempDir; + use wasm_encoder::ComponentSection; const TESTING_GAS_LIMIT: u64 = 500_000_000; // ~0.5ms const TESTING_MEMORY_LIMIT: Size = Size::mebi(16); @@ -1410,6 +1416,7 @@ mod tests { E::Query ]), required_capabilities: BTreeSet::new(), + contract_state_version: None, } ); @@ -1427,6 +1434,7 @@ mod tests { "iterator".to_string(), "stargate".to_string() ]), + contract_state_version: None, } ); @@ -1438,6 +1446,26 @@ mod tests { has_ibc_entry_points: false, entrypoints: BTreeSet::new(), required_capabilities: BTreeSet::from(["iterator".to_string()]), + contract_state_version: None, + } + ); + + let mut wasm_with_version = EMPTY_CONTRACT.to_vec(); + let custom_section = wasm_encoder::CustomSection { + name: Cow::Borrowed("cw_state_version"), + data: Cow::Borrowed(b"21"), + }; + custom_section.append_to_component(&mut wasm_with_version); + + let checksum4 = cache.save_wasm(&wasm_with_version).unwrap(); + let report4 = cache.analyze(&checksum4).unwrap(); + assert_eq!( + report4, + AnalysisReport { + has_ibc_entry_points: false, + entrypoints: BTreeSet::new(), + required_capabilities: BTreeSet::from(["iterator".to_string()]), + contract_state_version: Some(21), } ); } diff --git a/packages/vm/src/parsed_wasm.rs b/packages/vm/src/parsed_wasm.rs index 757852bd7d..3d428ba7a0 100644 --- a/packages/vm/src/parsed_wasm.rs +++ b/packages/vm/src/parsed_wasm.rs @@ -1,4 +1,4 @@ -use std::{fmt, mem}; +use std::{fmt, mem, str}; use wasmer::wasmparser::{ BinaryReaderError, CompositeType, Export, FuncToValidate, FunctionBody, Import, MemoryType, @@ -66,6 +66,8 @@ pub struct ParsedWasm<'a> { pub total_func_params: usize, /// Collections of functions that are potentially pending validation pub func_validator: FunctionValidator<'a>, + /// Contract state version as defined in a custom section + pub contract_state_version: Option, } impl<'a> ParsedWasm<'a> { @@ -108,6 +110,7 @@ impl<'a> ParsedWasm<'a> { max_func_results: 0, total_func_params: 0, func_validator: FunctionValidator::Pending(OpaqueDebug::default()), + contract_state_version: None, }; for p in Parser::new(0).parse_all(wasm) { @@ -179,6 +182,17 @@ impl<'a> ParsedWasm<'a> { Payload::ExportSection(e) => { this.exports = e.into_iter().collect::, _>>()?; } + Payload::CustomSection(reader) if reader.name() == "cw_state_version" => { + // This is supposed to be valid UTF-8 + let raw_version = str::from_utf8(reader.data()) + .map_err(|err| VmError::static_validation_err(err.to_string()))?; + + this.contract_state_version = Some( + raw_version + .parse::() + .map_err(|err| VmError::static_validation_err(err.to_string()))?, + ); + } _ => {} // ignore everything else } } @@ -214,3 +228,24 @@ impl<'a> ParsedWasm<'a> { } } } + +#[cfg(test)] +mod test { + use super::ParsedWasm; + + #[test] + fn read_state_version() { + let wasm_data = + wat::parse_str(r#"( module ( @custom "cw_state_version" "42" ) )"#).unwrap(); + let parsed = ParsedWasm::parse(&wasm_data).unwrap(); + + assert_eq!(parsed.contract_state_version, Some(42)); + } + + #[test] + fn read_state_version_fails() { + let wasm_data = + wat::parse_str(r#"( module ( @custom "cw_state_version" "not a number" ) )"#).unwrap(); + assert!(ParsedWasm::parse(&wasm_data).is_err()); + } +}