diff --git a/Cargo.lock b/Cargo.lock index bf7fe379e0..f772cf82ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -838,12 +838,6 @@ version = "3.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" -[[package]] -name = "byteorder" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fc10e8cc6b2580fda3f36eb6dc5316657f812a3df879a44a66fc9f0fdbc4855" - [[package]] name = "byteorder" version = "1.5.0" @@ -2166,7 +2160,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d60b12902ba28e2730cd37e95b8c9223af2808df9e902d4df49588d1470606" dependencies = [ - "byteorder 1.5.0", + "byteorder", ] [[package]] @@ -2989,7 +2983,7 @@ version = "3.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eebcc3aff044e5944a8fbaf69eb277d11986064cba30c468730e8b9909fb551c" dependencies = [ - "byteorder 1.5.0", + "byteorder", "dbus-secret-service", "log", "secret-service", @@ -3046,10 +3040,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" [[package]] -name = "leb128" -version = "0.2.5" +name = "leb128fmt" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "ledger-apdu" @@ -3078,7 +3072,7 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45ba81a1f5f24396b37211478aff7fbcd605dd4544df8dbed07b9da3c2057aee" dependencies = [ - "byteorder 1.5.0", + "byteorder", "cfg-if", "hex", "hidapi", @@ -5046,9 +5040,9 @@ dependencies = [ "ulid", "url", "walkdir", - "wasm-gen", + "wasm-encoder", "wasm-opt", - "wasmparser", + "wasmparser 0.116.1", "which", "whoami", "zeroize", @@ -5070,7 +5064,7 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-xdr", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -5117,7 +5111,7 @@ dependencies = [ "soroban-wasmi", "static_assertions", "stellar-strkey 0.0.13", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -5200,13 +5194,12 @@ dependencies = [ [[package]] name = "soroban-spec" version = "25.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c79501d0636f86fe2c9b1dd7e88b9397415b3493a59b34f466abd7758c84b92b" dependencies = [ "base64 0.22.1", + "sha2 0.10.9", "stellar-xdr", "thiserror 1.0.69", - "wasmparser", + "wasmparser 0.116.1", ] [[package]] @@ -5253,7 +5246,8 @@ dependencies = [ "stellar-xdr", "thiserror 1.0.69", "tokio", - "wasmparser", + "wasm-encoder", + "wasmparser 0.116.1", "which", ] @@ -5404,7 +5398,7 @@ version = "25.1.0" dependencies = [ "async-trait", "bollard", - "byteorder 1.5.0", + "byteorder", "ed25519-dalek", "env_logger", "hex", @@ -6593,13 +6587,13 @@ dependencies = [ ] [[package]] -name = "wasm-gen" -version = "0.1.4" +name = "wasm-encoder" +version = "0.235.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b854b1461005a7b3365742310f7faa3cac3add809d66928c64a40c7e9e842ebb" +checksum = "b3bc393c395cb621367ff02d854179882b9a351b4e0c93d1397e6090b53a5c2a" dependencies = [ - "byteorder 0.5.3", - "leb128", + "leb128fmt", + "wasmparser 0.235.0", ] [[package]] @@ -6683,6 +6677,17 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.235.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "161296c618fa2d63f6ed5fffd1112937e803cb9ec71b32b01a76321555660917" +dependencies = [ + "bitflags", + "indexmap 2.11.0", + "semver", +] + [[package]] name = "wasmparser-nostd" version = "0.100.2" diff --git a/Cargo.toml b/Cargo.toml index d1016a0f4a..b7d641b91d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -138,3 +138,5 @@ lto = true inherits = "release" panic = "unwind" +[patch.crates-io.soroban-spec] +path = "../rs-soroban-sdk-spec-markers/soroban-spec" diff --git a/cmd/crates/soroban-spec-tools/Cargo.toml b/cmd/crates/soroban-spec-tools/Cargo.toml index a17a5d31f1..d2b719eaec 100644 --- a/cmd/crates/soroban-spec-tools/Cargo.toml +++ b/cmd/crates/soroban-spec-tools/Cargo.toml @@ -27,6 +27,7 @@ hex = { workspace = true } wasmparser = { workspace = true } base64 = { workspace = true } thiserror = "1.0.31" +wasm-encoder = "0.235.0" [dev-dependencies] diff --git a/cmd/crates/soroban-spec-tools/src/contract.rs b/cmd/crates/soroban-spec-tools/src/contract.rs index 0f6bef7a04..deb61468bf 100644 --- a/cmd/crates/soroban-spec-tools/src/contract.rs +++ b/cmd/crates/soroban-spec-tools/src/contract.rs @@ -119,6 +119,93 @@ impl Spec { ScSpecEntry::read_xdr_iter(&mut read).collect::, xdr::Error>>()?, )) } + + /// Returns the filtered spec entries serialized as XDR bytes, filtering + /// based on markers in the WASM data section. + /// + /// The SDK embeds markers in the data section for each type/event that is + /// actually used in the contract. These markers survive dead code elimination, + /// so we can filter out any spec entries that don't have corresponding markers. + /// + /// Functions are always kept as they define the contract's API. + /// + /// # Arguments + /// + /// * `wasm_bytes` - The WASM binary to extract markers from + /// + /// # Returns + /// + /// XDR bytes of the filtered spec entries. + pub fn filtered_spec_xdr_with_markers(&self, wasm_bytes: &[u8]) -> Result, Error> { + use soroban_spec::marker; + + // Extract markers from the WASM data section + let markers = marker::find_all(wasm_bytes); + + // Filter all entries (types, events) based on markers + let filtered = marker::filter(self.spec.clone(), &markers); + + let mut buffer = Vec::new(); + let mut writer = Limited::new(Cursor::new(&mut buffer), Limits::none()); + for entry in filtered { + entry.write_xdr(&mut writer)?; + } + Ok(buffer) + } +} + +/// Replaces a custom section in WASM bytes with new content. +/// +/// This function parses the WASM to find the target custom section, then rebuilds +/// the WASM by copying all other sections verbatim and appending the new custom +/// section at the end. +/// +/// # Arguments +/// +/// * `wasm_bytes` - The original WASM binary +/// * `section_name` - The name of the custom section to replace +/// * `new_content` - The new content for the custom section +/// +/// # Returns +/// +/// A new WASM binary with the custom section replaced. +pub fn replace_custom_section( + wasm_bytes: &[u8], + section_name: &str, + new_content: &[u8], +) -> Result, Error> { + use wasm_encoder::{CustomSection, Module, RawSection}; + use wasmparser::Payload; + + let mut module = Module::new(); + + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(wasm_bytes) { + let payload = payload?; + + // Skip the target custom section - we'll append the new one at the end + let is_target_section = + matches!(&payload, Payload::CustomSection(section) if section.name() == section_name); + if !is_target_section { + // For all other payloads that represent sections, copy them verbatim + if let Some((id, range)) = payload.as_section() { + let raw = RawSection { + id, + data: &wasm_bytes[range], + }; + module.section(&raw); + } + } + } + + // Append the new custom section + let custom = CustomSection { + name: section_name.into(), + data: new_content.into(), + }; + module.section(&custom); + + Ok(module.finish()) } impl Display for Spec { diff --git a/cmd/crates/soroban-test/tests/it/build.rs b/cmd/crates/soroban-test/tests/it/build.rs index f9046615df..73f4a6ac03 100644 --- a/cmd/crates/soroban-test/tests/it/build.rs +++ b/cmd/crates/soroban-test/tests/it/build.rs @@ -321,6 +321,9 @@ fn parent_path() -> String { } fn with_flags(expected: &str) -> String { + const CFG_FLAG: &str = + "-- --cfg=soroban_sdk_build_system_supports_optimising_specs_using_data_markers"; + let cargo_home = home::cargo_home().unwrap(); let registry_prefix = cargo_home.join("registry").join("src"); let registry_prefix = registry_prefix.display().to_string(); @@ -328,14 +331,17 @@ fn with_flags(expected: &str) -> String { let registry_prefix = registry_prefix.replace('\\', "/"); let vec: Vec<_> = if env::var("RUSTFLAGS").is_ok() { - expected.split('\n').map(ToString::to_string).collect() + expected + .split('\n') + .map(|x| format!("{x} {CFG_FLAG}")) + .collect() } else { expected .split('\n') .map(|x| { let rustflags_value = format!("--remap-path-prefix={registry_prefix}="); let escaped_value = escape(std::borrow::Cow::Borrowed(&rustflags_value)); - format!("CARGO_BUILD_RUSTFLAGS={escaped_value} {x}") + format!("CARGO_BUILD_RUSTFLAGS={escaped_value} {x} {CFG_FLAG}") }) .collect() }; diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index c3868c91c6..d08bfe612b 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -122,7 +122,7 @@ glob = "0.3.1" fqdn = "0.3.12" open = "5.3.0" url = "2.5.2" -wasm-gen = "0.1.4" +wasm-encoder = "0.235.0" zeroize = "1.8.1" keyring = { version = "3", features = ["apple-native", "windows-native", "sync-secret-service", "crypto-rust"], optional = true } whoami = "1.5.2" diff --git a/cmd/soroban-cli/src/commands/contract/build.rs b/cmd/soroban-cli/src/commands/contract/build.rs index 56f2fd730e..f8fcced513 100644 --- a/cmd/soroban-cli/src/commands/contract/build.rs +++ b/cmd/soroban-cli/src/commands/contract/build.rs @@ -165,6 +165,12 @@ pub enum Error { #[error(transparent)] Wasm(#[from] wasm::Error), + + #[error(transparent)] + SpecTools(#[from] soroban_spec_tools::contract::Error), + + #[error(transparent)] + WasmParsing(#[from] wasmparser::BinaryReaderError), } const WASM_TARGET: &str = "wasm32v1-none"; @@ -231,6 +237,11 @@ impl Cmd { cmd.env("CARGO_BUILD_RUSTFLAGS", rustflags); } + // Pass cfg flag to rustc to inform the SDK that this CLI supports + // spec optimization using markers. + cmd.arg("--"); + cmd.arg("--cfg=soroban_sdk_build_system_supports_optimising_specs_using_data_markers"); + let mut cmd_str_parts = Vec::::new(); cmd_str_parts.extend(cmd.get_envs().map(|(key, val)| { format!( @@ -264,6 +275,7 @@ impl Cmd { .join(&file); self.inject_meta(&target_file_path)?; + Self::filter_spec(&target_file_path)?; let final_path = if let Some(out_dir) = &self.out_dir { fs::create_dir_all(out_dir).map_err(Error::CreatingOutDir)?; @@ -369,14 +381,82 @@ impl Cmd { } fn inject_meta(&self, target_file_path: &PathBuf) -> Result<(), Error> { - let mut wasm_bytes = fs::read(target_file_path).map_err(Error::ReadingWasmFile)?; - let xdr = self.encoded_new_meta()?; - wasm_gen::write_custom_section(&mut wasm_bytes, META_CUSTOM_SECTION_NAME, &xdr); + use wasm_encoder::{CustomSection, Module, RawSection}; + use wasmparser::Payload; + + let wasm_bytes = fs::read(target_file_path).map_err(Error::ReadingWasmFile)?; + + let mut module = Module::new(); + let mut existing_meta: Vec = Vec::new(); + + let parser = wasmparser::Parser::new(0); + for payload in parser.parse_all(&wasm_bytes) { + let payload = payload?; + + match &payload { + // Collect existing meta to merge with new meta + Payload::CustomSection(section) if section.name() == META_CUSTOM_SECTION_NAME => { + existing_meta.extend_from_slice(section.data()); + } + // Copy all other sections verbatim + _ => { + if let Some((id, range)) = payload.as_section() { + let raw = RawSection { + id, + data: &wasm_bytes[range], + }; + module.section(&raw); + } + } + } + } + + // Append new meta to existing meta + let new_meta = self.encoded_new_meta()?; + existing_meta.extend(new_meta); + + let meta_section = CustomSection { + name: META_CUSTOM_SECTION_NAME.into(), + data: existing_meta.into(), + }; + module.section(&meta_section); + + let updated_wasm = module.finish(); // Deleting .wasm file effectively unlinking it from /release/deps/.wasm preventing from overwrite // See https://github.com/stellar/stellar-cli/issues/1694#issuecomment-2709342205 fs::remove_file(target_file_path).map_err(Error::DeletingArtifact)?; - fs::write(target_file_path, wasm_bytes).map_err(Error::WritingWasmFile) + fs::write(target_file_path, updated_wasm).map_err(Error::WritingWasmFile) + } + + /// Filters unused types and events from the contract spec. + /// + /// This removes: + /// - Type definitions that are not referenced by any function + /// - Events that don't have corresponding markers in the WASM data section + /// (events that are defined but never published) + /// + /// The SDK embeds markers in the data section for types/events that are + /// actually used. These markers survive dead code elimination, so we can + /// detect which spec entries are truly needed. + fn filter_spec(target_file_path: &PathBuf) -> Result<(), Error> { + use soroban_spec_tools::contract::{replace_custom_section, Spec}; + + let wasm_bytes = fs::read(target_file_path).map_err(Error::ReadingWasmFile)?; + + // Parse the spec from the wasm + let spec = Spec::new(&wasm_bytes)?; + + // Get the filtered spec as XDR bytes, filtering both types and events + // based on markers in the WASM data section + let filtered_xdr = spec.filtered_spec_xdr_with_markers(&wasm_bytes)?; + + // Replace the contractspecv0 section with the filtered version + let new_wasm = replace_custom_section(&wasm_bytes, "contractspecv0", &filtered_xdr)?; + + // Write the modified wasm back + fs::remove_file(target_file_path).map_err(Error::DeletingArtifact)?; + fs::write(target_file_path, new_wasm).map_err(Error::WritingWasmFile) } fn encoded_new_meta(&self) -> Result, Error> {