From e2b70281c17630170093b2e4b910a1571a65f98e Mon Sep 17 00:00:00 2001 From: mootz12 Date: Mon, 4 May 2026 16:05:02 -0400 Subject: [PATCH] feat: use spec filtering logic from sdk --- Cargo.lock | 87 +++++++----- Cargo.toml | 20 ++- cmd/crates/soroban-test/Cargo.toml | 1 + .../contracts/shaking/src/lib.rs | 2 +- cmd/crates/soroban-test/tests/it/build.rs | 132 +++++++----------- cmd/soroban-cli/Cargo.toml | 2 + .../src/commands/contract/build.rs | 87 ++---------- .../src/commands/contract/info/interface.rs | 4 +- cmd/soroban-cli/src/commands/contract/mod.rs | 1 + .../src/commands/contract/spec_shaking.rs | 132 ++++++++++++++++++ 10 files changed, 279 insertions(+), 189 deletions(-) create mode 100644 cmd/soroban-cli/src/commands/contract/spec_shaking.rs diff --git a/Cargo.lock b/Cargo.lock index 7e5dc91354..ea9f606ca0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1978,7 +1978,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" dependencies = [ "libc", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -1999,9 +1999,9 @@ dependencies = [ [[package]] name = "ethnum" -version = "1.5.2" +version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca81e6b4777c89fd810c25a4be2b1bd93ea034fbe58e6a75216a34c6b82c539b" +checksum = "40404c3f5f511ec4da6fe866ddf6a717c309fdbb69fbbad7b0f3edab8f2e835f" [[package]] name = "event-listener" @@ -4325,7 +4325,7 @@ dependencies = [ "once_cell", "socket2 0.6.0", "tracing", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -4699,7 +4699,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys 0.9.4", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] @@ -5007,7 +5007,7 @@ checksum = "35a5a225b0cc092f0c818c7e51faf9d63d07da8c3157d6250096ca4b8708cfe3" dependencies = [ "ed25519-dalek", "ows-signer", - "stellar-strkey 0.0.16", + "stellar-strkey 0.0.15", "thiserror 2.0.18", "tiny-bip39", ] @@ -5357,9 +5357,9 @@ dependencies = [ [[package]] name = "soroban-builtin-sdk-macros" -version = "26.1.2" +version = "26.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df78c69d87834af6a53e47323a8fa02d68d92ab233476c860db643f8d1801cfe" +checksum = "35a3a2b57b132b800e132d2c81e1818359bb2cf787ca39c61c151d6bd0798403" dependencies = [ "itertools 0.13.0", "proc-macro2", @@ -5423,8 +5423,10 @@ dependencies = [ "shell-escape", "shlex", "soroban-ledger-snapshot", + "soroban-meta", "soroban-sdk", "soroban-spec", + "soroban-spec-markers", "soroban-spec-rust", "soroban-spec-tools", "soroban-spec-typescript", @@ -5460,9 +5462,9 @@ dependencies = [ [[package]] name = "soroban-env-common" -version = "26.1.2" +version = "26.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08582c2c21bd3f7b737bcb76db9d4ca473f8349d65f8952a50eeed8823f44aef" +checksum = "0c76fad735f9622d8aa0fa0c75838d2023a659a0c57638a783b8b2eb967f7822" dependencies = [ "arbitrary", "crate-git-revision", @@ -5479,9 +5481,9 @@ dependencies = [ [[package]] name = "soroban-env-guest" -version = "26.1.2" +version = "26.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aad436ff91576ce4c231b474aecd521ccf890df62042fc0234ffc2006b72fa1b" +checksum = "15aeed6d7a4dc4d3bba65e2ac92f7eeaa664900bbc82e1055e024bf637d74ed3" dependencies = [ "soroban-env-common", "static_assertions", @@ -5489,9 +5491,9 @@ dependencies = [ [[package]] name = "soroban-env-host" -version = "26.1.2" +version = "26.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2662cd060f6a3be269e23d0611bd2ea9eb6a0e7db77e11427e09de0efe547f8b" +checksum = "6fb523456b4efe9cdf869233cff5a2a1a5ebfae8c0acc405e57479c83781a20c" dependencies = [ "ark-bls12-381", "ark-bn254", @@ -5526,9 +5528,9 @@ dependencies = [ [[package]] name = "soroban-env-macros" -version = "26.1.2" +version = "26.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d5489ee232e1fa56c817ac57e2cba8b788685c48902928fecbae05cba97f065" +checksum = "2ada3449bb23c964a88a1bf633ac66ca3ebac2061693f53148b199ce816791d0" dependencies = [ "itertools 0.13.0", "proc-macro2", @@ -5546,8 +5548,7 @@ version = "26.0.0" [[package]] name = "soroban-ledger-snapshot" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8330f1ba47d85183b96f6a5f1f774d76100cd40baa79cada77147e11fb4c7b5" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "serde", "serde_json", @@ -5557,11 +5558,20 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "soroban-meta" +version = "26.0.0-rc.1" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" +dependencies = [ + "stellar-xdr", + "thiserror 1.0.69", + "wasmparser 0.116.1", +] + [[package]] name = "soroban-sdk" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bdc156a0183eb584e57d45f63f3bd7023165980131d6eecc939fe5cda2490c63" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "arbitrary", "bytes-lit", @@ -5577,6 +5587,7 @@ dependencies = [ "soroban-env-host", "soroban-ledger-snapshot", "soroban-sdk-macros", + "soroban-spec-markers", "stellar-strkey 0.0.16", "visibility", ] @@ -5584,8 +5595,7 @@ dependencies = [ [[package]] name = "soroban-sdk-macros" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "422af96b2e135390beb043480eb376b27943bd79e6857e5c950e307da057d0ad" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "darling", "heck 0.5.0", @@ -5596,6 +5606,7 @@ dependencies = [ "sha2 0.10.9", "soroban-env-common", "soroban-spec", + "soroban-spec-markers", "soroban-spec-rust", "stellar-xdr", "syn 2.0.106", @@ -5604,21 +5615,27 @@ dependencies = [ [[package]] name = "soroban-spec" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5077ba171ede4dd69428095a2760b01196291b9bd467a4f4ba27bdfec206ade5" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "base64 0.22.1", - "sha2 0.10.9", + "soroban-spec-markers", "stellar-xdr", "thiserror 1.0.69", "wasmparser 0.116.1", ] +[[package]] +name = "soroban-spec-markers" +version = "26.0.0-rc.1" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" +dependencies = [ + "sha2 0.10.9", +] + [[package]] name = "soroban-spec-rust" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ddc78f9da68c1d20fd25194e43113fdd0bf40ad51d891c20c90268379af4390" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "prettyplease", "proc-macro2", @@ -5695,6 +5712,7 @@ dependencies = [ "soroban-cli", "soroban-ledger-snapshot", "soroban-spec", + "soroban-spec-markers", "soroban-spec-tools", "stellar-ledger", "stellar-rpc-client", @@ -5716,8 +5734,7 @@ dependencies = [ [[package]] name = "soroban-token-sdk" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1898e882f2af93b2bda2c11543b9f189b8778110a66387df7e135f64f7f694e3" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "soroban-sdk", ] @@ -5725,8 +5742,7 @@ dependencies = [ [[package]] name = "soroban-token-spec" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41dfbce7705b66fc8adddf3170154a021f069584d9a541d9059ca748698c1cd5" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "soroban-sdk", "soroban-token-sdk", @@ -5776,8 +5792,7 @@ checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] name = "stellar-asset-spec" version = "26.0.0-rc.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca300556416dcb206b69a973612b8ae6adb6563942265d16c1f0b6ef69294343" +source = "git+https://github.com/stellar/rs-soroban-sdk?branch=spec-shaking-marker-fix-3#a88ddcb14d577a9983054460403281cca78d2553" dependencies = [ "soroban-sdk", "soroban-token-sdk", @@ -5891,9 +5906,9 @@ dependencies = [ [[package]] name = "stellar-xdr" -version = "26.0.0" +version = "26.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea3195594b044ea3a5b05906f81d945480825f00db4e3ae7d77526bf546ff3a" +checksum = "ea6e29c7e1f071c2767916460d006668197843d5d93f0ec8893a26f72a14f595" dependencies = [ "arbitrary", "base64 0.22.1", @@ -6101,7 +6116,7 @@ dependencies = [ "getrandom 0.3.3", "once_cell", "rustix 1.0.8", - "windows-sys 0.60.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e43666a0af..09a7ad6701 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,7 +44,7 @@ path = "cmd/crates/stellar-ledger" # Dependencies from the rs-stellar-xdr repo: [workspace.dependencies.stellar-xdr] -version = "26.0.0" +version = "26.0.1" # Dependencies from the rs-soroban-sdk repo: [workspace.dependencies.soroban-spec] @@ -53,9 +53,16 @@ version = "26.0.0-rc.1" [workspace.dependencies.soroban-spec-rust] version = "26.0.0-rc.1" +[workspace.dependencies.soroban-spec-markers] +git = "https://github.com/stellar/rs-soroban-sdk" +branch = "spec-shaking-marker-fix-3" + [workspace.dependencies.soroban-sdk] version = "26.0.0-rc.1" +[workspace.dependencies.soroban-meta] +version = "26.0.0-rc.1" + [workspace.dependencies.soroban-env-host] version = "26.0.0-rc.1" @@ -137,3 +144,14 @@ lto = true [profile.release-with-panic-unwind] inherits = "release" panic = "unwind" + +[patch.crates-io] +soroban-sdk = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-sdk-macros = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-meta = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-spec = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-spec-rust = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-ledger-snapshot = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-token-sdk = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +soroban-token-spec = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } +stellar-asset-spec = { git = "https://github.com/stellar/rs-soroban-sdk", branch = "spec-shaking-marker-fix-3" } diff --git a/cmd/crates/soroban-test/Cargo.toml b/cmd/crates/soroban-test/Cargo.toml index f89fc70668..d595e5a763 100644 --- a/cmd/crates/soroban-test/Cargo.toml +++ b/cmd/crates/soroban-test/Cargo.toml @@ -41,6 +41,7 @@ home = "0.5.9" shell-escape = "0.1.5" [dev-dependencies] +soroban-spec-markers = { workspace = true } wasm-encoder = "0.235.0" wasmparser = { workspace = true } serde_json = "1.0.93" diff --git a/cmd/crates/soroban-test/tests/fixtures/workspace-with-spec-shaking/contracts/shaking/src/lib.rs b/cmd/crates/soroban-test/tests/fixtures/workspace-with-spec-shaking/contracts/shaking/src/lib.rs index f7192a442d..62a332b71e 100644 --- a/cmd/crates/soroban-test/tests/fixtures/workspace-with-spec-shaking/contracts/shaking/src/lib.rs +++ b/cmd/crates/soroban-test/tests/fixtures/workspace-with-spec-shaking/contracts/shaking/src/lib.rs @@ -53,7 +53,7 @@ impl Contract { 42 } - pub fn hello(env: Env) -> Symbol { + pub fn hello(_env: Env) -> Symbol { symbol_short!("hello") } } diff --git a/cmd/crates/soroban-test/tests/it/build.rs b/cmd/crates/soroban-test/tests/it/build.rs index 1863af976b..025749696f 100644 --- a/cmd/crates/soroban-test/tests/it/build.rs +++ b/cmd/crates/soroban-test/tests/it/build.rs @@ -277,7 +277,7 @@ fn build_with_metadata_diff_dir() { assert_eq!(filtered_entries_dir2, expected_entries_dir2); } -fn build_spec_shaking_fixture() -> (Vec, Vec) { +fn build_spec_shaking_fixture() -> (Vec, Vec, Vec) { let sandbox = TestEnv::default(); let outdir = sandbox.dir().join("out"); let cargo_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); @@ -286,6 +286,7 @@ fn build_spec_shaking_fixture() -> (Vec, Vec) { let dir_path = temp.path(); fs_extra::dir::copy(fixture_path, dir_path, &CopyOptions::new()).unwrap(); let dir_path = dir_path.join("workspace-with-spec-shaking"); + configure_spec_shaking_fixture_from_workspace(&dir_path); sandbox .new_assert_cmd("contract") @@ -299,7 +300,39 @@ fn build_spec_shaking_fixture() -> (Vec, Vec) { let wasm_path = dir_path.join(&outdir).join("shaking.wasm"); let wasm = std::fs::read(wasm_path).unwrap(); let spec = Spec::new(&wasm).unwrap(); - (spec.spec, spec.meta) + (spec.spec, spec.meta, wasm) +} + +fn configure_spec_shaking_fixture_from_workspace(dir_path: &Path) { + let cargo_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + let workspace_manifest_path = cargo_dir.join("../../..").join("Cargo.toml"); + let workspace_manifest = std::fs::read_to_string(workspace_manifest_path).unwrap(); + let workspace_table = toml::from_str::(&workspace_manifest).unwrap(); + let workspace_dependencies = workspace_table["workspace"]["dependencies"] + .as_table() + .unwrap(); + + let mut soroban_sdk = workspace_dependencies["soroban-sdk"] + .as_table() + .unwrap() + .clone(); + soroban_sdk.insert( + "features".to_string(), + toml::Value::Array(vec![toml::Value::String( + "experimental_spec_shaking_v2".to_string(), + )]), + ); + + let fixture_manifest_path = dir_path.join("Cargo.toml"); + let fixture_manifest = std::fs::read_to_string(&fixture_manifest_path).unwrap(); + let mut fixture_table = toml::from_str::(&fixture_manifest).unwrap(); + fixture_table["workspace"]["dependencies"] + .as_table_mut() + .unwrap() + .insert("soroban-sdk".to_string(), toml::Value::Table(soroban_sdk)); + fixture_table.insert("patch".to_string(), workspace_table["patch"].clone()); + + std::fs::write(fixture_manifest_path, fixture_table.to_string()).unwrap(); } fn spec_entry_name(entry: &ScSpecEntry) -> String { @@ -315,7 +348,7 @@ fn spec_entry_name(entry: &ScSpecEntry) -> String { #[test] fn build_with_spec_shaking_filters_unused_types() { - let (spec, _meta) = build_spec_shaking_fixture(); + let (spec, _meta, _wasm) = build_spec_shaking_fixture(); let names: Vec = spec.iter().map(spec_entry_name).collect(); // All functions should be present @@ -359,7 +392,7 @@ fn build_with_spec_shaking_filters_unused_types() { #[test] fn build_with_spec_shaking_filters_unused_events() { - let (spec, _meta) = build_spec_shaking_fixture(); + let (spec, _meta, _wasm) = build_spec_shaking_fixture(); let names: Vec = spec.iter().map(spec_entry_name).collect(); // Used event should be present @@ -375,9 +408,24 @@ fn build_with_spec_shaking_filters_unused_events() { ); } +#[test] +fn build_with_spec_shaking_removes_sidecar_graph() { + let (_spec, _meta, wasm) = build_spec_shaking_fixture(); + + let has_graph = wasmparser::Parser::new(0).parse_all(&wasm).any(|payload| { + matches!( + payload, + Ok(wasmparser::Payload::CustomSection(section)) + if section.name() == soroban_spec_markers::GRAPH_SECTION + ) + }); + + assert!(!has_graph, "spec shaking sidecar graph should be removed"); +} + #[test] fn build_with_spec_shaking_preserves_all_functions() { - let (spec, _meta) = build_spec_shaking_fixture(); + let (spec, _meta, _wasm) = build_spec_shaking_fixture(); let function_names: Vec = spec .iter() .filter(|e| matches!(e, ScSpecEntry::FunctionV0(_))) @@ -391,81 +439,9 @@ fn build_with_spec_shaking_preserves_all_functions() { assert_eq!(function_names.len(), 4, "expected exactly 4 functions"); } -#[test] -fn filter_and_dedup_spec_removes_duplicates() { - use soroban_cli::commands::contract::build::filter_and_dedup_spec; - use soroban_cli::xdr::{ - ReadXdr, ScSpecEntry, ScSpecFunctionInputV0, ScSpecFunctionV0, ScSpecTypeDef, - ScSpecUdtStructFieldV0, ScSpecUdtStructV0, StringM, VecM, - }; - - let func = ScSpecEntry::FunctionV0(ScSpecFunctionV0 { - doc: StringM::default(), - name: "hello".try_into().unwrap(), - inputs: vec![ScSpecFunctionInputV0 { - doc: StringM::default(), - name: "arg0".try_into().unwrap(), - type_: ScSpecTypeDef::U32, - }] - .try_into() - .unwrap(), - outputs: VecM::default(), - }); - - let used_struct = ScSpecEntry::UdtStructV0(ScSpecUdtStructV0 { - doc: StringM::default(), - lib: StringM::default(), - name: "MyStruct".try_into().unwrap(), - fields: vec![ScSpecUdtStructFieldV0 { - doc: StringM::default(), - name: "field".try_into().unwrap(), - type_: ScSpecTypeDef::U32, - }] - .try_into() - .unwrap(), - }); - - // Build markers for the struct so it passes the filter - let mut markers = std::collections::HashSet::new(); - markers.insert(soroban_spec::shaking::generate_marker_for_entry( - &used_struct, - )); - - // Input: function appears twice, struct appears three times - let entries = vec![ - func.clone(), - func.clone(), - used_struct.clone(), - used_struct.clone(), - used_struct.clone(), - ]; - - let result_xdr = filter_and_dedup_spec(entries, &markers).unwrap(); - - // Parse back the entries from the XDR - let result_entries: Vec = - ScSpecEntry::read_xdr_iter(&mut Limited::new(Cursor::new(result_xdr), Limits::none())) - .collect::, _>>() - .unwrap(); - - // Should have exactly 1 function + 1 struct, no duplicates - assert_eq!( - result_entries.len(), - 2, - "expected 2 entries (1 function + 1 struct), got {}: {:?}", - result_entries.len(), - result_entries - .iter() - .map(spec_entry_name) - .collect::>() - ); - assert_eq!(spec_entry_name(&result_entries[0]), "hello"); - assert_eq!(spec_entry_name(&result_entries[1]), "MyStruct"); -} - #[test] fn build_with_spec_shaking_has_feature_meta() { - let (_spec, meta) = build_spec_shaking_fixture(); + let (_spec, meta, _wasm) = build_spec_shaking_fixture(); let version = soroban_spec::shaking::spec_shaking_version_for_meta(&meta); diff --git a/cmd/soroban-cli/Cargo.toml b/cmd/soroban-cli/Cargo.toml index f443372043..4c9bb6eab8 100644 --- a/cmd/soroban-cli/Cargo.toml +++ b/cmd/soroban-cli/Cargo.toml @@ -37,6 +37,7 @@ emulator-tests = ["stellar-ledger/emulator-tests"] [dependencies] stellar-xdr = { workspace = true, features = ["cli"] } soroban-spec = { workspace = true } +soroban-meta = { workspace = true } soroban-spec-rust = { workspace = true } soroban-spec-tools = { workspace = true } soroban-spec-typescript = { workspace = true } @@ -137,6 +138,7 @@ thiserror.workspace = true assert_cmd = "2.0.4" assert_fs = "1.0.7" predicates = { workspace = true } +soroban-spec-markers = { workspace = true } walkdir = "2.5.0" mockito = "1.5.0" serial_test = "3.0.0" diff --git a/cmd/soroban-cli/src/commands/contract/build.rs b/cmd/soroban-cli/src/commands/contract/build.rs index ad0012e8a4..ca874bceb2 100644 --- a/cmd/soroban-cli/src/commands/contract/build.rs +++ b/cmd/soroban-cli/src/commands/contract/build.rs @@ -16,6 +16,7 @@ use std::{ }; use stellar_xdr::curr::{Limited, Limits, ScMetaEntry, ScMetaV0, StringM, WriteXdr}; +use super::spec_shaking; #[cfg(feature = "additional-libs")] use crate::commands::contract::optimize; use crate::{ @@ -186,10 +187,7 @@ pub enum Error { Wasm(#[from] wasm::Error), #[error(transparent)] - SpecTools(#[from] soroban_spec_tools::contract::Error), - - #[error("wasm parsing error: {0}")] - WasmParsing(String), + SpecShaking(#[from] spec_shaking::Error), } const WASM_TARGET: &str = "wasm32v1-none"; @@ -301,27 +299,25 @@ 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)?; let out_file_path = Path::new(out_dir).join(&file); - fs::copy(target_file_path, &out_file_path).map_err(Error::CopyingWasmFile)?; + fs::copy(&target_file_path, &out_file_path).map_err(Error::CopyingWasmFile)?; + spec_shaking::shake_file_if_v2(&target_file_path)?; out_file_path } else { target_file_path }; - let wasm_bytes = fs::read(&final_path).map_err(Error::ReadingWasmFile)?; - #[cfg_attr(not(feature = "additional-libs"), allow(unused_mut))] - let mut optimized_wasm_bytes: Vec = Vec::new(); + let wasm_bytes_before_optimization = + fs::read(&final_path).map_err(Error::ReadingWasmFile)?; #[cfg(feature = "additional-libs")] if self.build_args.optimize { let mut path = final_path.clone(); path.set_extension("optimized.wasm"); optimize::optimize(true, vec![final_path.clone()], Some(path.clone()))?; - optimized_wasm_bytes = fs::read(&path).map_err(Error::ReadingWasmFile)?; fs::remove_file(&final_path).map_err(Error::DeletingArtifact)?; fs::rename(&path, &final_path).map_err(Error::CopyingWasmFile)?; @@ -332,6 +328,15 @@ impl Cmd { return Err(Error::OptimizeFeatureNotEnabled); } + spec_shaking::shake_file_if_v2(&final_path)?; + + let final_wasm_bytes = fs::read(&final_path).map_err(Error::ReadingWasmFile)?; + let (wasm_bytes, optimized_wasm_bytes) = if self.build_args.optimize { + (wasm_bytes_before_optimization, final_wasm_bytes) + } else { + (final_wasm_bytes, Vec::new()) + }; + Self::print_build_summary( &print, &p.name, @@ -428,46 +433,6 @@ impl Cmd { fs::write(target_file_path, wasm_bytes).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::Spec; - use soroban_spec_tools::wasm::replace_custom_section; - - let wasm_bytes = fs::read(target_file_path).map_err(Error::ReadingWasmFile)?; - - // Parse the spec from the wasm - let spec = Spec::new(&wasm_bytes)?; - - // Check if the contract meta indicates spec shaking v2 is enabled. - if soroban_spec::shaking::spec_shaking_version_for_meta(&spec.meta) != 2 { - return Ok(()); - } - - // Extract markers from the WASM data section - let markers = soroban_spec::shaking::find_all(&wasm_bytes); - - // Filter spec entries (types, events) based on markers, and - // deduplicate any exact duplicate entries. - let filtered_xdr = filter_and_dedup_spec(spec.spec.clone(), &markers)?; - - // Replace the contractspecv0 section with the filtered version - let new_wasm = replace_custom_section(&wasm_bytes, "contractspecv0", &filtered_xdr) - .map_err(|e| Error::WasmParsing(e.to_string()))?; - - // 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> { let mut new_meta: Vec = Vec::new(); @@ -794,28 +759,6 @@ fn check_overflow_checks(doc: &toml_edit::DocumentMut, profile: &str) -> Result< } } -/// Filters spec entries based on markers and deduplicates exact duplicates. -/// -/// Functions are always kept. Other entries (types, events) are kept only if a -/// matching marker exists. Exact duplicate entries (identical XDR) are collapsed -/// to a single occurrence. -#[allow(clippy::implicit_hasher)] -pub fn filter_and_dedup_spec( - entries: Vec, - markers: &HashSet, -) -> Result, Error> { - let mut seen = HashSet::new(); - let mut filtered_xdr = Vec::new(); - let mut writer = Limited::new(Cursor::new(&mut filtered_xdr), Limits::none()); - for entry in soroban_spec::shaking::filter(entries, markers) { - let entry_xdr = entry.to_xdr(Limits::none())?; - if seen.insert(entry_xdr) { - entry.write_xdr(&mut writer)?; - } - } - Ok(filtered_xdr) -} - #[cfg(test)] mod tests { use super::*; diff --git a/cmd/soroban-cli/src/commands/contract/info/interface.rs b/cmd/soroban-cli/src/commands/contract/info/interface.rs index bd1a74fe53..20c441005c 100644 --- a/cmd/soroban-cli/src/commands/contract/info/interface.rs +++ b/cmd/soroban-cli/src/commands/contract/info/interface.rs @@ -37,6 +37,8 @@ pub enum Error { Wasm(#[from] shared::Error), #[error(transparent)] Spec(#[from] contract::Error), + #[error(transparent)] + GenerateRust(#[from] soroban_spec_rust::GenerateError), #[error("no interface present in provided WASM file")] NoInterfacePresent(), #[error(transparent)] @@ -67,7 +69,7 @@ impl Cmd { InfoOutput::XdrBase64 => base64, InfoOutput::Json => serde_json::to_string(&spec)?, InfoOutput::JsonFormatted => serde_json::to_string_pretty(&spec)?, - InfoOutput::Rust => soroban_spec_rust::generate_without_file(&spec) + InfoOutput::Rust => soroban_spec_rust::generate_without_file(&spec)? .to_formatted_string() .expect("Unexpected spec format error"), }; diff --git a/cmd/soroban-cli/src/commands/contract/mod.rs b/cmd/soroban-cli/src/commands/contract/mod.rs index 294a6d1b00..7f58160440 100644 --- a/cmd/soroban-cli/src/commands/contract/mod.rs +++ b/cmd/soroban-cli/src/commands/contract/mod.rs @@ -14,6 +14,7 @@ pub mod invoke; pub mod optimize; pub mod read; pub mod restore; +pub(crate) mod spec_shaking; pub mod spec_verify; pub mod upload; diff --git a/cmd/soroban-cli/src/commands/contract/spec_shaking.rs b/cmd/soroban-cli/src/commands/contract/spec_shaking.rs new file mode 100644 index 0000000000..3f9ebcd849 --- /dev/null +++ b/cmd/soroban-cli/src/commands/contract/spec_shaking.rs @@ -0,0 +1,132 @@ +use std::{fs, io, path::Path}; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("reading wasm file: {0}")] + ReadingWasmFile(io::Error), + + #[error("deleting existing artifact: {0}")] + DeletingArtifact(io::Error), + + #[error("writing wasm file: {0}")] + WritingWasmFile(io::Error), + + #[error(transparent)] + Meta(#[from] soroban_meta::read::FromWasmError), + + #[error(transparent)] + Shake(#[from] soroban_spec::strip::ShakeError), +} + +pub fn shake_file_if_v2(wasm_path: &Path) -> Result<(), Error> { + let wasm = fs::read(wasm_path).map_err(Error::ReadingWasmFile)?; + let Some(shaken) = shake_if_v2(&wasm)? else { + return Ok(()); + }; + + fs::remove_file(wasm_path).map_err(Error::DeletingArtifact)?; + fs::write(wasm_path, shaken).map_err(Error::WritingWasmFile) +} + +fn shake_if_v2(wasm: &[u8]) -> Result>, Error> { + let meta = soroban_meta::read::from_wasm(wasm)?; + let version = soroban_spec::shaking::spec_shaking_version_for_meta(&meta); + + if version != 2 { + return Ok(None); + } + + soroban_spec::strip::shake_contract_spec(wasm) + .map(Some) + .map_err(Error::from) +} + +#[cfg(test)] +mod tests { + use super::*; + use soroban_spec_markers::{ + generate_graph_record, generate_spec_id_for_xdr, SpecGraphEntryKind, GRAPH_SECTION, + META_KEY, META_VALUE_V2, + }; + use stellar_xdr::curr::{ + Limits, ScMetaEntry, ScMetaV0, ScSpecEntry, ScSpecFunctionV0, StringM, VecM, WriteXdr, + }; + + const CONTRACT_META_SECTION: &str = "contractmetav0"; + const CONTRACT_SPEC_SECTION: &str = "contractspecv0"; + + #[test] + fn leaves_non_v2_wasm_unchanged() { + let wasm = wasm_with_sections(None, true); + + assert!(shake_if_v2(&wasm).unwrap().is_none()); + } + + #[test] + fn shakes_v2_wasm_and_removes_sidecar() { + let wasm = wasm_with_sections(Some(META_VALUE_V2), true); + + let shaken = shake_if_v2(&wasm).unwrap().unwrap(); + + let section_names = custom_section_names(&shaken); + assert!(section_names.contains(&CONTRACT_META_SECTION.to_string())); + assert!(section_names.contains(&CONTRACT_SPEC_SECTION.to_string())); + assert!(!section_names.contains(&GRAPH_SECTION.to_string())); + + let spec = soroban_spec::read::from_wasm(&shaken).unwrap(); + assert_eq!(spec.len(), 1); + } + + fn wasm_with_sections(meta_version: Option<&str>, include_graph: bool) -> Vec { + let mut wasm = b"\0asm\x01\0\0\0".to_vec(); + let spec = spec_xdr(); + wasm_gen::write_custom_section(&mut wasm, CONTRACT_META_SECTION, &meta_xdr(meta_version)); + wasm_gen::write_custom_section(&mut wasm, CONTRACT_SPEC_SECTION, &spec); + if include_graph { + wasm_gen::write_custom_section(&mut wasm, GRAPH_SECTION, &graph_record(&spec)); + } + wasm + } + + fn graph_record(spec_xdr: &[u8]) -> Vec { + generate_graph_record( + SpecGraphEntryKind::Function, + generate_spec_id_for_xdr(spec_xdr), + &[], + ) + } + + fn meta_xdr(version: Option<&str>) -> Vec { + let Some(version) = version else { + return Vec::new(); + }; + + ScMetaEntry::ScMetaV0(ScMetaV0 { + key: META_KEY.try_into().unwrap(), + val: version.try_into().unwrap(), + }) + .to_xdr(Limits::none()) + .unwrap() + } + + fn spec_xdr() -> Vec { + ScSpecEntry::FunctionV0(ScSpecFunctionV0 { + doc: StringM::default(), + name: "hello".try_into().unwrap(), + inputs: VecM::default(), + outputs: VecM::default(), + }) + .to_xdr(Limits::none()) + .unwrap() + } + + fn custom_section_names(wasm: &[u8]) -> Vec { + let mut names = Vec::new(); + for payload in wasmparser::Parser::new(0).parse_all(wasm) { + if let Ok(wasmparser::Payload::CustomSection(section)) = payload { + names.push(section.name().to_string()); + } + } + names + } +}