Graph-based spec shaking v2#1861
Conversation
Add a `SpecShakingMarker` bound to `Env::panic_with_error` under `experimental_spec_shaking_v2` and call `I::spec_shaking_marker()` before erasing the user's type via `.into()`. Share the body with the non-feature build via a private `panic_with_error_inner` that takes the already-converted `internal::Error`. Add tests in `tests/spec_shaking_v2/`: - `with_panic_error` + `UsedPanicErrorEnum` — error enum used only via `panic_with_error!`; asserts the spec entry is retained. - `with_assert_error` + `UsedAssertErrorEnum` — same, via `assert_with_error!`. - `with_panic_raw_error` — passes a raw `soroban_sdk::Error` to `panic_with_error!`; regression check that the new bound does not break callers using the un-typed error. - `UnusedPubError` — a `#[contracterror]` enum referenced nowhere; asserts it is correctly shaken out of filtered entries. Closes #1817. Spec shaking v2 keeps a type's `contractspecv0` entry only if its `SpecShakingMarker::spec_shaking_marker()` survives DCE, and the only paths that call the marker are the v2 blanket impls of `IntoValForContractFn` (return values) and `TryFromValForContractFn` (parameters). A contract that defined a `#[contracterror]` enum and used it only via `panic_with_error!` or `assert_with_error!` — without ever appearing in a `Result<_, E>` return type — had no live caller for the marker, so the marker function was eliminated and the spec entry stripped from the WASM, even though the enum is part of the contract's observable error interface.
### What: Add a GitHub Actions workflow that runs `anthropics/claude-code-action` on non-draft pull requests to review code quality, bugs, security, and performance, posting inline comments. Concurrency cancels in-progress runs per PR. ### Why: Experimenting with and comparing the outcomes and experience with using the built-in GitHub AI reviews.
There was a problem hiding this comment.
Pull request overview
Adds graph-based spec shaking v2 to the Rust SDK stack so post-build tooling can keep only spec entries reachable from contract APIs, published events, and typed thrown errors, using exact hashed graph edges instead of name-based markers.
Changes:
- Introduces the v2 spec-shaking pipeline: exact
SpecTypeIds, removable sidecar graph records, marker scanning, and WASM rewriting/graph stripping. - Updates SDK/macros/auth/address behavior and tests for constructors, custom-account auth, recursive UDTs, panic/assert errors, and duplicate-name cases.
- Refreshes generated fixtures/snapshots across the test suite, and also adds a new Claude review workflow.
Reviewed changes
Copilot reviewed 116 out of 125 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/udt/test_snapshots/test/test_recursive.1.json | Snapshot for recursive UDT wasm test. |
| tests/udt/test_snapshots/test/test_recursive_enum.1.json | Snapshot for recursive enum wasm test. |
| tests/udt/src/lib.rs | Adds recursive UDT contract cases and wasm-import tests. |
| tests/spec_shaking_v2/src/test.rs | Expands v2 reachability/filter/strip assertions. |
| tests/spec_shaking_v2/src/lib.rs | Adds broad v2 fixture coverage for roots and graph traversal. |
| tests/spec_shaking_v1/src/test.rs | Updates v1 expectations for constructor/auth and duplicate names. |
| tests/associated_type_contracttrait/test_snapshots/test_with_wasm/test_exec.1.json | Snapshot refresh for associated-type contracttrait wasm. |
| tests-expanded/test_workspace_lib_wasm32v1-none.rs | Wasm-expanded fixture showing v2 type graph emission. |
| tests-expanded/test_workspace_lib_tests.rs | Host-expanded fixture mirroring v2 type graph emission. |
| tests-expanded/test_workspace_contract_wasm32v1-none.rs | Wasm-expanded fixture showing v2 function graph emission. |
| tests-expanded/test_workspace_contract_tests.rs | Host-expanded fixture for workspace contract graph code. |
| tests-expanded/test_tuples_wasm32v1-none.rs | Wasm-expanded tuple fixture with function graph records. |
| tests-expanded/test_mutability_wasm32v1-none.rs | Wasm-expanded mutability fixture with function graph records. |
| tests-expanded/test_mutability_tests.rs | Host-expanded mutability fixture for generated graph code. |
| tests-expanded/test_multiimpl_wasm32v1-none.rs | Wasm-expanded multi-impl fixture with graph records. |
| tests-expanded/test_multiimpl_tests.rs | Host-expanded multi-impl fixture for generated graph code. |
| tests-expanded/test_modular_wasm32v1-none.rs | Wasm-expanded modular fixture with graph records. |
| tests-expanded/test_modular_tests.rs | Host-expanded modular fixture for generated graph code. |
| tests-expanded/test_macros_wasm32v1-none.rs | Wasm-expanded macro fixture with graph records. |
| tests-expanded/test_macros_tests.rs | Host-expanded macro fixture for generated graph code. |
| tests-expanded/test_logging_wasm32v1-none.rs | Wasm-expanded logging fixture with graph records. |
| tests-expanded/test_logging_tests.rs | Host-expanded logging fixture for generated graph code. |
| tests-expanded/test_invoke_contract_wasm32v1-none.rs | Wasm-expanded cross-contract invoke fixture with graph records. |
| tests-expanded/test_invoke_contract_tests.rs | Host-expanded cross-contract invoke fixture. |
| tests-expanded/test_generics_wasm32v1-none.rs | Wasm-expanded generics fixture with graph records. |
| tests-expanded/test_generics_tests.rs | Host-expanded generics fixture for generated graph code. |
| tests-expanded/test_fuzz_wasm32v1-none.rs | Wasm-expanded fuzz fixture with graph records. |
| tests-expanded/test_fuzz_tests.rs | Host-expanded fuzz fixture for generated graph code. |
| tests-expanded/test_events_wasm32v1-none.rs | Wasm-expanded event fixture with event roots and graph records. |
| tests-expanded/test_events_tests.rs | Host-expanded event fixture reflecting v2 event generation. |
| tests-expanded/test_events_ref_wasm32v1-none.rs | Wasm-expanded ref-event fixture with event roots and graph records. |
| tests-expanded/test_events_ref_tests.rs | Host-expanded ref-event fixture reflecting v2 generation. |
| tests-expanded/test_errors_wasm32v1-none.rs | Wasm-expanded error fixture with type IDs/graph records. |
| tests-expanded/test_errors_tests.rs | Host-expanded error fixture reflecting v2 generation. |
| tests-expanded/test_empty_wasm32v1-none.rs | Wasm-expanded empty-contract fixture with graph records. |
| tests-expanded/test_empty_tests.rs | Host-expanded empty-contract fixture. |
| tests-expanded/test_contracttrait_trait_wasm32v1-none.rs | Wasm-expanded contracttrait fixture with UDT graph metadata. |
| tests-expanded/test_contracttrait_path_super_wasm32v1-none.rs | Wasm-expanded path-super fixture with graph records. |
| tests-expanded/test_contracttrait_path_super_tests.rs | Host-expanded path-super fixture. |
| tests-expanded/test_contracttrait_path_self_wasm32v1-none.rs | Wasm-expanded path-self fixture with graph records. |
| tests-expanded/test_contracttrait_path_self_tests.rs | Host-expanded path-self fixture. |
| tests-expanded/test_contracttrait_path_relative_wasm32v1-none.rs | Wasm-expanded path-relative fixture with graph records. |
| tests-expanded/test_contracttrait_path_relative_tests.rs | Host-expanded path-relative fixture. |
| tests-expanded/test_contracttrait_path_crate_wasm32v1-none.rs | Wasm-expanded path-crate fixture with graph records. |
| tests-expanded/test_contracttrait_path_crate_tests.rs | Host-expanded path-crate fixture. |
| tests-expanded/test_contract_data_wasm32v1-none.rs | Wasm-expanded contract-data fixture with graph records. |
| tests-expanded/test_contract_data_tests.rs | Host-expanded contract-data fixture. |
| tests-expanded/test_constructor_wasm32v1-none.rs | Wasm-expanded constructor fixture with graph records. |
| tests-expanded/test_constructor_tests.rs | Host-expanded constructor fixture. |
| tests-expanded/test_bn254_wasm32v1-none.rs | Wasm-expanded bn254 fixture with type/function graph metadata. |
| tests-expanded/test_bn254_tests.rs | Host-expanded bn254 fixture. |
| tests-expanded/test_bls_wasm32v1-none.rs | Wasm-expanded bls fixture with type/function graph metadata. |
| tests-expanded/test_bls_tests.rs | Host-expanded bls fixture. |
| tests-expanded/test_auth_wasm32v1-none.rs | Wasm-expanded auth fixture with function/type graph metadata. |
| tests-expanded/test_auth_tests.rs | Host-expanded auth fixture. |
| tests-expanded/test_associated_types_wasm32v1-none.rs | Wasm-expanded associated-types fixture with graph records. |
| tests-expanded/test_associated_types_tests.rs | Host-expanded associated-types fixture. |
| tests-expanded/test_associated_types_contracttrait_wasm32v1-none.rs | Wasm-expanded associated-types contracttrait fixture. |
| tests-expanded/test_associated_type_chained_wasm32v1-none.rs | Wasm-expanded chained-associated-type fixture with graph records. |
| tests-expanded/test_associated_type_chained_tests.rs | Host-expanded chained-associated-type fixture. |
| tests-expanded/test_add_u64_wasm32v1-none.rs | Wasm-expanded add-u64 fixture with graph records. |
| tests-expanded/test_add_u64_tests.rs | Host-expanded add-u64 fixture. |
| tests-expanded/test_add_u128_wasm32v1-none.rs | Wasm-expanded add-u128 fixture with graph records. |
| tests-expanded/test_add_u128_tests.rs | Host-expanded add-u128 fixture. |
| tests-expanded/test_add_i128_wasm32v1-none.rs | Wasm-expanded add-i128 fixture with graph records. |
| tests-expanded/test_add_i128_tests.rs | Host-expanded add-i128 fixture. |
| tests-expanded/test_account_wasm32v1-none.rs | Wasm-expanded account fixture with auth-context graph metadata. |
| tests-expanded/test_account_tests.rs | Host-expanded account fixture. |
| soroban-spec/src/lib.rs | Exposes new strip module. |
| soroban-spec/src/shaking.rs | Implements v2 marker scanning and graph-based reachability. |
| soroban-spec/src/strip.rs | Adds WASM spec rewrite/graph stripping helpers. |
| soroban-spec/Cargo.toml | Adds marker crate dependency for v2 support. |
| soroban-spec-rust/src/lib.rs | Refreshes generated Rust bindings for new recursive/auth types. |
| soroban-spec-markers/Cargo.toml | Adds new crate manifest for marker/graph wire formats. |
| soroban-spec-markers/src/lib.rs | Defines marker/spec-id/graph record wire-format helpers. |
| soroban-sdk/src/try_from_val_for_contract_fn.rs | Removes v2 boundary marker rooting from arg conversion path. |
| soroban-sdk/src/lib.rs | Re-exports hidden v2 support module and updates docs. |
| soroban-sdk/src/into_val_for_contract_fn.rs | Removes v2 boundary marker rooting from return conversion path. |
| soroban-sdk/src/env.rs | Routes panic-with-error through typed marker hook under v2. |
| soroban-sdk/src/auth.rs | Makes auth boundary SDK types exportable under v2. |
| soroban-sdk/src/address.rs | Makes address Executable exportable under v2. |
| soroban-sdk/src/spec_shaking.rs | Adds hidden runtime traits/helpers for v2 generation. |
| soroban-sdk/src/_features.rs | Rewrites v2 feature docs around graph-based shaking. |
| soroban-sdk/Cargo.toml | Adds marker crate dependency. |
| soroban-sdk-macros/src/lib.rs | Threads crate_path into contract spec generation. |
| soroban-sdk-macros/src/derive_trait.rs | Passes crate_path into generated contractspecfn. |
| soroban-sdk-macros/src/derive_struct.rs | Emits v2 type IDs/graph records for struct UDTs. |
| soroban-sdk-macros/src/derive_struct_tuple.rs | Emits v2 type IDs/graph records for tuple structs. |
| soroban-sdk-macros/src/derive_spec_fn.rs | Emits function graph records from exact referenced UDTs. |
| soroban-sdk-macros/src/derive_event.rs | Emits event graph records and inline event root markers. |
| soroban-sdk-macros/src/derive_error_enum_int.rs | Emits v2 type IDs/graph records for contract errors. |
| soroban-sdk-macros/src/derive_enum.rs | Emits v2 type IDs/graph records for union enums. |
| soroban-sdk-macros/src/derive_enum_int.rs | Emits v2 type IDs/graph records for int enums. |
| soroban-sdk-macros/src/derive_contractimpl_trait_default_fns_not_overridden.rs | Updates trait-default spec generation to pass crate path. |
| soroban-sdk-macros/src/shaking.rs | Adds macro-side helpers for markers, type IDs, and graph records. |
| soroban-sdk-macros/Cargo.toml | Adds marker crate dependency with alloc/hash features. |
| Makefile | Exports v2 build-support env var in test command. |
| Cargo.toml | Registers new soroban-spec-markers workspace member. |
| Cargo.lock | Locks new marker crate dependency edges. |
| .github/workflows/claude-review.yml | Adds a new Claude-based PR review workflow. |
|
Claude finished @mootz12's task in 4m 6s —— View job PR Review: Graph-based spec shaking v2
|
|
Responses to #1861 (comment) High Priority
Events can be emitted as raw data, without specs, through these mediums. Added a TODO for this, but it's not relevant to the new design. Will consider patching here or at least in a followup if we opt to take this solutions approach.
Tracked in a TODO
This was an intentional choice. WASMs currently don't have the ability to differentiate a Medium Priority
We can consider this if it's an issue, but is not necessary per my testing.
Fixed, prefer
Fixed
Won't fix - 2 of these should never exist and this PR opts to ignore intentionally broken WASMs. Low Priority / Observations
Improved the existing validation test to reduce drift risk.
Fixed
Nothing to fix
Fixed |
|
Claude finished @mootz12's task in 2m 25s —— View job PR Review: Graph-based spec shaking v2 — Follow-upTasks:
All four claimed fixes are confirmed correct, plus two unrequested improvements. Here's the full accounting. Confirmed Fixes#5 — Error attribution order ( The #6 — Cargo.toml trailing newline ( Confirmed: file now ends with a newline. #9 — Correctly changed to #11 — Renamed consistently. ✓ Unrequested Improvements (Good Additions)Panic → compile error in The
The Test Coverage Improvement (#8)The Remaining Open ItemsThe three items from the previous review that the author deferred are still open:
Minor ObservationThe Everything else looks clean. The four claimed fixes are correct, the two bonus improvements are sound, and the test expansion is substantial. The outstanding items are either known-acceptable tradeoffs or tracked TODOs. |
|
Some light thoughts after a first read: Very nice. 😎
I would not expect it to be eliminated based on how we already use the other custom sections.
I'd like to understand more about why this is. Is this a limitation of the existing spec shaking v2, or only the spec shaking v2 in this PR? Does this implementation avoid recursive types from stack overflow? In this design where the graph is separate from the spec and has a narrow use case I understand the motivation to discard it. However, I think the relationships and the type id's more concretely linking type dependencies is helpful and if that information was instead stored in the spec itself, it would make the spec more exact/accurate. I think we should produce XDR valid values for the graph / spec-with-graph. That shouldn't be hard for us to do either by hand or by introducing a const form of xdr encoding into the rs-stellar-xdr crate. |
Since we are walking specs exactly, we now error whenever a spec is missing for a type that is defined at a public endpoint. This is problematic for contracts that write their own Given these types could change as they are not defined in XDR so don't have any backwards compatibility guarantees, it makes sense that they are included in the spec when used to avoid drift.
I agree. But it's not as obvious what the new XDR shape should be. Currently, the graph uses hashes of the spec entry that is structure / name based, but if we make the branches rely on hashes over names, now we can't compute hashes of recursive structures.
I also agree. Just defined it locally for now to see if this direction is right, and if we wanted to pursue just updating the spec-xdr directly instead. |

What
Add graph-based spec shaking v2 for Rust contracts. The SDK macros now emit a removable sidecar graph that records exact spec-entry references by hash, while
contractspecv0keeps the public contract interface. Post-build tooling treats exported functions as roots, adds marker roots for events and thrown errors, follows the sidecar graph to keep only reachable UDT specs, rewritescontractspecv0, and removes the private graph section from the final WASM.This changes spec shaking from a marker-per-type scheme to a root-and-graph traversal: markers identify roots that cannot be inferred from the public spec, and the graph resolves the UDTs reachable from those roots without relying on ambiguous type names.
soroban-spec-markersto own spec-shaking marker, metadata, and sidecar graph wire formats.SpecTypeIdvalues, and removable graph records keyed by spec-entry hash.contractspecv0entries through function roots, marker roots, and exact graph references, then strip the private graph section during post-build rewriting.Why
Spec shaking needs to remove unused contract spec entries without relying on ambiguous UDT names or keeping marker bytes in the final public interface. The v2 graph gives post-build tooling exact spec-entry identities, lets functions act as roots from their public specs, and reserves markers for roots that are not visible in
contractspecv0, such as published events and errors thrown throughpanic_with_error!orassert_with_error!.Architecture
flowchart LR Macros[SDK macros] --> Markers[SpEcV1 root markers] Macros --> Graph[SpGrV1 sidecar graph] Macros --> Spec[contractspecv0 entries] Markers --> Filter[soroban-spec shake_contract_spec] Graph --> Filter Spec --> Filter Filter --> Shaken[rewritten contractspecv0] Filter --> Strip[drop private sidecar]Known limitations
export = falseremain hidden fromcontractspecv0; if a public boundary references one, graph validation reports the missing referenced spec instead of synthesizing a public entry.contractimport!but have not been shaken will include allcontracttypetypes defined by dependencies, including thesoroban-sdkwith types likesoroban-sdk::auth::Context.contracttypewith the same name as another, importedcontracttype, like via a WASM import, both are including in the spec. If both types are referenced by a boundary, they will both exist.TODO
contractspecv0.rssdk.graphv0graph records won't be eliminated by LTO (testing says it won't, but find some more concrete evidence)shake_contract_specsoroban-specversion? E.g. - passsoroban-spec-versionvia env variable during build and use that overexperimental_spec_shaking_v2env var?