Stoffel Rust SDK
The Rust SDK is the library entry point for embedding Stoffel-Lang compilation, bytecode loading, VM execution, and MPC participant configuration in Rust apps.
Getting Started
Add the crate and import the prelude:
= "=0.1.3"
use *;
let result = compile?
.with_inputs
.execute_clear?;
assert_eq!;
assert_eq!;
# Ok::
For local MPC smoke runs, use the same builder and call execute_local().await.
This starts real localhost VM parties through stoffel-vm's local coordinator
runner when a built stoffel-run binary is available:
use *;
# async
Current Execution Modes
-
execute_clear()runs non-secret Stoffel programs through the embeddedstoffel-vmand is intended for fast local development of clear logic. -
execute_local()usesstoffel-vm's real local coordinator runner for local HoneyBadger and AVSS smoke runs. Buildstoffel-runfirst withcargo build -p stoffel-vm-runner --bin stoffel-run, or setSTOFFEL_RUN_BINorlocal_runner_path(...). Relative runner paths are resolved from the process working directory first and then from the workspace root, sotarget/debug/stoffel-runworks for normal workspace runs. Namedwith_inputsvalues are adapted into local coordinator client input for source, file, and loaded-bytecode programs; source/file functions returning asecretvalue are opened by the generated wrapper soexecute_local()returns clear SDK values. Programs that explicitly read fromClientStore.take_sharecan still receive local coordinator client input throughwith_client_inputor replace the full local client input set throughwith_client_inputs. AVSS local coordinator execution is wired for no-input programs and BLS12-381 local client input; other AVSS curves still reject local client input at the SDK boundary until the lower runner supports them. -
runtime.local_network()exposes the same coordinator-backed local run as a small builder for advanced local smoke tests that need a custom entrypoint, timeout, or runner binary path:# use Duration; # use *; # async -
Consensus ordering/gate/error types are re-exported from
stoffel-networking. The SDK does not duplicate the digest exchange or networking protocol. When deployment code obtains aVerifiedOrderingfromstoffel-networking, attach it withClientBuilder::with_verified_ordering(...)orServerBuilder::with_verified_ordering(...)so SDK participant summaries andclient.verify_ordering().awaitcan expose that consensus evidence. -
NetworkConfigvalidates PRD-style TOML and fallibly converts intostoffel-networking'sQuicNetworkConfigfor code that needs direct transport setup. -
MpcConfig::to_vm_topology(party_id)delegates session topology validation tostoffel-vm. -
MpcBackend::minimum_reconstruction_shares(t)reports backend-specific reconstruction requirements: HoneyBadger uses2 * t + 1, AVSS usest + 1. This is separate from Byzantine party-count validation (n >= 4 * t + 1). -
StoffelClient::connectopens real QUIC client connections throughstoffel-networking. For programs withClientStoremetadata,runtime.offchain_client_config(slot)derives the typed client IO settings from the compiled program and MPC runtime; callers then provide coordinator address, node RPC addresses, timestamp, and client identity material before callingclient.run_typed(...),client.submit_typed(...),client.run(...), orclient.submit(...). Server launch for those programs usesServerBuilder::offchain_coordinator(...)to pass the coordinator address, node RPC bind address, party identity files, timestamp, and expected client certificates through tostoffel-run. -
AVSS protocol operations are owned by
stoffel-vmandmpc-protocols. The SDK exposes the intended API boundary but does not implement an in-memory AVSS substitute. Local BLS12-381 AVSS programs delegate through the realstoffel-runcoordinator path:use *; # asyncPrograms that read
ClientStore.take_sharecan also receive BLS12-381 AVSS local client input throughwith_client_input.When deployment/server code already owns a live BLS12-381
stoffel-vm::net::avss_engine::Bls12381AvssMpcEngine, wrap it withAvssEngine::from_bls12381_engine(...)and attach it withServerBuilder::with_avss_engine(...)to use the SDK AVSS methods. The builder validates that the engine curve matches the selected AVSS backend. A configuredStoffelServerthat has not been started does not fabricate an engine;engine.is_live()reports whether calls will delegate to a VM engine.AVSS/Feldman share payloads can still be represented at the SDK boundary and inspected without running protocol logic:
use *; let share = feldman; assert!; assert_eq!; assert_eq!;
Typed Client IO Bindings
Programs that use ClientStore carry ordered input and output types in the
.stflb manifest. Generate Rust bindings from that exact bytecode in your app's
build.rs to get compile-time input/output structs for the program you will
execute:
include!;
# async
Generated bindings use Client{slot}Inputs and Client{slot}Outputs with
ordered fields such as input_0 and output_0. Integers map to i64, boolean
secret integers map to bool, and fixed-point shares map to f64. The SDK also
validates the generated type shape against the loaded program manifest before
submitting to the off-chain coordinator.
Core Builders
Use Stoffel as the main entry point:
let runtime = compile_file?
.parties
.threshold
.avss
.build?;
# Ok::
Compiled runtimes can be reused with different local inputs without compiling again:
let runtime = compile?
.build?;
let result = runtime
.with_inputs
.execute_clear?;
assert_eq!;
# Ok::
Use NetworkConfig when deploying servers from TOML:
let config = builder
.party_id
.bind_address
.expected_parties
.expected_clients
.peers
.threshold
.honeybadger
.consensus_timeout
.preprocessing
.build?;
config.validate_server_addresses?;
let server = builder.network_config.build?;
let client = builder.network_config.build?;
# Ok::
For a local or staging cluster where all party addresses are known up front, derive one validated config per party from a single deployment plan:
use Duration;
use *;
let deployment = builder
.expected_clients
.threshold
.honeybadger
.consensus_timeout
.preprocessing
.build?;
let server = builder
.network_deployment
.build?;
let client = builder
.network_deployment
.build?;
# let _ = ;
# Ok::
Use deployment.save_toml_files("deploy/config")? to write
party-0.toml, party-1.toml, and so on for operators or process managers.
Compiled runtimes can attach program metadata to the same deployment plan:
use *;
let deployment = builder
.build?;
let runtime = compile?
.build?;
let server_builders = runtime.servers_for_deployment;
let party_zero = server_builders.clone.build?;
let client = runtime.client_for_deployment.build?;
assert!;
assert!;
# Ok::
When a NetworkConfig is attached to Stoffel, its [network] and [mpc]
sections are authoritative for runtime parties, threshold, and backend.
This keeps deployment TOML from drifting away from the compiled program
metadata. If the compiled program declares ClientStore inputs, the SDK also
validates that expected_clients covers the declared client slots.
Preprocessing triples and random_shares must both be greater than zero.
Use NetworkConfig::to_mpc_config(instance_id) when an application needs the
same runtime MPC settings without compiling a program.
Use network_config.summary()? for a serializable deployment summary that
captures party/client counts, peer count, backend, threshold, reconstruction
shares, and preprocessing sizes.
Built runtimes expose the same status payloads through
runtime.mpc_summary()? and runtime.network_summary()?. Use
runtime.summary()? for a serializable status payload that combines program,
MPC, network, and local input configuration.
runtime.client() reuses the compiled program metadata and, when network
config is present, pre-populates the client builder with the configured server
addresses in party-id order. That client path requires complete server
addresses for every party; call NetworkConfig::validate_server_addresses()
when validating deployment TOML up front.
runtime.server(party_id) also uses the attached network config, and fails
fast if party_id does not match the config's [network].party_id.
For deployment plans with all party addresses available, use
runtime.servers_for_deployment(&deployment) and
runtime.client_for_deployment(&deployment) to attach the compiled program
while reusing the validated server addresses in party-id order.
StoffelClient::builder().network_config_file(path) and
StoffelServer::builder(party_id).network_config_file(path) provide the same
validation for direct participant construction. Direct server builders also
validate that expected_clients covers the attached program's declared
ClientStore slots.
Advanced applications that already have bytecode/program deployment handled
elsewhere can construct participants without compiling through
Stoffel::server(party_id) and Stoffel::client(). These are thin aliases for
the server and client builders; live network operations still require the
lower-level networking and VM integration rather than SDK-side simulation.
Both builders expose configured_* accessors so deployment code can inspect
addresses, backend selection, preprocessing settings, and attached program
metadata before consuming the builder with build().
Server builders accept either additive .peer(...) calls or batch
.peers([...]) / .with_peers(&[...]) replacement, matching
NetworkConfigBuilder.
Client builders likewise accept additive .server(...) calls or replacement
.servers([...]) / .with_servers(&[...]) configuration.
Built servers expose server.summary() for a serializable operational snapshot
containing party identity, bind address, peer count, backend, preprocessing,
state, readiness, health, and whether an AVSS engine has been configured.
Built clients expose client.summary() for a serializable status snapshot
containing client identity, server count, attached-program flag, and state.
Programs that use ClientStore expose their required local coordinator inputs
through Program metadata:
let runtime = compile?
.parties
.threshold
.build?;
let client = runtime.program.client.expect;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
runtime.program.validate_expected_clients?;
let client_values = ;
runtime.program.validate_client_inputs?;
runtime.clone.with_client_input.validate_client_inputs?;
compile?
.parties
.threshold
.with_client_input
.validate_client_inputs?;
# Ok::
Program metadata also includes function iteration and bytecode summary helpers
such as function_names(), total_instruction_count(), and
total_register_count() for tooling and diagnostics.
Use runtime.program().summary() for a serializable view of function counts,
function names, bytecode backend, and ClientStore input/output metadata.
For CLI-compatible bytecode artifacts, use runtime.to_bytecode() or
runtime.save_bytecode("program.stflb"); these delegate to the underlying
Program serialization. Use runtime.bytecode_summary()? for a
serde-friendly artifact summary that includes byte length and program metadata.
for function in runtime.program.functions
# Ok::
Value and the small SDK crypto wrapper types (Share, PublicKey,
FieldElement, and GroupElement) implement serde so application payloads can
be persisted or passed through existing API layers without conversion glue.
For typed output extraction, Value supports exact fallible conversions such
as i64::try_from(value) and String::try_from(value) in addition to the
borrowed as_* helpers.
Use value.summary() for a serializable, shallow status view containing the
value kind plus byte length or item count when applicable.
NetworkConfig::to_quic_config() and NetworkConfig::to_quic_manager() bridge
to stoffel-networking for the transport settings that QuicNetworkConfig
owns, including expected party/client counts and consensus timeout. Bind
addresses and static peer maps remain in the SDK/server configuration layer
because the lower-level QUIC config does not model those fields.
Loaded bytecode keeps its own MPC backend metadata. If no backend or network config is supplied, the SDK infers the runtime backend from the bytecode. If a backend or network config is supplied, it must match the bytecode metadata.
MpcConfig validates Byzantine safety with n >= 4 * t + 1. Reconstruction
share requirements are backend-specific: HoneyBadger uses 2 * t + 1, while
AVSS uses t + 1. Use MpcConfig::minimum_parties_for_threshold(t) and
MpcConfig::maximum_threshold_for_parties(n) when deriving deployment sizes.
Use mpc_config.summary()? for a serializable deployment summary that includes
party count, backend, Byzantine limits, and backend-specific reconstruction
shares.
MpcConfig and MpcBackend implement serde for applications that persist SDK
runtime settings directly; backends serialize as the same readable strings used
in network TOML, such as honeybadger and avss:ed25519.
The Backend trait and typed HoneyBadgerBackend/AvssBackend helpers expose
protocol identity for builder/configuration code; protocol execution remains in
the VM and MPC protocol crates.
Observability
SDK operations create tracing spans on async lifecycle and protocol-boundary
methods. Applications that do not already install a subscriber can use the SDK
helper:
use *;
use Level;
builder
.max_level
.ansi
.install?;
# Ok::
Use TracingConfig::summary() for a serde-friendly status payload:
let config = builder
.service_name
.ansi
.build;
config.validate?;
let summary = config.summary;
assert_eq!;
# Ok::
For OpenTelemetry traces during development, install the stdout exporter and hold the returned guard until shutdown:
use *;
use Level;
let otel = builder
.service_name
.max_level
.install_opentelemetry_stdout?;
otel.shutdown?;
# Ok::
Servers expose lightweight metrics snapshots and health/readiness state:
let server = builder
.bind
.peer
.with_preprocessing
.build?;
server.metrics.record_connected_peers;
server.metrics.record_preprocessing_remaining;
server.metrics.record_computation_latency_ms;
server.metrics.record_computation_latency_ms;
let snapshot = server.metrics.snapshot;
assert_eq!;
assert_eq!;
assert_eq!;
assert_eq!;
# Ok::
HealthStatus and ServerMetricsSnapshot implement serde for health endpoints
and operator dashboards. HealthStatus also implements Display, producing
compact strings such as healthy or unhealthy: server has been shut down.
Lifecycle enums (ClientState, ServerState, and ComputationStatus) also
implement serde and FromStr with readable snake-case values for status APIs.
Clients can establish real QUIC transport connections, and programs with
ClientStore metadata can submit typed inputs through the configured off-chain
coordinator and node RPC endpoints:
# use Duration;
# use *;
# async
Clients validate named computations before handing work to live networking:
client.run_function("name", inputs).await and
client.submit_function("name", inputs).await both check attached program
metadata. ClientStore submissions additionally validate the program's typed
client slot metadata before using the configured off-chain coordinator and node
RPC endpoints.
Computation handles expose stable local state for live client integrations and tests. Pending handles do not synthesize network results:
# async
Errors expose categories for recovery logic without string matching:
let error = Preprocessing;
assert_eq!;
assert_eq!;
assert!;
assert!;
assert_eq!;
ErrorCategory implements serde and FromStr using the same strings returned
by as_str(), which keeps structured error summaries stable across logs,
status APIs, and persisted diagnostics.
Core Crate Boundaries
The SDK is intentionally thin around protocol-owned functionality:
- Compilation delegates to
stoffellang. - Clear bytecode execution delegates to
stoffel-vm. - MPC session topology delegates to
stoffel-vm. - Consensus gates, verified ordering, network errors, and QUIC types are
re-exported from
stoffel-networking. - Off-chain coordinator types are re-exported from
stoffel-mpc-coordinator. - AVSS and HoneyBadger protocol internals remain in
stoffel-vmandmpc-protocols.
This keeps the SDK ergonomic without introducing redundant protocol, networking, compiler, VM, or coordinator implementations.
The crate root uses #![forbid(unsafe_code)]; protocol crates remain
responsible for any lower-level cryptographic or networking internals.
Release Readiness
The SDK is published to crates.io from the stoffel-rust-sdk-vX.Y.Z tag
workflow. Its internal Stoffel dependencies carry both an exact version and a
workspace path: local builds use the path, while Cargo packages the exact
registry requirement.
Publish a coordinated release in dependency order so the SDK dry run can resolve every exact requirement from crates.io:
stoffel-vm-typesstoffellangstoffel-vmstoffel-vm-runnerstoffel-bindgenstoffel-rust-sdk
After the upstream versions are available, validate the SDK package with the same locked dry run used by the release workflow:
Examples
Run the examples with:
network_config demonstrates deriving one config per party with
NetworkDeployment and writing party-*.toml files for operators.
The local MPC examples use the real localhost coordinator/party runner. Build the VM runner first, then run them:
The real coordinator-backed tests are marked ignored because they spawn local processes. Run them explicitly when validating the local MPC path:
The common SDK verification aliases are:
RUSTDOCFLAGS='-D warnings'
Those aliases expand to: