Skip to main content
The Rust SDK exposes a single Error enum with typed variants. The engine classifies RPC / relay responses into these variants internally so callers (and the retry loop) can match on them without string-sniffing.

The Error enum

Classification

Retry loops

Two loops run automatically; callers rarely need to retry themselves.

Outer (transact-level)

Up to opts.max_root_retries (default 40) iterations. Mapped via classify_iteration_error:
  • RootNotFound → drop LoopState.tree_state → refetch from RPC → re-prove
  • StaleProofState → drop LoopState.merkle_tree + set force_chain_indexing → re-prove
  • Any other → bubble up as FatalError

Inner (submit_direct)

Wrapped around the signed-tx send:
  • BlockhashExpired → refresh blockhash, recompile the v0 message, re-sign, re-send (up to 2 refreshes).
  • Relay(_) → retry the same wire bytes with exponential backoff + jitter (up to 3 attempts, base 500 ms).
  • RootNotFound → bubble up immediately for the outer loop.
  • Anything else → bubble up.
Tune the backoff base with opts.transport_backoff_base_ms (set to 0 in tests).

Matching errors in caller code

Most callers only need to distinguish “fatal” from “ask-again” cases:
Most Error variants are non-exhaustive-friendly (#[non_exhaustive] on the enum), so caller code should include a default arm.

Debug tracing

Dynamic error bodies don’t live inside the typed Error to keep the variant set stable. They’re logged via tracing at debug level with target = "cloak_sdk::relay" or target = "cloak_sdk::rpc":
Run with RUST_LOG=cloak_sdk=debug to see the full RPC / relay bodies that led to each classification.

Common failure modes