truapi-provider's UniFFI boundary has gaps that are not reachable by a shipped consumer yet: there is no @parity/ios-provider release, the release allowlist excludes it, the Maven publication is local-only, and the npm builds are wasm where mod ffi is compiled out.
Android has no working checksum guard
Generated Kotlin puts the checksum comparisons in object IntegrityCheckingUniffiLib and registers the callback vtable in a separate object UniffiLib. Kotlin object init is lazy, and the only reference to the first is uniffiEnsureInitialized(), which nothing calls. iOS runs the equivalent guard from makeRustCall before the vtable is installed.
Consequence: a stale .so with fresh bindings is undetected on Android. Callback arity can change without the C symbol name changing, so the linker does not catch it either, and the result is a wrong-slot RustBuffer read. The Android README now says the guard is iOS-only; the guard itself is generated, so the fix is upstream.
A ChainCloseReason variant addition is not detected at runtime
A uniffi enum's type id comes from its module path and name, not its variants, so adding a variant does not move uniffi_truapi_provider_checksum_method_chainmessagelistener_on_closed. Old bindings pass the integrity check, then fail in FfiConverterTypeChainCloseReason.read with unexpectedEnumCase, which becomes ChainProviderError::Listener and is only logged. The host's on_closed never fires.
Both READMEs now say the @unknown default / else guidance is source compatibility only. What is still open is the process.
No Kotlin gate for the provider
make android-check covers :truapi-host only, and no workflow invokes Gradle for :truapi-provider. A naive gate would pass vacuously: build.gradle.kts uses java.srcDirs("src/main/kotlin") while make provider-kotlin writes to the gitignored src/main/kotlin/generated/, so the module compiles zero files without a prior generation.
tracing is compiled out of the shipped native artifact
mod logging is #[cfg(all(feature = "js", target_arch = "wasm32"))] and provider builds use --no-default-features --features uniffi, so no subscriber can be installed and warn! fields are never formatted. Every diagnostic in ffi.rs is invisible on device. The unbounded reason strings passed to warn! are also one set_global_default away from being live, and truapi-server calls it at native host boot.
Uncapped resources
The bound on a close reason is 256 Unicode scalar values, which both READMEs now state along with what that means per platform. It has no byte ceiling, so a reason can still reach 1024 bytes.
No cap on concurrent connections. Each costs a pump-thread stack (2.0 MiB measured, Darwin arm64), an add_chain, a 1024-slot frame channel, and about 1.1 MiB of transient chain-spec parse per connect. Frames are capped per connection, not globally, and response frames have no size cap here.
The StreamEnded invariant is not pinned
ChainCloseReason::StreamEnded's doc asserts that every connection this crate hands a host runs on the embedded light client, whose stream ends only when the connection is closed. True today: responses() is a select over smoldot's stream and an error channel closed only by close(), and the FFI has one constructor whose builder registers no chains, so resolution always falls to the catalog.
Nothing enforces it. JsonRpcConnection::responses promises nothing, ws.rs documents the opposite behaviour, and no test or cfg prevents a ws-backed source becoming FFI-reachable.
Smaller
send on a closed connection returns early with no synthetic error frame, so a consumer correlating by id waits forever. LightConnection::refuse exists for this case.
ChainCloseReason::ListenerFailed echoes the ChainProviderError a listener threw, so BadGenesis surfaces as "genesis hash must be 32 bytes" in a close reason.
- The battery's signing and pairing phases need a personhood-ring member, and running them rewrites
explorer/diagnosis-reports/spa/signing-host-cli.md with failure rows.
truapi-provider's UniFFI boundary has gaps that are not reachable by a shipped consumer yet: there is no@parity/ios-providerrelease, the release allowlist excludes it, the Maven publication is local-only, and the npm builds are wasm wheremod ffiis compiled out.Android has no working checksum guard
Generated Kotlin puts the checksum comparisons in
object IntegrityCheckingUniffiLiband registers the callback vtable in a separateobject UniffiLib. Kotlinobjectinit is lazy, and the only reference to the first isuniffiEnsureInitialized(), which nothing calls. iOS runs the equivalent guard frommakeRustCallbefore the vtable is installed.Consequence: a stale
.sowith fresh bindings is undetected on Android. Callback arity can change without the C symbol name changing, so the linker does not catch it either, and the result is a wrong-slotRustBufferread. The Android README now says the guard is iOS-only; the guard itself is generated, so the fix is upstream.A
ChainCloseReasonvariant addition is not detected at runtimeA uniffi enum's type id comes from its module path and name, not its variants, so adding a variant does not move
uniffi_truapi_provider_checksum_method_chainmessagelistener_on_closed. Old bindings pass the integrity check, then fail inFfiConverterTypeChainCloseReason.readwithunexpectedEnumCase, which becomesChainProviderError::Listenerand is only logged. The host'son_closednever fires.Both READMEs now say the
@unknown default/elseguidance is source compatibility only. What is still open is the process.No Kotlin gate for the provider
make android-checkcovers:truapi-hostonly, and no workflow invokes Gradle for:truapi-provider. A naive gate would pass vacuously:build.gradle.ktsusesjava.srcDirs("src/main/kotlin")whilemake provider-kotlinwrites to the gitignoredsrc/main/kotlin/generated/, so the module compiles zero files without a prior generation.provider-android-checkthat generates then compiles, failing on an empty source settracingis compiled out of the shipped native artifactmod loggingis#[cfg(all(feature = "js", target_arch = "wasm32"))]and provider builds use--no-default-features --features uniffi, so no subscriber can be installed andwarn!fields are never formatted. Every diagnostic inffi.rsis invisible on device. The unbounded reason strings passed towarn!are also oneset_global_defaultaway from being live, andtruapi-servercalls it at native host boot.Uncapped resources
The bound on a close reason is 256 Unicode scalar values, which both READMEs now state along with what that means per platform. It has no byte ceiling, so a reason can still reach 1024 bytes.
No cap on concurrent connections. Each costs a pump-thread stack (2.0 MiB measured, Darwin arm64), an
add_chain, a 1024-slot frame channel, and about 1.1 MiB of transient chain-spec parse perconnect. Frames are capped per connection, not globally, and response frames have no size cap here.The
StreamEndedinvariant is not pinnedChainCloseReason::StreamEnded's doc asserts that every connection this crate hands a host runs on the embedded light client, whose stream ends only when the connection is closed. True today:responses()is aselectover smoldot's stream and an error channel closed only byclose(), and the FFI has one constructor whose builder registers no chains, so resolution always falls to the catalog.Nothing enforces it.
JsonRpcConnection::responsespromises nothing,ws.rsdocuments the opposite behaviour, and no test orcfgprevents aws-backed source becoming FFI-reachable.LightConnectionor trait level, or test that a non-light source cannot reach the FFISmaller
sendon a closed connection returns early with no synthetic error frame, so a consumer correlating by id waits forever.LightConnection::refuseexists for this case.ChainCloseReason::ListenerFailedechoes theChainProviderErrora listener threw, soBadGenesissurfaces as "genesis hash must be 32 bytes" in a close reason.explorer/diagnosis-reports/spa/signing-host-cli.mdwith failure rows.