stoffel-rust-sdk 0.1.3

Rust SDK for building Stoffel MPC applications
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
# 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:

```toml
stoffel-rust-sdk = "=0.1.3"
```

```rust
use stoffel::prelude::*;

let result = Stoffel::compile(
    "def main(a: int64, b: int64) -> int64:\n  return a + b",
)?
.with_inputs(&[("a", 42_i64), ("b", 58_i64)])
.execute_clear()?;

assert_eq!(result, vec![Value::I64(100)]);
assert_eq!(result[0].as_i64(), Some(100));
# Ok::<(), stoffel::Error>(())
```

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:

```rust
use stoffel::prelude::*;

# async fn example() -> stoffel::Result<()> {
let result = Stoffel::compile(
    "def main(a: secret int64, b: secret int64) -> secret int64:\n  return a + b",
)?
.parties(5)
.threshold(1)
.honeybadger()
.local_runner_path("target/debug/stoffel-run")
.with_inputs(&[("a", 42_i64), ("b", 58_i64)])
.execute_local()
.await?;

assert_eq!(result, vec![Value::I64(100)]);
# Ok(())
# }
```

## Current Execution Modes

- `execute_clear()` runs non-secret Stoffel programs through the embedded
  `stoffel-vm` and is intended for fast local development of clear logic.
- `execute_local()` uses `stoffel-vm`'s real local coordinator runner for
  local HoneyBadger and AVSS smoke runs. Build
  `stoffel-run` first with `cargo build -p stoffel-vm-runner --bin stoffel-run`, or
  set `STOFFEL_RUN_BIN` or `local_runner_path(...)`. Relative runner paths are
  resolved from the process working directory first and then from the workspace
  root, so `target/debug/stoffel-run` works for normal workspace runs. Named
  `with_inputs` values are adapted into local coordinator client input for
  source, file, and loaded-bytecode programs; source/file functions returning a
  `secret` value are opened by the generated wrapper so `execute_local()`
  returns clear SDK values. Programs that explicitly read from
  `ClientStore.take_share` can still receive local coordinator client input
  through `with_client_input` or replace the full local client input set through
  `with_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:

  ```rust
  # use std::time::Duration;
  # use stoffel::prelude::*;
  # async fn example(runtime: StoffelRuntime) -> stoffel::Result<()> {
  let result = runtime
      .local_network()
      .entry("main")
      .runner_path("target/debug/stoffel-run")
      .timeout(Duration::from_secs(180))
      .run()
      .await?;
  # let _ = result;
  # Ok(())
  # }
  ```
- 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 a `VerifiedOrdering` from `stoffel-networking`,
  attach it with `ClientBuilder::with_verified_ordering(...)` or
  `ServerBuilder::with_verified_ordering(...)` so SDK participant summaries and
  `client.verify_ordering().await` can expose that consensus evidence.
- `NetworkConfig` validates PRD-style TOML and fallibly converts into
  `stoffel-networking`'s `QuicNetworkConfig` for code that needs direct
  transport setup.
- `MpcConfig::to_vm_topology(party_id)` delegates session topology validation to
  `stoffel-vm`.
- `MpcBackend::minimum_reconstruction_shares(t)` reports backend-specific
  reconstruction requirements: HoneyBadger uses `2 * t + 1`, AVSS uses
  `t + 1`. This is separate from Byzantine party-count validation
  (`n >= 4 * t + 1`).
- `StoffelClient::connect` opens real QUIC client connections through
  `stoffel-networking`. For programs with `ClientStore` metadata,
  `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
  calling `client.run_typed(...)`, `client.submit_typed(...)`, `client.run(...)`,
  or `client.submit(...)`. Server launch for those programs uses
  `ServerBuilder::offchain_coordinator(...)` to pass the
  coordinator address, node RPC bind address, party identity files, timestamp,
  and expected client certificates through to `stoffel-run`.
- AVSS protocol operations are owned by `stoffel-vm` and `mpc-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 real
  `stoffel-run` coordinator path:

  ```rust
  use stoffel::prelude::*;

  # async fn example() -> stoffel::Result<()> {
  let result = Stoffel::compile("def main() -> int64:\n  return 7")?
      .avss(Curve::Bls12_381)
      .local_runner_path("target/debug/stoffel-run")
      .execute_local()
      .await?;
  # let _ = result;
  # Ok(())
  # }
  ```

  Programs that read `ClientStore.take_share` can also receive BLS12-381 AVSS
  local client input through `with_client_input`.

  When deployment/server code already owns a live BLS12-381
  `stoffel-vm::net::avss_engine::Bls12381AvssMpcEngine`, wrap it with
  `AvssEngine::from_bls12381_engine(...)` and attach it with
  `ServerBuilder::with_avss_engine(...)` to use the SDK AVSS methods. The
  builder validates that the engine curve matches the selected AVSS backend. A
  configured `StoffelServer` that 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:

  ```rust
  use stoffel::prelude::*;

  let share = Share::feldman("threshold-key", [0x01, 0x02], [[0x10], [0x20]]);
  assert!(share.is_feldman());
  assert_eq!(share.commitment_count(), 2);
  assert_eq!(
      share.public_key(),
      Some(PublicKey::new("threshold-key", [0x10]))
  );
  ```

## 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:

```rust
fn main() -> Result<(), Box<dyn std::error::Error>> {
    let out_dir = std::env::var("OUT_DIR")?;
    stoffel_bindgen::generate_bindings(
        "program.stflb",
        format!("{out_dir}/stoffel_bindings.rs"),
    )?;
    Ok(())
}
```

```rust
include!(concat!(env!("OUT_DIR"), "/stoffel_bindings.rs"));

# async fn example(client: stoffel::StoffelClient) -> stoffel::Result<()> {
let outputs: Client0Outputs = client
    .run_typed(Client0Inputs {
        input_0: 42_i64,
    })
    .await?;
# let _ = outputs;
# Ok(())
# }
```

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:

```rust
let runtime = Stoffel::compile_file("program.stfl")?
    .parties(9)
    .threshold(2)
    .avss(Curve::Bls12_381)
    .build()?;
# Ok::<(), stoffel::Error>(())
```

Compiled runtimes can be reused with different local inputs without compiling
again:

```rust
let runtime = Stoffel::compile("def main(a: int64, b: int64) -> int64:\n  return a + b")?
    .build()?;

let result = runtime
    .with_inputs(&[("a", 40_i64), ("b", 2_i64)])
    .execute_clear()?;
assert_eq!(result, vec![Value::I64(42)]);
# Ok::<(), stoffel::Error>(())
```

Use `NetworkConfig` when deploying servers from TOML:

```rust
let config = NetworkConfig::builder()
    .party_id(0)
    .bind_address("127.0.0.1:19200")
    .expected_parties(5)
    .expected_clients(1)
    .peers([
        (1, "127.0.0.1:19201"),
        (2, "127.0.0.1:19202"),
        (3, "127.0.0.1:19203"),
        (4, "127.0.0.1:19204"),
    ])
    .threshold(1)
    .honeybadger()
    .consensus_timeout(std::time::Duration::from_secs(60))
    .preprocessing(1000, 500)
    .build()?;

config.validate_server_addresses()?;
let server = StoffelServer::builder(0).network_config(&config).build()?;
let client = StoffelClient::builder().network_config(&config).build()?;
# Ok::<(), stoffel::Error>(())
```

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:

```rust
use std::time::Duration;
use stoffel::prelude::*;

let deployment = NetworkDeployment::builder([
    "127.0.0.1:19200",
    "127.0.0.1:19201",
    "127.0.0.1:19202",
    "127.0.0.1:19203",
    "127.0.0.1:19204",
])
.expected_clients(1)
.threshold(1)
.honeybadger()
.consensus_timeout(Duration::from_secs(60))
.preprocessing(1000, 500)
.build()?;

let server = StoffelServer::builder(0)
    .network_deployment(&deployment)
    .build()?;
let client = StoffelClient::builder()
    .network_deployment(&deployment)
    .build()?;
# let _ = (server, client);
# Ok::<(), stoffel::Error>(())
```

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:

```rust
use stoffel::prelude::*;

let deployment = NetworkDeployment::builder([
    "127.0.0.1:19200",
    "127.0.0.1:19201",
    "127.0.0.1:19202",
    "127.0.0.1:19203",
    "127.0.0.1:19204",
])
.build()?;

let runtime = Stoffel::compile(
    "def main(a: secret int64, b: secret int64) -> secret int64:\n  return a + b",
)?
.build()?;

let server_builders = runtime.servers_for_deployment(&deployment);
let party_zero = server_builders[0].clone().build()?;
let client = runtime.client_for_deployment(&deployment).build()?;
assert!(party_zero.program().is_some());
assert!(client.has_program());
# Ok::<(), stoffel::Error>(())
```

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:

```rust
let runtime = Stoffel::compile(
    "def main() -> int64:\n  var share = ClientStore.take_share(0, 0)\n  var opened: int64 = share.open()\n  return opened",
)?
.parties(5)
.threshold(1)
.build()?;

let client = runtime.program().client(0).expect("client slot 0");
assert_eq!(client.input_count(), 1);
assert_eq!(runtime.program().client_slots().collect::<Vec<_>>(), vec![0]);
assert_eq!(runtime.program().total_client_input_count(), 1);
assert_eq!(runtime.program().minimum_expected_clients(), 1);
runtime.program().validate_expected_clients(1)?;
let client_values = [Value::I64(42)];
runtime.program().validate_client_inputs(&[(0, client_values.as_slice())])?;
runtime.clone().with_client_input(0, &[42_i64]).validate_client_inputs()?;
Stoffel::compile(
    "def main() -> int64:\n  var share = ClientStore.take_share(0, 0)\n  return share.open()",
)?
.parties(5)
.threshold(1)
.with_client_input(0, &[42_i64])
.validate_client_inputs()?;
# Ok::<(), stoffel::Error>(())
```

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.

```rust
for function in runtime.program().functions() {
    println!("{function}");
}
# Ok::<(), stoffel::Error>(())
```

`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:

```rust
use stoffel::prelude::*;
use tracing::Level;

TracingConfig::builder()
    .max_level(Level::INFO)
    .ansi(false)
    .install()?;
# Ok::<(), stoffel::Error>(())
```

Use `TracingConfig::summary()` for a serde-friendly status payload:

```rust
let config = TracingConfig::builder()
    .service_name("my-stoffel-app")
    .ansi(false)
    .build();
config.validate()?;
let summary = config.summary();
assert_eq!(summary.service_name, "my-stoffel-app");
# Ok::<(), stoffel::Error>(())
```

For OpenTelemetry traces during development, install the stdout exporter and
hold the returned guard until shutdown:

```rust
use stoffel::prelude::*;
use tracing::Level;

let otel = TracingConfig::builder()
    .service_name("my-stoffel-app")
    .max_level(Level::INFO)
    .install_opentelemetry_stdout()?;

otel.shutdown()?;
# Ok::<(), stoffel::Error>(())
```

Servers expose lightweight metrics snapshots and health/readiness state:

```rust
let server = StoffelServer::builder(0)
    .bind("127.0.0.1:19200")
    .peer(1, "127.0.0.1:19201")
    .with_preprocessing(100, 50)
    .build()?;

server.metrics().record_connected_peers(4);
server.metrics().record_preprocessing_remaining(80, 40);
server.metrics().record_computation_latency_ms(25);
server.metrics().record_computation_latency_ms(35);
let snapshot = server.metrics().snapshot();
assert_eq!(snapshot.connected_peers, 4);
assert_eq!(snapshot.preprocessing_random_shares_remaining, 40);
assert_eq!(server.metrics().computation_latency_average_ms(), Some(30));
assert_eq!(snapshot.computation_latency_average_ms(), Some(30));
# Ok::<(), stoffel::Error>(())
```

`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:

```rust
# use std::time::Duration;
# use stoffel::prelude::*;
# async fn example() -> stoffel::Result<()> {
let client = StoffelClient::builder()
    .server("127.0.0.1:19200")
    .connection_timeout(Duration::from_secs(10))
    .connect()
    .await?;

assert_eq!(client.state(), ClientState::Connected);
assert!(client.is_connected());
assert!(client.transport_client_id().is_some());
# Ok(())
# }
```

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:

```rust
# async fn example() -> stoffel::Result<()> {
let handle = ComputationHandle::pending();
assert_eq!(handle.status(), ComputationStatus::Pending);
assert_eq!(handle.status().to_string(), "pending");
assert!(handle.is_pending());
assert_eq!(handle.summary().result_count, 0);
assert!(matches!(
    handle.clone().await_result().await,
    Err(Error::Unsupported(_))
));

handle.cancel();
assert_eq!(handle.status(), ComputationStatus::Cancelled);
assert!(handle.is_cancelled());

let completed = ComputationHandle::completed(vec![Value::I64(100)]);
assert!(completed.is_completed());
assert_eq!(completed.summary().result_count, 1);
assert_eq!(completed.await_result().await?, vec![Value::I64(100)]);
# Ok(())
# }
```

Errors expose categories for recovery logic without string matching:

```rust
let error = Error::Preprocessing("not enough triples".to_owned());
assert_eq!(error.category(), ErrorCategory::Preprocessing);
assert_eq!(error.category().as_str(), "preprocessing");
assert!(ErrorCategory::Preprocessing.is_recoverable());
assert!(error.is_recoverable());
assert_eq!(
    error.recovery_hint(),
    Some("ensure each server has enough triples and random shares before retrying")
);
```

`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-vm` and
  `mpc-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:

1. `stoffel-vm-types`
2. `stoffellang`
3. `stoffel-vm`
4. `stoffel-vm-runner`
5. `stoffel-bindgen`
6. `stoffel-rust-sdk`

After the upstream versions are available, validate the SDK package with the
same locked dry run used by the release workflow:

```sh
cargo publish -p stoffel-rust-sdk --locked --dry-run
cargo package -p stoffel-rust-sdk --locked
```

## Examples

Run the examples with:

```sh
cargo run -p stoffel-rust-sdk --example quickstart
cargo run -p stoffel-rust-sdk --example local_clear
cargo run -p stoffel-rust-sdk --example bytecode_roundtrip
cargo run -p stoffel-rust-sdk --example client_server
cargo run -p stoffel-rust-sdk --example network_config
cargo run -p stoffel-rust-sdk --example observability
cargo run -p stoffel-rust-sdk --example avss
```

`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:

```sh
cargo build -p stoffel-vm-runner --bin stoffel-run
cargo run -p stoffel-rust-sdk --example quickstart_mpc
cargo run -p stoffel-rust-sdk --example local_mpc_client_input
cargo run -p stoffel-rust-sdk --example local_mpc_named_inputs
```

The real coordinator-backed tests are marked ignored because they spawn local
processes. Run them explicitly when validating the local MPC path:

```sh
cargo vm-local-e2e
cargo sdk-local-e2e
```

The common SDK verification aliases are:

```sh
cargo sdk-test
cargo sdk-local-e2e
cargo sdk-examples
RUSTDOCFLAGS='-D warnings' cargo sdk-doc
cargo sdk-clippy
cargo sdk-coverage-report
```

Those aliases expand to:

```sh
cargo test -p stoffel-vm --test local_coordinator_e2e -- --ignored --test-threads=1
cargo test -p stoffel-rust-sdk --test sdk_usage -- --ignored --test-threads=1
```