Skip to main content

stoffel/
runtime.rs

1//! Built runtime for a compiled or loaded Stoffel program.
2//!
3//! A runtime owns program metadata, validated MPC/network settings, and local
4//! execution inputs. It can construct participant builders or execute supported
5//! local development paths.
6
7use std::path::{Path, PathBuf};
8use std::time::Duration;
9
10use crate::client::{ClientBuilder, OffChainClientConfigBuilder};
11use crate::config::{
12    MpcConfig, MpcConfigSummary, NetworkConfig, NetworkConfigSummary, NetworkDeployment,
13};
14use crate::error::{Error, Result};
15use crate::program::{BytecodeSummary, Program, ProgramSummary};
16use crate::server::ServerBuilder;
17use crate::types::{ProgramArgs, Value};
18use crate::vm;
19
20#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
21pub struct RuntimeSummary {
22    pub program: ProgramSummary,
23    pub mpc: Option<MpcConfigSummary>,
24    pub network: Option<NetworkConfigSummary>,
25    pub named_input_count: usize,
26    pub client_input_count: usize,
27    pub expected_clients: Option<usize>,
28    pub local_runner_configured: bool,
29}
30
31#[derive(Debug, Clone)]
32pub struct StoffelRuntime {
33    program: Program,
34    mpc_config: Option<MpcConfig>,
35    network_config: Option<NetworkConfig>,
36    local_runner_path: Option<PathBuf>,
37    inputs: Vec<(String, Value)>,
38    client_inputs: Vec<(u64, Vec<Value>)>,
39    expected_clients: Option<usize>,
40    /// Developer-supplied per-client output counts, used by the local runner as
41    /// a fallback only when the program does not statically declare outputs for
42    /// a client (otherwise the manifest count wins).
43    client_output_counts: std::collections::HashMap<u64, u64>,
44}
45
46impl StoffelRuntime {
47    pub(crate) fn new(
48        program: Program,
49        mpc_config: Option<MpcConfig>,
50        network_config: Option<NetworkConfig>,
51        local_runner_path: Option<PathBuf>,
52        inputs: Vec<(String, Value)>,
53        client_inputs: Vec<(u64, Vec<Value>)>,
54        expected_clients: Option<usize>,
55    ) -> Self {
56        Self {
57            program,
58            mpc_config,
59            network_config,
60            local_runner_path,
61            inputs,
62            client_inputs,
63            expected_clients,
64            client_output_counts: std::collections::HashMap::new(),
65        }
66    }
67
68    /// Set developer-supplied per-client output counts (CLI / Stoffel.toml).
69    pub(crate) fn set_client_output_counts(&mut self, counts: std::collections::HashMap<u64, u64>) {
70        self.client_output_counts = counts;
71    }
72
73    /// Developer-supplied per-client output counts (local-runner fallback).
74    pub(crate) fn client_output_counts(&self) -> &std::collections::HashMap<u64, u64> {
75        &self.client_output_counts
76    }
77
78    pub fn program(&self) -> &Program {
79        &self.program
80    }
81
82    pub(crate) fn with_program_and_client_inputs(
83        mut self,
84        program: Program,
85        client_inputs: Vec<(u64, Vec<Value>)>,
86    ) -> Self {
87        self.program = program;
88        self.inputs.clear();
89        self.client_inputs = client_inputs;
90        self
91    }
92
93    /// Serialize the compiled program as CLI-compatible Stoffel bytecode.
94    pub fn to_bytecode(&self) -> Result<Vec<u8>> {
95        self.program.to_bytecode()
96    }
97
98    /// Save the compiled program as CLI-compatible Stoffel bytecode.
99    pub fn save_bytecode(&self, path: impl AsRef<Path>) -> Result<()> {
100        self.program.save_bytecode(path)
101    }
102
103    /// Summarize the CLI-compatible bytecode artifact without changing it.
104    pub fn bytecode_summary(&self) -> Result<BytecodeSummary> {
105        self.program.bytecode_summary()
106    }
107
108    pub fn client(&self) -> ClientBuilder {
109        let builder = ClientBuilder::new().with_program(self.program.clone());
110        match &self.network_config {
111            Some(config) => match config.server_addresses() {
112                Ok(addresses) => builder.servers(addresses),
113                Err(error) => builder.configuration_error(error.to_string()),
114            },
115            None => builder,
116        }
117    }
118
119    /// Create a client builder with this runtime's program and all deployment servers.
120    pub fn client_for_deployment(&self, deployment: &NetworkDeployment) -> ClientBuilder {
121        ClientBuilder::new()
122            .with_program(self.program.clone())
123            .network_deployment(deployment)
124    }
125
126    /// Create an off-chain client IO config builder from this runtime's typed program metadata.
127    ///
128    /// The runtime supplies the MPC backend, topology, and output count for
129    /// `client_slot`. Callers still provide coordinator/node endpoints,
130    /// timestamp, and client identity explicitly.
131    pub fn offchain_client_config(&self, client_slot: u64) -> Result<OffChainClientConfigBuilder> {
132        let client = self.program.client(client_slot).ok_or_else(|| {
133            Error::Configuration(format!(
134                "program does not declare ClientStore metadata for client slot {client_slot}"
135            ))
136        })?;
137        let input_start_index = self
138            .program
139            .clients()
140            .take_while(|schema| schema.client_slot() != client_slot)
141            .map(|schema| schema.input_count() as u64)
142            .sum();
143        let mpc_config = self
144            .mpc_config
145            .as_ref()
146            .ok_or_else(|| Error::Configuration("MPC configuration is required".to_owned()))?;
147        Ok(OffChainClientConfigBuilder::default()
148            .client_slot(client_slot)
149            .input_start_index(input_start_index)
150            .parties(mpc_config.parties)
151            .threshold(mpc_config.threshold)
152            .backend(mpc_config.backend)
153            .input_types(client.inputs().iter().copied())
154            .output_types(client.outputs().iter().copied()))
155    }
156
157    pub fn server(&self, party_id: usize) -> ServerBuilder {
158        let mut builder = ServerBuilder::new(party_id).with_program(self.program.clone());
159        if let Some(config) = &self.mpc_config {
160            builder = builder.mpc_config(config);
161        }
162        match &self.network_config {
163            Some(config) if config.network.party_id == party_id => builder.network_config(config),
164            Some(config) => builder.configuration_error(format!(
165                "network config party_id {} does not match requested server party_id {party_id}",
166                config.network.party_id
167            )),
168            None => builder,
169        }
170    }
171
172    /// Create a server builder for one party config while carrying this program.
173    pub fn server_for_config(&self, config: &NetworkConfig) -> ServerBuilder {
174        let mut builder = ServerBuilder::new(config.party_id()).with_program(self.program.clone());
175        if let Some(mpc_config) = &self.mpc_config {
176            builder = builder.mpc_config(mpc_config);
177        }
178        builder.network_config(config)
179    }
180
181    /// Create one server builder per party in a validated deployment plan.
182    ///
183    /// Each builder carries this runtime's compiled program metadata and uses
184    /// the corresponding party's network config.
185    pub fn servers_for_deployment(&self, deployment: &NetworkDeployment) -> Vec<ServerBuilder> {
186        deployment
187            .configs()
188            .iter()
189            .map(|config| self.server_for_config(config))
190            .collect()
191    }
192
193    pub fn mpc_config(&self) -> Option<&MpcConfig> {
194        self.mpc_config.as_ref()
195    }
196
197    pub fn network_config(&self) -> Option<&NetworkConfig> {
198        self.network_config.as_ref()
199    }
200
201    pub fn mpc_summary(&self) -> Result<Option<MpcConfigSummary>> {
202        self.mpc_config.as_ref().map(MpcConfig::summary).transpose()
203    }
204
205    pub fn network_summary(&self) -> Result<Option<NetworkConfigSummary>> {
206        self.network_config
207            .as_ref()
208            .map(NetworkConfig::summary)
209            .transpose()
210    }
211
212    pub fn summary(&self) -> Result<RuntimeSummary> {
213        Ok(RuntimeSummary {
214            program: self.program.summary(),
215            mpc: self.mpc_summary()?,
216            network: self.network_summary()?,
217            named_input_count: self.inputs.len(),
218            client_input_count: self.client_inputs.len(),
219            expected_clients: self.expected_clients,
220            local_runner_configured: self.local_runner_path.is_some(),
221        })
222    }
223
224    pub fn local_runner_binary_path(&self) -> Option<&Path> {
225        self.local_runner_path.as_deref()
226    }
227
228    pub fn inputs(&self) -> &[(String, Value)] {
229        &self.inputs
230    }
231
232    pub fn client_inputs(&self) -> &[(u64, Vec<Value>)] {
233        &self.client_inputs
234    }
235
236    pub fn configured_expected_clients(&self) -> Option<usize> {
237        self.expected_clients
238    }
239
240    pub fn validate_client_inputs(&self) -> Result<()> {
241        if let Some(expected_clients) = self.expected_clients {
242            if expected_clients == 0 {
243                return Err(Error::Configuration(
244                    "expected_clients must be greater than 0".to_owned(),
245                ));
246            }
247            self.program.validate_expected_clients(expected_clients)?;
248        }
249        self.program
250            .validate_owned_client_inputs(&self.client_inputs)
251    }
252
253    /// Replace named function inputs for clear execution.
254    pub fn with_inputs<V>(mut self, inputs: &[(&str, V)]) -> Self
255    where
256        V: Clone + Into<Value>,
257    {
258        self.inputs = inputs
259            .iter()
260            .map(|(name, value)| ((*name).to_owned(), value.clone().into()))
261            .collect();
262        self
263    }
264
265    /// Add one named function input for clear execution.
266    pub fn with_input<V>(mut self, name: impl Into<String>, value: V) -> Self
267    where
268        V: Into<Value>,
269    {
270        self.inputs.push((name.into(), value.into()));
271        self
272    }
273
274    /// Replace named function inputs with values loaded from a `.json`, `.csv`, or `.txt` file.
275    pub fn with_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
276        self.inputs = crate::input_file::load_named_inputs_file(path)?;
277        Ok(self)
278    }
279
280    /// Add one coordinator client input set for local networked execution.
281    pub fn with_client_input<V>(mut self, client_slot: u64, values: &[V]) -> Self
282    where
283        V: Clone + Into<Value>,
284    {
285        self.client_inputs.push((
286            client_slot,
287            values.iter().cloned().map(Into::into).collect(),
288        ));
289        self
290    }
291
292    /// Replace local ClientStore inputs with values loaded from a `.json`, `.csv`, or `.txt` file.
293    pub fn with_client_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
294        self.client_inputs = crate::input_file::load_client_inputs_file(path)?;
295        Ok(self)
296    }
297
298    /// Replace all coordinator client input sets for local networked execution.
299    pub fn with_client_inputs<V>(mut self, inputs: &[(u64, &[V])]) -> Self
300    where
301        V: Clone + Into<Value>,
302    {
303        self.client_inputs = inputs
304            .iter()
305            .map(|(client_slot, values)| {
306                (
307                    *client_slot,
308                    values.iter().cloned().map(Into::into).collect(),
309                )
310            })
311            .collect();
312        self
313    }
314
315    /// Declare output-capable client slots `0..n-1` for local execution.
316    pub fn expected_output_clients(mut self, n: usize) -> Self {
317        self.expected_clients = Some(n);
318        self
319    }
320
321    /// Set the `stoffel-run` binary path used by local coordinator execution.
322    pub fn local_runner_path(mut self, path: impl AsRef<Path>) -> Self {
323        self.local_runner_path = Some(path.as_ref().to_path_buf());
324        self
325    }
326
327    /// Configure a real localhost coordinator-backed MPC run.
328    ///
329    /// This builder exposes local development controls without replacing the
330    /// networking, coordinator, or protocol implementations owned by the lower
331    /// crates.
332    pub fn local_network(&self) -> LocalNetworkBuilder<'_> {
333        LocalNetworkBuilder::new(self)
334    }
335
336    pub(crate) fn input_values_for_function(&self, function_name: &str) -> Result<Vec<Value>> {
337        let function = self
338            .program
339            .function(function_name)
340            .ok_or_else(|| Error::FunctionNotFound(function_name.to_owned()))?;
341        function.validate_inputs(&self.inputs)?;
342        let parameters = function.parameters();
343        let mut values = Vec::with_capacity(parameters.len());
344
345        for parameter in parameters {
346            let value = self
347                .inputs
348                .iter()
349                .find(|(name, _)| name == parameter)
350                .map(|(_, value)| value)
351                .expect("function input validation should guarantee one value per parameter");
352            values.push(value.clone());
353        }
354
355        Ok(values)
356    }
357
358    pub fn execute_clear(&self) -> Result<Vec<Value>> {
359        self.execute_clear_function("main")
360    }
361
362    pub fn execute_clear_function(&self, name: &str) -> Result<Vec<Value>> {
363        vm::execute_clear(self, name)
364    }
365
366    /// Execute a named function with positional SDK values.
367    ///
368    /// This is the direct, local cleartext execution path for a loaded or
369    /// compiled program. Arguments are validated against the function metadata
370    /// stored in the binary before the VM is invoked.
371    pub fn execute<A>(&self, name: &str, args: A) -> Result<Vec<Value>>
372    where
373        A: ProgramArgs,
374    {
375        let args = args.into_values();
376        vm::execute_clear_with_sdk_args(self, name, &args)
377    }
378
379    /// Execute `main` with positional SDK values.
380    pub fn execute_main<A>(&self, args: A) -> Result<Vec<Value>>
381    where
382        A: ProgramArgs,
383    {
384        self.execute("main", args)
385    }
386
387    /// Execute the runtime's `main` entrypoint using the local coordinator runner.
388    ///
389    /// This uses the same real localhost party mesh as [`crate::Stoffel::execute_local`].
390    /// It is intended for runtimes that were built from no-argument
391    /// `ClientStore` entrypoints with client input supplied through the builder.
392    pub async fn execute_local(&self) -> Result<Vec<Value>> {
393        self.execute_local_function("main").await
394    }
395
396    /// Execute a named runtime entrypoint using the local coordinator runner.
397    pub async fn execute_local_function(&self, name: &str) -> Result<Vec<Value>> {
398        vm::execute_local(self, name).await
399    }
400}
401
402#[derive(Debug, Clone)]
403pub struct LocalNetworkBuilder<'a> {
404    runtime: &'a StoffelRuntime,
405    entry: String,
406    runner_path: Option<PathBuf>,
407    timeout: Option<Duration>,
408}
409
410impl<'a> LocalNetworkBuilder<'a> {
411    fn new(runtime: &'a StoffelRuntime) -> Self {
412        Self {
413            runtime,
414            entry: "main".to_owned(),
415            runner_path: None,
416            timeout: None,
417        }
418    }
419
420    /// Select the runtime entrypoint to execute. Default is `main`.
421    pub fn entry(mut self, entry: impl Into<String>) -> Self {
422        self.entry = entry.into();
423        self
424    }
425
426    /// Override the `stoffel-run` binary path for this local run.
427    pub fn runner_path(mut self, path: impl AsRef<Path>) -> Self {
428        self.runner_path = Some(path.as_ref().to_path_buf());
429        self
430    }
431
432    /// Set the timeout applied to the local coordinator and party processes.
433    pub fn timeout(mut self, timeout: Duration) -> Self {
434        self.timeout = Some(timeout);
435        self
436    }
437
438    pub fn configured_entry(&self) -> &str {
439        &self.entry
440    }
441
442    pub fn configured_runner_path(&self) -> Option<&Path> {
443        self.runner_path.as_deref()
444    }
445
446    pub fn configured_timeout(&self) -> Option<Duration> {
447        self.timeout
448    }
449
450    /// Execute the configured local coordinator-backed MPC run.
451    pub async fn run(self) -> Result<Vec<Value>> {
452        if matches!(self.timeout, Some(timeout) if timeout.is_zero()) {
453            return Err(Error::Configuration(
454                "local network timeout must be greater than zero".to_owned(),
455            ));
456        }
457        vm::execute_local_with_options(
458            self.runtime,
459            &self.entry,
460            vm::LocalExecutionOptions {
461                runner_path: self.runner_path,
462                timeout: self.timeout,
463            },
464        )
465        .await
466    }
467}