Problem Statement / Feature Objective
OpenTelemetry distributed tracing currently captures spans within the Rust backend but breaks at the Soroban boundary: each cross-contract call from the settlement pipeline to the Soroban runtime is a black box with no propagated trace context. When a mint_token call fails due to a contract panic or a rollback_batch call exceeds its gas limit, the root cause is invisible in traces — engineers must manually correlate Soroban logs with backend spans. A trace context propagation mechanism must be implemented that injects the current trace_id, span_id, and trace_flags into Soroban contract calls via the contract's env::invoke_contract arguments, and captures Soroban execution spans on the host side via Soroban's diagnostic event emission.
Technical Invariants & Bounds
- Trace context serialized as a 32-byte
trace_id + 8-byte span_id + 1-byte trace_flags (W3C traceparent format), hex-encoded into a 73-byte ASCII string.
- Context passed as an additional
BytesN<73> argument to every Soroban contract invocation. The contract extracts it and emits a diagnostic_event at each entry/exit point.
- Soroban diagnostic events have a max size of 1,024 bytes per event; the trace context payload plus a 1-byte event type code fits well within this limit.
- Each Soroban invocation may spawn up to 5 nested sub-calls (depth limit per Soroban's
max_call_depth config); each sub-call propagates the same trace_id with a new span_id.
- Host-side span capture: a background task polls the Soroban RPC
get_events endpoint every 500 ms, filters for diagnostic events matching the known trace context pattern, and exports them as OpenTelemetry spans.
- Trace context injection must add ≤ 100 µs overhead per contract call on the Rust side (negligible compared to the ~500 ms Soroban round-trip).
Codebase Navigation Guide
src/tracing/soroban_propagator.rs — new module: context injection into Soroban invocations and extraction from diagnostic events.
src/blockchain/soroban/client.rs — extend all contract invocation methods to accept an optional trace_context: &TraceContext parameter.
src/blockchain/soroban/contracts/settlement.wat — add WAT code that extracts the trace context from the extra argument and emits diagnostic events at function entry/exit.
src/tracing/exporters/soroban.rs — new module: OpenTelemetry span exporter that reads Soroban diagnostic events.
src/tracing/lib.rs — create SorobanTraceLayer that wraps the Soroban client and automatically injects the current span context.
src/config/default.toml — add [tracing.soroban_propagation] with { enabled = true, poll_interval_ms = 500 }.
tests/tracing/soroban_trace_propagation_test.rs — integration test using a local Soroban devnet.
Implementation Blueprint
- In
soroban_propagator.rs, define fn inject_context(trace_context: &SpanContext) -> String that hex-encodes trace_id, span_id, trace_flags into a 73-byte ASCII string, and fn extract_context(diagnostic_event: &SorobanEvent) -> Option<(TraceId, SpanId, TraceFlags)> that parses it back.
- In
client.rs, add an overload for each contract method (e.g., async fn submit_batch_proof(&self, ..., trace_ctx: Option<TraceContext>)) that prepends the trace context as an additional ScVal::Symbol argument to the Soroban invoke_contract operation. If trace_ctx is None, pass an empty placeholder.
- In the Soroban contract (WAT), at the top of each exported function, emit a
diagnostic_event with type 0x01 (entry) containing the trace context bytes. Before returning, emit 0x02 (exit) with the same trace context and an i64 status code. The contract must accept an extra parameter trace_ctx at index 0 and pass it through to sub-calls.
- In
exporters/soroban.rs, implement SorobanEventPoller that runs in a background tokio task: every poll_interval_ms, call get_events on the Soroban RPC, filtering for events where event_type == "diagnostic" and the payload starts with known marker bytes. Parse each event into an OpenTelemetry span skeleton, linking parent-child spans via the span_id chain.
- In
lib.rs, define SorobanTraceLayer that implements tower::Layer for the Soroban client service. On each request, if there is an active OpenTelemetry span, call inject_context; on response, await the poller to report the corresponding Soroban-side span (correlated by the span_id the backend generated). Export both sides as a single composite trace.
- Add a
SpanLink in the OpenTelemetry trace to correlate the backend's HTTP POST /invoke span with the Soroban execution span, using SpanKind::CLIENT for the backend and SpanKind::SERVER for the contract.
- Write an integration test that calls a Soroban contract with trace propagation, captures both sides of the trace, and verifies the
trace_id is identical, the parent span_id of the Soroban span matches the backend span's span_id, and duration deltas are within 1% of the measured wall-clock time.
Problem Statement / Feature Objective
OpenTelemetry distributed tracing currently captures spans within the Rust backend but breaks at the Soroban boundary: each cross-contract call from the settlement pipeline to the Soroban runtime is a black box with no propagated trace context. When a
mint_tokencall fails due to a contract panic or arollback_batchcall exceeds its gas limit, the root cause is invisible in traces — engineers must manually correlate Soroban logs with backend spans. A trace context propagation mechanism must be implemented that injects the currenttrace_id,span_id, andtrace_flagsinto Soroban contract calls via the contract'senv::invoke_contractarguments, and captures Soroban execution spans on the host side via Soroban's diagnostic event emission.Technical Invariants & Bounds
trace_id+ 8-bytespan_id+ 1-bytetrace_flags(W3C traceparent format), hex-encoded into a 73-byte ASCII string.BytesN<73>argument to every Soroban contract invocation. The contract extracts it and emits adiagnostic_eventat each entry/exit point.max_call_depthconfig); each sub-call propagates the sametrace_idwith a newspan_id.get_eventsendpoint every 500 ms, filters for diagnostic events matching the known trace context pattern, and exports them as OpenTelemetry spans.Codebase Navigation Guide
src/tracing/soroban_propagator.rs— new module: context injection into Soroban invocations and extraction from diagnostic events.src/blockchain/soroban/client.rs— extend all contract invocation methods to accept an optionaltrace_context: &TraceContextparameter.src/blockchain/soroban/contracts/settlement.wat— add WAT code that extracts the trace context from the extra argument and emits diagnostic events at function entry/exit.src/tracing/exporters/soroban.rs— new module: OpenTelemetry span exporter that reads Soroban diagnostic events.src/tracing/lib.rs— createSorobanTraceLayerthat wraps the Soroban client and automatically injects the current span context.src/config/default.toml— add[tracing.soroban_propagation]with{ enabled = true, poll_interval_ms = 500 }.tests/tracing/soroban_trace_propagation_test.rs— integration test using a local Soroban devnet.Implementation Blueprint
soroban_propagator.rs, definefn inject_context(trace_context: &SpanContext) -> Stringthat hex-encodestrace_id,span_id,trace_flagsinto a 73-byte ASCII string, andfn extract_context(diagnostic_event: &SorobanEvent) -> Option<(TraceId, SpanId, TraceFlags)>that parses it back.client.rs, add an overload for each contract method (e.g.,async fn submit_batch_proof(&self, ..., trace_ctx: Option<TraceContext>)) that prepends the trace context as an additionalScVal::Symbolargument to the Sorobaninvoke_contractoperation. Iftrace_ctxis None, pass an empty placeholder.diagnostic_eventwith type0x01(entry) containing the trace context bytes. Before returning, emit0x02(exit) with the same trace context and ani64status code. The contract must accept an extra parametertrace_ctxat index 0 and pass it through to sub-calls.exporters/soroban.rs, implementSorobanEventPollerthat runs in a background tokio task: everypoll_interval_ms, callget_eventson the Soroban RPC, filtering for events whereevent_type == "diagnostic"and the payload starts with known marker bytes. Parse each event into an OpenTelemetry span skeleton, linking parent-child spans via thespan_idchain.lib.rs, defineSorobanTraceLayerthat implementstower::Layerfor the Soroban client service. On each request, if there is an active OpenTelemetry span, callinject_context; on response, await the poller to report the corresponding Soroban-side span (correlated by thespan_idthe backend generated). Export both sides as a single composite trace.SpanLinkin the OpenTelemetry trace to correlate the backend'sHTTP POST /invokespan with the Soroban execution span, usingSpanKind::CLIENTfor the backend andSpanKind::SERVERfor the contract.trace_idis identical, the parentspan_idof the Soroban span matches the backend span'sspan_id, and duration deltas are within 1% of the measured wall-clock time.