Skip to main content

stoffel/
lib.rs

1//! Reference Rust SDK for building Stoffel MPC applications.
2//!
3//! The SDK provides a small public entry point, [`Stoffel`], for compiling
4//! Stoffel-Lang programs, loading bytecode, configuring MPC runtime settings,
5//! executing local development programs, and constructing client/server
6//! participants.
7//!
8//! ```
9//! use stoffel::prelude::*;
10//!
11//! # fn main() -> stoffel::Result<()> {
12//! let result = Stoffel::compile(
13//!     "def main(a: int64, b: int64) -> int64:\n  return a + b",
14//! )?
15//! .with_inputs(&[("a", 42_i64), ("b", 58_i64)])
16//! .execute_clear()?;
17//!
18//! assert_eq!(result[0].as_i64(), Some(100));
19//! # Ok(())
20//! # }
21//! ```
22//!
23//! Local MPC development uses the same builder, but delegates to
24//! `stoffel-vm`'s real localhost coordinator runner and a built `stoffel-run`
25//! binary instead of simulating the protocol in the SDK:
26//!
27//! ```no_run
28//! use stoffel::prelude::*;
29//!
30//! # async fn example() -> stoffel::Result<()> {
31//! let result = Stoffel::compile(
32//!     "def main() -> int64:\n  var share = ClientStore.take_share(0, 0)\n  return share.open()",
33//! )?
34//! .parties(5)
35//! .threshold(1)
36//! .with_client_input(0, &[42_i64])
37//! .execute_local()
38//! .await?;
39//!
40//! assert_eq!(result[0].as_i64(), Some(42));
41//! # Ok(())
42//! # }
43//! ```
44
45#![forbid(unsafe_code)]
46
47pub mod backend;
48pub mod client;
49pub mod compiler;
50pub mod config;
51pub mod consensus;
52pub mod coordinator;
53pub mod docs;
54pub mod error;
55pub mod input_file;
56pub mod networking;
57pub mod observability;
58pub mod prelude;
59pub mod program;
60pub mod runtime;
61pub mod server;
62pub mod types;
63pub mod vm;
64
65use std::path::{Path, PathBuf};
66use std::time::Duration;
67
68pub use backend::{
69    avss::{AvssBackend, AvssEngine},
70    honeybadger::HoneyBadgerBackend,
71    Backend, MpcBackend,
72};
73pub use client::{
74    ClientBuilder, ClientState, ClientSummary, ComputationHandle, ComputationStatus,
75    ComputationSummary, OffChainClientConfig, OffChainClientConfigBuilder, StoffelClient,
76};
77pub use compiler::CompilationOptions;
78pub use config::{
79    Curve, MpcConfig, MpcConfigBuilder, MpcConfigSummary, MpcSection, NetworkConfig,
80    NetworkConfigBuilder, NetworkConfigSummary, NetworkDeployment, NetworkDeploymentBuilder,
81    NetworkSection, PreprocessingConfig,
82};
83pub use consensus::{ConsensusGate, NodePublicKey, VerifiedOrdering};
84pub use coordinator::{
85    Coordinator, OffChainCoordinator, OffChainCoordinatorClient, OffChainCoordinatorServer,
86    ShareBound,
87};
88pub use error::{ConsensusError, CoordinatorError, Error, ErrorCategory, NetworkError, Result};
89pub use input_file::{load_client_inputs_file, load_named_inputs_file};
90pub use networking::{NetworkManager, QuicNetworkConfig, QuicNetworkManager};
91pub use observability::{
92    init_tracing, HealthStatus, OpenTelemetryGuard, ServerMetrics, ServerMetricsSnapshot,
93    TracingConfig, TracingConfigBuilder, TracingConfigSummary,
94};
95pub use program::{
96    BytecodeSummary, ClientMetadata, ClientMetadataSummary, FunctionMetadata, FunctionSummary,
97    Program, ProgramSummary,
98};
99pub use runtime::{LocalNetworkBuilder, RuntimeSummary, StoffelRuntime};
100pub use server::{
101    OffChainServerConfig, OffChainServerConfigBuilder, ServerBuilder, ServerState, ServerSummary,
102    StoffelServer,
103};
104pub use stoffel_vm_types::compiled_binary::FunctionType;
105pub use stoffel_vm_types::core_types::ShareType;
106pub use types::{
107    ClientId, ClientInputValue, ClientOutputValue, ClientValueType, FieldElement,
108    GeneratedProgramManifest, GroupElement, MaskIndex, PartyId, ProgramArgs, PublicKey, Round,
109    Share, TypedClientInputs, TypedClientOutputs, Value, ValueSummary,
110};
111pub use vm::LocalClientOutput;
112
113#[derive(Debug, Clone)]
114enum ProgramSource {
115    Source { source: String, filename: String },
116    File { path: std::path::PathBuf },
117    Bytecode(Vec<u8>),
118    Program(Program),
119}
120
121/// Inputs accepted by [`Stoffel::load_program`].
122pub trait LoadableProgram {
123    fn load_program(self) -> Result<Program>;
124}
125
126impl LoadableProgram for Program {
127    fn load_program(self) -> Result<Program> {
128        Ok(self)
129    }
130}
131
132impl LoadableProgram for &Program {
133    fn load_program(self) -> Result<Program> {
134        Ok(self.clone())
135    }
136}
137
138impl LoadableProgram for &[u8] {
139    fn load_program(self) -> Result<Program> {
140        Program::from_bytecode(self)
141    }
142}
143
144impl LoadableProgram for Vec<u8> {
145    fn load_program(self) -> Result<Program> {
146        Program::from_bytecode(&self)
147    }
148}
149
150impl LoadableProgram for &Path {
151    fn load_program(self) -> Result<Program> {
152        Program::from_bytecode_file(self)
153    }
154}
155
156impl LoadableProgram for PathBuf {
157    fn load_program(self) -> Result<Program> {
158        Program::from_bytecode_file(self)
159    }
160}
161
162impl LoadableProgram for &PathBuf {
163    fn load_program(self) -> Result<Program> {
164        Program::from_bytecode_file(self)
165    }
166}
167
168/// SDK entry point and builder.
169#[derive(Debug, Clone)]
170pub struct Stoffel {
171    source: Option<ProgramSource>,
172    mpc_config: MpcConfig,
173    backend_explicit: bool,
174    network_config: Option<NetworkConfig>,
175    local_runner_path: Option<PathBuf>,
176    config_error: Option<String>,
177    inputs: Vec<(String, Value)>,
178    client_inputs: Vec<(u64, Vec<Value>)>,
179    expected_clients: Option<usize>,
180    client_output_counts: std::collections::HashMap<u64, u64>,
181    compiler_options: CompilationOptions,
182}
183
184impl Stoffel {
185    /// Compile a Stoffel-Lang source string.
186    pub fn compile(source: &str) -> Result<Self> {
187        Ok(Self::from_source(ProgramSource::Source {
188            source: source.to_owned(),
189            filename: "<sdk>".to_owned(),
190        }))
191    }
192
193    /// Compile a Stoffel-Lang file.
194    pub fn compile_file(path: impl AsRef<Path>) -> Result<Self> {
195        Ok(Self::from_source(ProgramSource::File {
196            path: path.as_ref().to_path_buf(),
197        }))
198    }
199
200    /// Load serialized Stoffel bytecode.
201    pub fn load(bytecode: &[u8]) -> Result<Self> {
202        Ok(Self::from_source(ProgramSource::Bytecode(
203            bytecode.to_vec(),
204        )))
205    }
206
207    /// Load serialized Stoffel bytecode from a file.
208    pub fn load_file(path: impl AsRef<Path>) -> Result<Self> {
209        let bytecode = std::fs::read(path)?;
210        Self::load(&bytecode)
211    }
212
213    /// Load an executable program directly from bytecode, a bytecode path, or
214    /// an already constructed [`Program`].
215    ///
216    /// ```
217    /// use stoffel::prelude::*;
218    ///
219    /// # fn main() -> stoffel::Result<()> {
220    /// let bytecode = Stoffel::compile(
221    ///     "def main(a: int64, b: int64) -> int64:\n  return a + b",
222    /// )?
223    /// .build()?
224    /// .to_bytecode()?;
225    ///
226    /// let program = Stoffel::load_program(bytecode)?;
227    /// let result = program.execute("main", (40_i64, 2_i64))?;
228    ///
229    /// assert_eq!(result[0].as_i64(), Some(42));
230    /// # Ok(())
231    /// # }
232    /// ```
233    pub fn load_program(program: impl LoadableProgram) -> Result<StoffelRuntime> {
234        Self::from_source(ProgramSource::Program(program.load_program()?)).build()
235    }
236
237    /// Set the number of MPC parties. Default is 5.
238    pub fn parties(mut self, n: usize) -> Self {
239        self.mpc_config.parties = n;
240        self
241    }
242
243    /// Set the Byzantine threshold. Default is 1.
244    pub fn threshold(mut self, t: usize) -> Self {
245        self.mpc_config.threshold = t;
246        self
247    }
248
249    /// Set the MPC instance identifier. Default is generated from process-local time.
250    pub fn instance_id(mut self, id: u64) -> Self {
251        self.mpc_config.instance_id = id;
252        self
253    }
254
255    /// Select an MPC backend. Default is HoneyBadger.
256    pub fn backend(mut self, backend: MpcBackend) -> Self {
257        self.mpc_config.backend = backend;
258        self.backend_explicit = true;
259        self
260    }
261
262    /// Select the backend and curve declared by generated program bindings.
263    pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
264        self.backend(M::BACKEND)
265    }
266
267    /// Select the HoneyBadger backend.
268    pub fn honeybadger(mut self) -> Self {
269        self.mpc_config.backend = MpcBackend::HoneyBadger;
270        self.backend_explicit = true;
271        self
272    }
273
274    /// Select the AVSS backend with the given curve.
275    pub fn avss(mut self, curve: Curve) -> Self {
276        self.mpc_config.backend = MpcBackend::Avss { curve };
277        self.backend_explicit = true;
278        self
279    }
280
281    /// Select the AVSS curve. This also selects the AVSS backend.
282    pub fn curve(mut self, curve: Curve) -> Self {
283        self.mpc_config.backend = MpcBackend::Avss { curve };
284        self.backend_explicit = true;
285        self
286    }
287
288    /// Enable or disable compiler optimizations.
289    pub fn optimize(mut self, enabled: bool) -> Self {
290        self.compiler_options.optimize = enabled;
291        self
292    }
293
294    /// Set the compiler optimization level (0-3).
295    pub fn optimization_level(mut self, level: u8) -> Self {
296        self.compiler_options.optimization_level = level;
297        if level > 0 {
298            self.compiler_options.optimize = true;
299        }
300        self
301    }
302
303    /// Print compiler intermediate representations while compiling.
304    pub fn print_ir(mut self, enabled: bool) -> Self {
305        self.compiler_options.print_ir = enabled;
306        self
307    }
308
309    /// Replace all Stoffel-Lang compiler options carried by this builder.
310    pub fn compiler_options(mut self, options: CompilationOptions) -> Self {
311        self.compiler_options = options;
312        self
313    }
314
315    /// Preserve additional named functions as executable entrypoints when
316    /// compiling source. This is useful for programs that expose more than
317    /// `main` through generated bindings.
318    pub fn entry_points<I, S>(mut self, entry_points: I) -> Self
319    where
320        I: IntoIterator<Item = S>,
321        S: Into<String>,
322    {
323        self.compiler_options.entry_points = entry_points.into_iter().map(Into::into).collect();
324        self
325    }
326
327    /// Attach named inputs for local execution.
328    pub fn with_inputs<V>(mut self, inputs: &[(&str, V)]) -> Self
329    where
330        V: Clone + Into<Value>,
331    {
332        self.inputs = inputs
333            .iter()
334            .map(|(name, value)| ((*name).to_owned(), value.clone().into()))
335            .collect();
336        self
337    }
338
339    /// Attach one named input for local execution.
340    pub fn with_input<V>(mut self, name: impl Into<String>, value: V) -> Self
341    where
342        V: Into<Value>,
343    {
344        self.inputs.push((name.into(), value.into()));
345        self
346    }
347
348    /// Replace named inputs with values loaded from a `.json`, `.csv`, or `.txt` file.
349    pub fn with_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
350        self.inputs = input_file::load_named_inputs_file(path)?;
351        Ok(self)
352    }
353
354    /// Attach one coordinator client input set for local networked execution.
355    ///
356    /// This is distinct from named function parameters. It feeds programs that
357    /// read secret client values through `ClientStore.take_share(client_slot, i)`.
358    pub fn with_client_input<V>(mut self, client_slot: u64, values: &[V]) -> Self
359    where
360        V: Clone + Into<Value>,
361    {
362        self.client_inputs.push((
363            client_slot,
364            values.iter().cloned().map(Into::into).collect(),
365        ));
366        self
367    }
368
369    /// Replace local ClientStore inputs with values loaded from a `.json`, `.csv`, or `.txt` file.
370    pub fn with_client_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
371        self.client_inputs = input_file::load_client_inputs_file(path)?;
372        Ok(self)
373    }
374
375    /// Replace all coordinator client input sets for local networked execution.
376    pub fn with_client_inputs<V>(mut self, inputs: &[(u64, &[V])]) -> Self
377    where
378        V: Clone + Into<Value>,
379    {
380        self.client_inputs = inputs
381            .iter()
382            .map(|(client_slot, values)| {
383                (
384                    *client_slot,
385                    values.iter().cloned().map(Into::into).collect(),
386                )
387            })
388            .collect();
389        self
390    }
391
392    /// Declare output-capable client slots `0..n-1` for local execution.
393    pub fn expected_output_clients(mut self, n: usize) -> Self {
394        self.expected_clients = Some(n);
395        self
396    }
397
398    /// Provide the number of output values a client receives via
399    /// `send_to_client`, for the local simulator. This is a **fallback**: when
400    /// the program statically declares the client's outputs (recorded in the
401    /// client-IO manifest), that count is used instead. Needed only when the
402    /// program sends to a slot the compiler cannot resolve statically.
403    pub fn client_output_count(mut self, client_slot: u64, count: u64) -> Self {
404        self.client_output_counts.insert(client_slot, count);
405        self
406    }
407
408    /// Attach explicit network configuration.
409    pub fn network_config(mut self, config: NetworkConfig) -> Self {
410        self.network_config = Some(config);
411        self
412    }
413
414    /// Load network configuration from a TOML file.
415    pub fn network_config_file(mut self, path: impl AsRef<Path>) -> Self {
416        match NetworkConfig::from_toml_file(path) {
417            Ok(config) => self.network_config = Some(config),
418            Err(error) => self.config_error = Some(error.to_string()),
419        }
420        self
421    }
422
423    /// Set the `stoffel-run` binary path used by local coordinator execution.
424    pub fn local_runner_path(mut self, path: impl AsRef<Path>) -> Self {
425        self.local_runner_path = Some(path.as_ref().to_path_buf());
426        self
427    }
428
429    /// Build a runtime from the configured program and MPC settings.
430    pub fn build(self) -> Result<StoffelRuntime> {
431        if let Some(error) = self.config_error {
432            return Err(Error::Configuration(error));
433        }
434        let mut mpc_config = self.mpc_config;
435        let mut backend_explicit = self.backend_explicit;
436        if let Some(config) = &self.network_config {
437            mpc_config = config.to_mpc_config(mpc_config.instance_id)?;
438            backend_explicit = true;
439        }
440        mpc_config.validate()?;
441
442        let source = self
443            .source
444            .ok_or_else(|| Error::Configuration("no program source configured".to_owned()))?;
445        let program = match source {
446            ProgramSource::Source { source, filename } => compiler::compile_source_with_options(
447                &source,
448                &filename,
449                mpc_config.backend,
450                self.compiler_options,
451            )?,
452            ProgramSource::File { path } => compiler::compile_file_with_options(
453                &path,
454                mpc_config.backend,
455                self.compiler_options,
456            )?,
457            ProgramSource::Bytecode(bytecode) => {
458                let program = Program::from_bytecode(&bytecode)?;
459                let bytecode_backend =
460                    backend_from_bytecode(program.bytecode_backend(), program.bytecode_curve())?;
461                if backend_explicit {
462                    validate_bytecode_backend(&program, mpc_config.backend)?;
463                } else {
464                    mpc_config.backend = bytecode_backend;
465                }
466                program
467            }
468            ProgramSource::Program(program) => {
469                let bytecode_backend =
470                    backend_from_bytecode(program.bytecode_backend(), program.bytecode_curve())?;
471                if backend_explicit {
472                    validate_bytecode_backend(&program, mpc_config.backend)?;
473                } else {
474                    mpc_config.backend = bytecode_backend;
475                }
476                program
477            }
478        };
479        validate_bytecode_backend(&program, mpc_config.backend)?;
480        if program.is_empty() {
481            return Err(Error::Configuration(
482                "program must contain at least one function".to_owned(),
483            ));
484        }
485        if let Some(expected_clients) = self.expected_clients {
486            if expected_clients == 0 {
487                return Err(Error::Configuration(
488                    "expected_clients must be greater than 0".to_owned(),
489                ));
490            }
491            program.validate_expected_clients(expected_clients)?;
492        }
493        if let Some(config) = &self.network_config {
494            validate_program_network_config(&program, config)?;
495        }
496        let mut runtime = StoffelRuntime::new(
497            program,
498            Some(mpc_config),
499            self.network_config,
500            self.local_runner_path,
501            self.inputs,
502            self.client_inputs,
503            self.expected_clients,
504        );
505        runtime.set_client_output_counts(self.client_output_counts);
506        Ok(runtime)
507    }
508
509    /// Validate attached coordinator client inputs against compiled program metadata.
510    pub fn validate_client_inputs(self) -> Result<()> {
511        let runtime = self.build()?;
512        runtime.validate_client_inputs()
513    }
514
515    /// Execute locally using real MPC nodes.
516    ///
517    /// This delegates to `stoffel-vm`'s local coordinator runner, which starts a
518    /// real localhost party mesh via `stoffel-run`. HoneyBadger supports
519    /// no-argument `ClientStore` entrypoints and named [`Self::with_inputs`]
520    /// values, which are adapted into local coordinator client input by a
521    /// generated bytecode wrapper. AVSS local execution supports no-input
522    /// programs and BLS12-381 local client input through the same lower runner.
523    #[tracing::instrument(skip_all)]
524    pub async fn execute_local(self) -> Result<Vec<Value>> {
525        self.execute_local_function("main").await
526    }
527
528    /// Execute a named function locally using real MPC nodes.
529    #[tracing::instrument(skip_all, fields(function = name))]
530    pub async fn execute_local_function(self, name: &str) -> Result<Vec<Value>> {
531        let (runtime, entry) = self.build_for_local_execution(name)?;
532        vm::execute_local(&runtime, &entry).await
533    }
534
535    /// Execute `main` locally and also return the program's printed output.
536    ///
537    /// Useful when the result a client cares about is emitted with `print`
538    /// (e.g. a 16-byte cipher block) rather than returned as a scalar — a
539    /// returned aggregate such as a `list` only comes back as an opaque handle.
540    pub async fn execute_local_capturing_output(self) -> Result<(Vec<Value>, String)> {
541        self.execute_local_function_capturing_output("main").await
542    }
543
544    /// Execute a named function locally and also return its printed output.
545    #[tracing::instrument(skip_all, fields(function = name))]
546    pub async fn execute_local_function_capturing_output(
547        self,
548        name: &str,
549    ) -> Result<(Vec<Value>, String)> {
550        let (runtime, entry) = self.build_for_local_execution(name)?;
551        let (values, program_output, _client_outputs) =
552            vm::execute_local_capturing_with_options(&runtime, &entry, Default::default()).await?;
553        Ok((values, program_output))
554    }
555
556    /// Execute `main` locally and return the **client outputs** delivered via
557    /// `send_to_client`, reconstructed by the off-chain client (alongside the
558    /// VM return values). This is the value a client actually receives — not a
559    /// public reveal to the compute nodes.
560    pub async fn execute_local_capturing_client_outputs(
561        self,
562    ) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
563        self.execute_local_function_capturing_client_outputs("main")
564            .await
565    }
566
567    /// Named-function variant of [`Self::execute_local_capturing_client_outputs`].
568    #[tracing::instrument(skip_all, fields(function = name))]
569    pub async fn execute_local_function_capturing_client_outputs(
570        self,
571        name: &str,
572    ) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
573        let (runtime, entry) = self.build_for_local_execution(name)?;
574        let (values, _program_output, client_outputs) =
575            vm::execute_local_capturing_with_options(&runtime, &entry, Default::default()).await?;
576        Ok((values, client_outputs))
577    }
578
579    /// Execute `main` locally with an explicit timeout and return reconstructed
580    /// client outputs delivered via `send_to_client`.
581    pub async fn execute_local_capturing_client_outputs_with_timeout(
582        self,
583        timeout: Duration,
584    ) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
585        self.execute_local_function_capturing_client_outputs_with_timeout("main", timeout)
586            .await
587    }
588
589    /// Named-function variant of
590    /// [`Self::execute_local_capturing_client_outputs_with_timeout`].
591    #[tracing::instrument(skip_all, fields(function = name))]
592    pub async fn execute_local_function_capturing_client_outputs_with_timeout(
593        self,
594        name: &str,
595        timeout: Duration,
596    ) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
597        if timeout.is_zero() {
598            return Err(Error::Configuration(
599                "local network timeout must be greater than zero".to_owned(),
600            ));
601        }
602        let (runtime, entry) = self.build_for_local_execution(name)?;
603        let (values, _program_output, client_outputs) = vm::execute_local_capturing_with_options(
604            &runtime,
605            &entry,
606            vm::LocalExecutionOptions {
607                runner_path: None,
608                timeout: Some(timeout),
609            },
610        )
611        .await?;
612        Ok((values, client_outputs))
613    }
614
615    /// Execute locally using real MPC nodes with an explicit coordinator timeout.
616    #[tracing::instrument(skip_all)]
617    pub async fn execute_local_with_timeout(self, timeout: Duration) -> Result<Vec<Value>> {
618        self.execute_local_function_with_timeout("main", timeout)
619            .await
620    }
621
622    /// Execute a named function locally using real MPC nodes with an explicit
623    /// coordinator timeout.
624    #[tracing::instrument(skip_all, fields(function = name))]
625    pub async fn execute_local_function_with_timeout(
626        self,
627        name: &str,
628        timeout: Duration,
629    ) -> Result<Vec<Value>> {
630        if timeout.is_zero() {
631            return Err(Error::Configuration(
632                "local network timeout must be greater than zero".to_owned(),
633            ));
634        }
635        let (runtime, entry) = self.build_for_local_execution(name)?;
636        vm::execute_local_with_options(
637            &runtime,
638            &entry,
639            vm::LocalExecutionOptions {
640                runner_path: None,
641                timeout: Some(timeout),
642            },
643        )
644        .await
645    }
646
647    /// Execute a cleartext Stoffel program with the embedded VM.
648    ///
649    /// This is intended for local development of non-secret logic. Secret MPC
650    /// programs should use [`Self::execute_local`], which is reserved for the
651    /// PRD's full multi-node localhost execution model.
652    pub fn execute_clear(self) -> Result<Vec<Value>> {
653        self.execute_clear_function("main")
654    }
655
656    /// Execute a named cleartext function with the embedded VM.
657    pub fn execute_clear_function(self, name: &str) -> Result<Vec<Value>> {
658        let runtime = self.build()?;
659        vm::execute_clear(&runtime, name)
660    }
661
662    /// Create a server builder without compiling a program.
663    pub fn server(party_id: usize) -> ServerBuilder {
664        ServerBuilder::new(party_id)
665    }
666
667    /// Create a client builder.
668    pub fn client() -> ClientBuilder {
669        ClientBuilder::new()
670    }
671
672    fn from_source(source: ProgramSource) -> Self {
673        Self {
674            source: Some(source),
675            mpc_config: MpcConfig::default(),
676            backend_explicit: false,
677            network_config: None,
678            local_runner_path: None,
679            config_error: None,
680            inputs: Vec::new(),
681            client_inputs: Vec::new(),
682            expected_clients: None,
683            client_output_counts: std::collections::HashMap::new(),
684            compiler_options: CompilationOptions::default(),
685        }
686    }
687
688    fn build_for_local_execution(self, name: &str) -> Result<(StoffelRuntime, String)> {
689        if self.inputs.is_empty() {
690            return Ok((self.build()?, name.to_owned()));
691        }
692        if !self.client_inputs.is_empty() {
693            return Err(Error::Configuration(
694                "`with_inputs` and `with_client_input` cannot be combined for local execution"
695                    .to_owned(),
696            ));
697        }
698        let parameter_types = local_source_parameter_types(&self.source, name)?;
699        let mut probe = self.clone();
700        probe.inputs.clear();
701        let probe_runtime = probe.build()?;
702        if probe_runtime.program().has_client_input_metadata() {
703            return Err(Error::Unsupported(
704                "SDK local named-input adapter does not support programs that already declare ClientStore input metadata"
705                    .to_owned(),
706            ));
707        }
708
709        let function = probe_runtime
710            .program()
711            .function(name)
712            .ok_or_else(|| Error::FunctionNotFound(name.to_owned()))?;
713        let ordered_inputs =
714            ordered_inputs_for_parameters(&self.inputs, name, function.parameters())?;
715        let entry = unique_local_entry_name(probe_runtime.program(), "__stoffel_sdk_local_entry");
716        let input_shapes = ordered_inputs
717            .iter()
718            .enumerate()
719            .map(|(index, value)| {
720                if parameter_types
721                    .as_ref()
722                    .and_then(|types| types.get(index))
723                    .is_some_and(|ty| !local_parameter_type_is_secret(ty))
724                {
725                    crate::program::LocalInputShape::clear_from_value(value)
726                } else {
727                    crate::program::LocalInputShape::secret_from_value(value)
728                }
729            })
730            .collect::<Vec<_>>();
731        let client_inputs = flatten_local_named_inputs(&ordered_inputs, &input_shapes)?;
732        let wrapped_program =
733            probe_runtime
734                .program()
735                .with_local_client_input_wrapper(name, &entry, &input_shapes)?;
736        let local_client_inputs = if client_inputs.is_empty() {
737            Vec::new()
738        } else {
739            vec![(0, client_inputs)]
740        };
741        Ok((
742            probe_runtime.with_program_and_client_inputs(wrapped_program, local_client_inputs),
743            entry,
744        ))
745    }
746}
747
748fn flatten_local_named_inputs(
749    values: &[Value],
750    shapes: &[crate::program::LocalInputShape],
751) -> Result<Vec<Value>> {
752    let mut out = Vec::new();
753    for (value, shape) in values.iter().zip(shapes) {
754        flatten_local_named_input(value, shape, &mut out)?;
755    }
756    Ok(out)
757}
758
759fn flatten_local_named_input(
760    value: &Value,
761    shape: &crate::program::LocalInputShape,
762    out: &mut Vec<Value>,
763) -> Result<()> {
764    match (value, shape) {
765        (_, crate::program::LocalInputShape::Clear(_)) => Ok(()),
766        (Value::List(values), crate::program::LocalInputShape::List(shapes)) => {
767            for (value, shape) in values.iter().zip(shapes) {
768                flatten_local_named_input(value, shape, out)?;
769            }
770            Ok(())
771        }
772        (Value::Object(fields), crate::program::LocalInputShape::Object(shapes)) => {
773            for (name, shape) in shapes {
774                if let Some(value) = fields.get(name) {
775                    flatten_local_named_input(value, shape, out)?;
776                }
777            }
778            Ok(())
779        }
780        (_, crate::program::LocalInputShape::Share(_)) => {
781            out.push(value.clone());
782            Ok(())
783        }
784        _ => Err(Error::InvalidInput(
785            "local named-input wrapper shape does not match input value".to_owned(),
786        )),
787    }
788}
789
790fn local_source_parameter_types(
791    source: &Option<ProgramSource>,
792    function_name: &str,
793) -> Result<Option<Vec<String>>> {
794    let Some(source) = source else {
795        return Ok(None);
796    };
797    let source = match source {
798        ProgramSource::Source { source, .. } => source.clone(),
799        ProgramSource::File { path } => std::fs::read_to_string(path)?,
800        ProgramSource::Bytecode(_) | ProgramSource::Program(_) => return Ok(None),
801    };
802    Ok(parse_function_parameter_types(&source, function_name))
803}
804
805fn parse_function_parameter_types(source: &str, function_name: &str) -> Option<Vec<String>> {
806    let prefix = format!("def {function_name}(");
807    let start = source.find(&prefix)? + prefix.len();
808    let mut depth = 1usize;
809    let mut end = start;
810    for (offset, character) in source[start..].char_indices() {
811        match character {
812            '(' => depth += 1,
813            ')' => {
814                depth = depth.checked_sub(1)?;
815                if depth == 0 {
816                    end = start + offset;
817                    break;
818                }
819            }
820            _ => {}
821        }
822    }
823    if depth != 0 {
824        return None;
825    }
826
827    let params = &source[start..end];
828    let mut types = Vec::new();
829    for param in split_parameter_list(params) {
830        let (_, ty) = param.split_once(':')?;
831        types.push(ty.trim().to_owned());
832    }
833    Some(types)
834}
835
836fn split_parameter_list(params: &str) -> Vec<&str> {
837    let mut parts = Vec::new();
838    let mut start = 0;
839    let mut depth = 0usize;
840    for (index, character) in params.char_indices() {
841        match character {
842            '[' | '(' => depth += 1,
843            ']' | ')' => depth = depth.saturating_sub(1),
844            ',' if depth == 0 => {
845                let part = params[start..index].trim();
846                if !part.is_empty() {
847                    parts.push(part);
848                }
849                start = index + character.len_utf8();
850            }
851            _ => {}
852        }
853    }
854    let part = params[start..].trim();
855    if !part.is_empty() {
856        parts.push(part);
857    }
858    parts
859}
860
861fn local_parameter_type_is_secret(ty: &str) -> bool {
862    ty.split(|character: char| {
863        !(character.is_ascii_alphanumeric()
864            || character == '_'
865            || character == '['
866            || character == ']')
867    })
868    .any(|token| token == "secret" || token == "Share" || token.contains("secret"))
869}
870
871fn ordered_inputs_for_parameters(
872    inputs: &[(String, Value)],
873    function_name: &str,
874    parameters: &[String],
875) -> Result<Vec<Value>> {
876    for (name, _) in inputs {
877        if !parameters.iter().any(|parameter| parameter == name) {
878            return Err(Error::InvalidInput(format!(
879                "unexpected input '{name}' for function '{function_name}'"
880            )));
881        }
882    }
883
884    let mut values = Vec::with_capacity(parameters.len());
885    for parameter in parameters {
886        let mut matches = inputs
887            .iter()
888            .filter(|(name, _)| name == parameter)
889            .map(|(_, value)| value);
890        let Some(value) = matches.next() else {
891            return Err(Error::InvalidInput(format!(
892                "missing input '{parameter}' for function '{function_name}'"
893            )));
894        };
895        if matches.next().is_some() {
896            return Err(Error::InvalidInput(format!(
897                "duplicate input '{parameter}' for function '{function_name}'"
898            )));
899        }
900        values.push(value.clone());
901    }
902    Ok(values)
903}
904
905fn unique_local_entry_name(program: &Program, base: &str) -> String {
906    if program.function(base).is_none() {
907        return base.to_owned();
908    }
909    for suffix in 1.. {
910        let candidate = format!("{base}_{suffix}");
911        if program.function(&candidate).is_none() {
912            return candidate;
913        }
914    }
915    unreachable!("unbounded suffix search should always find a unique function name")
916}
917
918fn backend_from_bytecode(
919    backend: stoffel_vm_types::compiled_binary::MpcBackend,
920    curve: stoffel_vm_types::compiled_binary::MpcCurve,
921) -> Result<MpcBackend> {
922    match backend {
923        stoffel_vm_types::compiled_binary::MpcBackend::HoneyBadger => Ok(MpcBackend::HoneyBadger),
924        stoffel_vm_types::compiled_binary::MpcBackend::Avss => Ok(MpcBackend::Avss {
925            curve: curve_from_bytecode(curve),
926        }),
927    }
928}
929
930fn curve_from_bytecode(curve: stoffel_vm_types::compiled_binary::MpcCurve) -> Curve {
931    match curve {
932        stoffel_vm_types::compiled_binary::MpcCurve::Bls12_381 => Curve::Bls12_381,
933        stoffel_vm_types::compiled_binary::MpcCurve::Bn254 => Curve::Bn254,
934        stoffel_vm_types::compiled_binary::MpcCurve::Curve25519 => Curve::Curve25519,
935        stoffel_vm_types::compiled_binary::MpcCurve::Ed25519 => Curve::Ed25519,
936        stoffel_vm_types::compiled_binary::MpcCurve::Secp256k1 => Curve::Secp256k1,
937        stoffel_vm_types::compiled_binary::MpcCurve::Secp256r1 => Curve::Secp256r1,
938    }
939}
940
941fn validate_bytecode_backend(program: &Program, expected: MpcBackend) -> Result<()> {
942    let actual = program.bytecode_backend();
943    if actual != expected.compiler_backend() {
944        return Err(Error::Configuration(format!(
945            "bytecode MPC backend ({actual:?}) does not match runtime backend ({expected})"
946        )));
947    }
948    let Some(expected_curve) = expected.curve() else {
949        return Ok(());
950    };
951    let actual_curve = curve_from_bytecode(program.bytecode_curve());
952    if actual_curve != expected_curve {
953        return Err(Error::Configuration(format!(
954            "bytecode AVSS curve ({actual_curve}) does not match runtime backend ({expected})"
955        )));
956    }
957    Ok(())
958}
959
960fn validate_program_network_config(program: &Program, config: &NetworkConfig) -> Result<()> {
961    config.validate_server_addresses()?;
962    program.validate_expected_clients(config.network.expected_clients)
963}