#![forbid(unsafe_code)]
pub mod backend;
pub mod client;
pub mod compiler;
pub mod config;
pub mod consensus;
pub mod coordinator;
pub mod docs;
pub mod error;
pub mod input_file;
pub mod networking;
pub mod observability;
pub mod prelude;
pub mod program;
pub mod runtime;
pub mod server;
pub mod types;
pub mod vm;
use std::path::{Path, PathBuf};
use std::time::Duration;
pub use backend::{
avss::{AvssBackend, AvssEngine},
honeybadger::HoneyBadgerBackend,
Backend, MpcBackend,
};
pub use client::{
ClientBuilder, ClientState, ClientSummary, ComputationHandle, ComputationStatus,
ComputationSummary, OffChainClientConfig, OffChainClientConfigBuilder, StoffelClient,
};
pub use compiler::CompilationOptions;
pub use config::{
Curve, MpcConfig, MpcConfigBuilder, MpcConfigSummary, MpcSection, NetworkConfig,
NetworkConfigBuilder, NetworkConfigSummary, NetworkDeployment, NetworkDeploymentBuilder,
NetworkSection, PreprocessingConfig,
};
pub use consensus::{ConsensusGate, NodePublicKey, VerifiedOrdering};
pub use coordinator::{
Coordinator, OffChainCoordinator, OffChainCoordinatorClient, OffChainCoordinatorServer,
ShareBound,
};
pub use error::{ConsensusError, CoordinatorError, Error, ErrorCategory, NetworkError, Result};
pub use input_file::{load_client_inputs_file, load_named_inputs_file};
pub use networking::{NetworkManager, QuicNetworkConfig, QuicNetworkManager};
pub use observability::{
init_tracing, HealthStatus, OpenTelemetryGuard, ServerMetrics, ServerMetricsSnapshot,
TracingConfig, TracingConfigBuilder, TracingConfigSummary,
};
pub use program::{
BytecodeSummary, ClientMetadata, ClientMetadataSummary, FunctionMetadata, FunctionSummary,
Program, ProgramSummary,
};
pub use runtime::{LocalNetworkBuilder, RuntimeSummary, StoffelRuntime};
pub use server::{
OffChainServerConfig, OffChainServerConfigBuilder, ServerBuilder, ServerState, ServerSummary,
StoffelServer,
};
pub use stoffel_vm_types::compiled_binary::FunctionType;
pub use stoffel_vm_types::core_types::ShareType;
pub use types::{
ClientId, ClientInputValue, ClientOutputValue, ClientValueType, FieldElement,
GeneratedProgramManifest, GroupElement, MaskIndex, PartyId, ProgramArgs, PublicKey, Round,
Share, TypedClientInputs, TypedClientOutputs, Value, ValueSummary,
};
pub use vm::LocalClientOutput;
#[derive(Debug, Clone)]
enum ProgramSource {
Source { source: String, filename: String },
File { path: std::path::PathBuf },
Bytecode(Vec<u8>),
Program(Program),
}
pub trait LoadableProgram {
fn load_program(self) -> Result<Program>;
}
impl LoadableProgram for Program {
fn load_program(self) -> Result<Program> {
Ok(self)
}
}
impl LoadableProgram for &Program {
fn load_program(self) -> Result<Program> {
Ok(self.clone())
}
}
impl LoadableProgram for &[u8] {
fn load_program(self) -> Result<Program> {
Program::from_bytecode(self)
}
}
impl LoadableProgram for Vec<u8> {
fn load_program(self) -> Result<Program> {
Program::from_bytecode(&self)
}
}
impl LoadableProgram for &Path {
fn load_program(self) -> Result<Program> {
Program::from_bytecode_file(self)
}
}
impl LoadableProgram for PathBuf {
fn load_program(self) -> Result<Program> {
Program::from_bytecode_file(self)
}
}
impl LoadableProgram for &PathBuf {
fn load_program(self) -> Result<Program> {
Program::from_bytecode_file(self)
}
}
#[derive(Debug, Clone)]
pub struct Stoffel {
source: Option<ProgramSource>,
mpc_config: MpcConfig,
backend_explicit: bool,
network_config: Option<NetworkConfig>,
local_runner_path: Option<PathBuf>,
config_error: Option<String>,
inputs: Vec<(String, Value)>,
client_inputs: Vec<(u64, Vec<Value>)>,
expected_clients: Option<usize>,
client_output_counts: std::collections::HashMap<u64, u64>,
compiler_options: CompilationOptions,
}
impl Stoffel {
pub fn compile(source: &str) -> Result<Self> {
Ok(Self::from_source(ProgramSource::Source {
source: source.to_owned(),
filename: "<sdk>".to_owned(),
}))
}
pub fn compile_file(path: impl AsRef<Path>) -> Result<Self> {
Ok(Self::from_source(ProgramSource::File {
path: path.as_ref().to_path_buf(),
}))
}
pub fn load(bytecode: &[u8]) -> Result<Self> {
Ok(Self::from_source(ProgramSource::Bytecode(
bytecode.to_vec(),
)))
}
pub fn load_file(path: impl AsRef<Path>) -> Result<Self> {
let bytecode = std::fs::read(path)?;
Self::load(&bytecode)
}
pub fn load_program(program: impl LoadableProgram) -> Result<StoffelRuntime> {
Self::from_source(ProgramSource::Program(program.load_program()?)).build()
}
pub fn parties(mut self, n: usize) -> Self {
self.mpc_config.parties = n;
self
}
pub fn threshold(mut self, t: usize) -> Self {
self.mpc_config.threshold = t;
self
}
pub fn instance_id(mut self, id: u64) -> Self {
self.mpc_config.instance_id = id;
self
}
pub fn backend(mut self, backend: MpcBackend) -> Self {
self.mpc_config.backend = backend;
self.backend_explicit = true;
self
}
pub fn manifest<M: GeneratedProgramManifest>(self) -> Self {
self.backend(M::BACKEND)
}
pub fn honeybadger(mut self) -> Self {
self.mpc_config.backend = MpcBackend::HoneyBadger;
self.backend_explicit = true;
self
}
pub fn avss(mut self, curve: Curve) -> Self {
self.mpc_config.backend = MpcBackend::Avss { curve };
self.backend_explicit = true;
self
}
pub fn curve(mut self, curve: Curve) -> Self {
self.mpc_config.backend = MpcBackend::Avss { curve };
self.backend_explicit = true;
self
}
pub fn optimize(mut self, enabled: bool) -> Self {
self.compiler_options.optimize = enabled;
self
}
pub fn optimization_level(mut self, level: u8) -> Self {
self.compiler_options.optimization_level = level;
if level > 0 {
self.compiler_options.optimize = true;
}
self
}
pub fn print_ir(mut self, enabled: bool) -> Self {
self.compiler_options.print_ir = enabled;
self
}
pub fn compiler_options(mut self, options: CompilationOptions) -> Self {
self.compiler_options = options;
self
}
pub fn entry_points<I, S>(mut self, entry_points: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.compiler_options.entry_points = entry_points.into_iter().map(Into::into).collect();
self
}
pub fn with_inputs<V>(mut self, inputs: &[(&str, V)]) -> Self
where
V: Clone + Into<Value>,
{
self.inputs = inputs
.iter()
.map(|(name, value)| ((*name).to_owned(), value.clone().into()))
.collect();
self
}
pub fn with_input<V>(mut self, name: impl Into<String>, value: V) -> Self
where
V: Into<Value>,
{
self.inputs.push((name.into(), value.into()));
self
}
pub fn with_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
self.inputs = input_file::load_named_inputs_file(path)?;
Ok(self)
}
pub fn with_client_input<V>(mut self, client_slot: u64, values: &[V]) -> Self
where
V: Clone + Into<Value>,
{
self.client_inputs.push((
client_slot,
values.iter().cloned().map(Into::into).collect(),
));
self
}
pub fn with_client_input_file(mut self, path: impl AsRef<Path>) -> Result<Self> {
self.client_inputs = input_file::load_client_inputs_file(path)?;
Ok(self)
}
pub fn with_client_inputs<V>(mut self, inputs: &[(u64, &[V])]) -> Self
where
V: Clone + Into<Value>,
{
self.client_inputs = inputs
.iter()
.map(|(client_slot, values)| {
(
*client_slot,
values.iter().cloned().map(Into::into).collect(),
)
})
.collect();
self
}
pub fn expected_output_clients(mut self, n: usize) -> Self {
self.expected_clients = Some(n);
self
}
pub fn client_output_count(mut self, client_slot: u64, count: u64) -> Self {
self.client_output_counts.insert(client_slot, count);
self
}
pub fn network_config(mut self, config: NetworkConfig) -> Self {
self.network_config = Some(config);
self
}
pub fn network_config_file(mut self, path: impl AsRef<Path>) -> Self {
match NetworkConfig::from_toml_file(path) {
Ok(config) => self.network_config = Some(config),
Err(error) => self.config_error = Some(error.to_string()),
}
self
}
pub fn local_runner_path(mut self, path: impl AsRef<Path>) -> Self {
self.local_runner_path = Some(path.as_ref().to_path_buf());
self
}
pub fn build(self) -> Result<StoffelRuntime> {
if let Some(error) = self.config_error {
return Err(Error::Configuration(error));
}
let mut mpc_config = self.mpc_config;
let mut backend_explicit = self.backend_explicit;
if let Some(config) = &self.network_config {
mpc_config = config.to_mpc_config(mpc_config.instance_id)?;
backend_explicit = true;
}
mpc_config.validate()?;
let source = self
.source
.ok_or_else(|| Error::Configuration("no program source configured".to_owned()))?;
let program = match source {
ProgramSource::Source { source, filename } => compiler::compile_source_with_options(
&source,
&filename,
mpc_config.backend,
self.compiler_options,
)?,
ProgramSource::File { path } => compiler::compile_file_with_options(
&path,
mpc_config.backend,
self.compiler_options,
)?,
ProgramSource::Bytecode(bytecode) => {
let program = Program::from_bytecode(&bytecode)?;
let bytecode_backend =
backend_from_bytecode(program.bytecode_backend(), program.bytecode_curve())?;
if backend_explicit {
validate_bytecode_backend(&program, mpc_config.backend)?;
} else {
mpc_config.backend = bytecode_backend;
}
program
}
ProgramSource::Program(program) => {
let bytecode_backend =
backend_from_bytecode(program.bytecode_backend(), program.bytecode_curve())?;
if backend_explicit {
validate_bytecode_backend(&program, mpc_config.backend)?;
} else {
mpc_config.backend = bytecode_backend;
}
program
}
};
validate_bytecode_backend(&program, mpc_config.backend)?;
if program.is_empty() {
return Err(Error::Configuration(
"program must contain at least one function".to_owned(),
));
}
if let Some(expected_clients) = self.expected_clients {
if expected_clients == 0 {
return Err(Error::Configuration(
"expected_clients must be greater than 0".to_owned(),
));
}
program.validate_expected_clients(expected_clients)?;
}
if let Some(config) = &self.network_config {
validate_program_network_config(&program, config)?;
}
let mut runtime = StoffelRuntime::new(
program,
Some(mpc_config),
self.network_config,
self.local_runner_path,
self.inputs,
self.client_inputs,
self.expected_clients,
);
runtime.set_client_output_counts(self.client_output_counts);
Ok(runtime)
}
pub fn validate_client_inputs(self) -> Result<()> {
let runtime = self.build()?;
runtime.validate_client_inputs()
}
#[tracing::instrument(skip_all)]
pub async fn execute_local(self) -> Result<Vec<Value>> {
self.execute_local_function("main").await
}
#[tracing::instrument(skip_all, fields(function = name))]
pub async fn execute_local_function(self, name: &str) -> Result<Vec<Value>> {
let (runtime, entry) = self.build_for_local_execution(name)?;
vm::execute_local(&runtime, &entry).await
}
pub async fn execute_local_capturing_output(self) -> Result<(Vec<Value>, String)> {
self.execute_local_function_capturing_output("main").await
}
#[tracing::instrument(skip_all, fields(function = name))]
pub async fn execute_local_function_capturing_output(
self,
name: &str,
) -> Result<(Vec<Value>, String)> {
let (runtime, entry) = self.build_for_local_execution(name)?;
let (values, program_output, _client_outputs) =
vm::execute_local_capturing_with_options(&runtime, &entry, Default::default()).await?;
Ok((values, program_output))
}
pub async fn execute_local_capturing_client_outputs(
self,
) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
self.execute_local_function_capturing_client_outputs("main")
.await
}
#[tracing::instrument(skip_all, fields(function = name))]
pub async fn execute_local_function_capturing_client_outputs(
self,
name: &str,
) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
let (runtime, entry) = self.build_for_local_execution(name)?;
let (values, _program_output, client_outputs) =
vm::execute_local_capturing_with_options(&runtime, &entry, Default::default()).await?;
Ok((values, client_outputs))
}
pub async fn execute_local_capturing_client_outputs_with_timeout(
self,
timeout: Duration,
) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
self.execute_local_function_capturing_client_outputs_with_timeout("main", timeout)
.await
}
#[tracing::instrument(skip_all, fields(function = name))]
pub async fn execute_local_function_capturing_client_outputs_with_timeout(
self,
name: &str,
timeout: Duration,
) -> Result<(Vec<Value>, Vec<vm::LocalClientOutput>)> {
if timeout.is_zero() {
return Err(Error::Configuration(
"local network timeout must be greater than zero".to_owned(),
));
}
let (runtime, entry) = self.build_for_local_execution(name)?;
let (values, _program_output, client_outputs) = vm::execute_local_capturing_with_options(
&runtime,
&entry,
vm::LocalExecutionOptions {
runner_path: None,
timeout: Some(timeout),
},
)
.await?;
Ok((values, client_outputs))
}
#[tracing::instrument(skip_all)]
pub async fn execute_local_with_timeout(self, timeout: Duration) -> Result<Vec<Value>> {
self.execute_local_function_with_timeout("main", timeout)
.await
}
#[tracing::instrument(skip_all, fields(function = name))]
pub async fn execute_local_function_with_timeout(
self,
name: &str,
timeout: Duration,
) -> Result<Vec<Value>> {
if timeout.is_zero() {
return Err(Error::Configuration(
"local network timeout must be greater than zero".to_owned(),
));
}
let (runtime, entry) = self.build_for_local_execution(name)?;
vm::execute_local_with_options(
&runtime,
&entry,
vm::LocalExecutionOptions {
runner_path: None,
timeout: Some(timeout),
},
)
.await
}
pub fn execute_clear(self) -> Result<Vec<Value>> {
self.execute_clear_function("main")
}
pub fn execute_clear_function(self, name: &str) -> Result<Vec<Value>> {
let runtime = self.build()?;
vm::execute_clear(&runtime, name)
}
pub fn server(party_id: usize) -> ServerBuilder {
ServerBuilder::new(party_id)
}
pub fn client() -> ClientBuilder {
ClientBuilder::new()
}
fn from_source(source: ProgramSource) -> Self {
Self {
source: Some(source),
mpc_config: MpcConfig::default(),
backend_explicit: false,
network_config: None,
local_runner_path: None,
config_error: None,
inputs: Vec::new(),
client_inputs: Vec::new(),
expected_clients: None,
client_output_counts: std::collections::HashMap::new(),
compiler_options: CompilationOptions::default(),
}
}
fn build_for_local_execution(self, name: &str) -> Result<(StoffelRuntime, String)> {
if self.inputs.is_empty() {
return Ok((self.build()?, name.to_owned()));
}
if !self.client_inputs.is_empty() {
return Err(Error::Configuration(
"`with_inputs` and `with_client_input` cannot be combined for local execution"
.to_owned(),
));
}
let parameter_types = local_source_parameter_types(&self.source, name)?;
let mut probe = self.clone();
probe.inputs.clear();
let probe_runtime = probe.build()?;
if probe_runtime.program().has_client_input_metadata() {
return Err(Error::Unsupported(
"SDK local named-input adapter does not support programs that already declare ClientStore input metadata"
.to_owned(),
));
}
let function = probe_runtime
.program()
.function(name)
.ok_or_else(|| Error::FunctionNotFound(name.to_owned()))?;
let ordered_inputs =
ordered_inputs_for_parameters(&self.inputs, name, function.parameters())?;
let entry = unique_local_entry_name(probe_runtime.program(), "__stoffel_sdk_local_entry");
let input_shapes = ordered_inputs
.iter()
.enumerate()
.map(|(index, value)| {
if parameter_types
.as_ref()
.and_then(|types| types.get(index))
.is_some_and(|ty| !local_parameter_type_is_secret(ty))
{
crate::program::LocalInputShape::clear_from_value(value)
} else {
crate::program::LocalInputShape::secret_from_value(value)
}
})
.collect::<Vec<_>>();
let client_inputs = flatten_local_named_inputs(&ordered_inputs, &input_shapes)?;
let wrapped_program =
probe_runtime
.program()
.with_local_client_input_wrapper(name, &entry, &input_shapes)?;
let local_client_inputs = if client_inputs.is_empty() {
Vec::new()
} else {
vec![(0, client_inputs)]
};
Ok((
probe_runtime.with_program_and_client_inputs(wrapped_program, local_client_inputs),
entry,
))
}
}
fn flatten_local_named_inputs(
values: &[Value],
shapes: &[crate::program::LocalInputShape],
) -> Result<Vec<Value>> {
let mut out = Vec::new();
for (value, shape) in values.iter().zip(shapes) {
flatten_local_named_input(value, shape, &mut out)?;
}
Ok(out)
}
fn flatten_local_named_input(
value: &Value,
shape: &crate::program::LocalInputShape,
out: &mut Vec<Value>,
) -> Result<()> {
match (value, shape) {
(_, crate::program::LocalInputShape::Clear(_)) => Ok(()),
(Value::List(values), crate::program::LocalInputShape::List(shapes)) => {
for (value, shape) in values.iter().zip(shapes) {
flatten_local_named_input(value, shape, out)?;
}
Ok(())
}
(Value::Object(fields), crate::program::LocalInputShape::Object(shapes)) => {
for (name, shape) in shapes {
if let Some(value) = fields.get(name) {
flatten_local_named_input(value, shape, out)?;
}
}
Ok(())
}
(_, crate::program::LocalInputShape::Share(_)) => {
out.push(value.clone());
Ok(())
}
_ => Err(Error::InvalidInput(
"local named-input wrapper shape does not match input value".to_owned(),
)),
}
}
fn local_source_parameter_types(
source: &Option<ProgramSource>,
function_name: &str,
) -> Result<Option<Vec<String>>> {
let Some(source) = source else {
return Ok(None);
};
let source = match source {
ProgramSource::Source { source, .. } => source.clone(),
ProgramSource::File { path } => std::fs::read_to_string(path)?,
ProgramSource::Bytecode(_) | ProgramSource::Program(_) => return Ok(None),
};
Ok(parse_function_parameter_types(&source, function_name))
}
fn parse_function_parameter_types(source: &str, function_name: &str) -> Option<Vec<String>> {
let prefix = format!("def {function_name}(");
let start = source.find(&prefix)? + prefix.len();
let mut depth = 1usize;
let mut end = start;
for (offset, character) in source[start..].char_indices() {
match character {
'(' => depth += 1,
')' => {
depth = depth.checked_sub(1)?;
if depth == 0 {
end = start + offset;
break;
}
}
_ => {}
}
}
if depth != 0 {
return None;
}
let params = &source[start..end];
let mut types = Vec::new();
for param in split_parameter_list(params) {
let (_, ty) = param.split_once(':')?;
types.push(ty.trim().to_owned());
}
Some(types)
}
fn split_parameter_list(params: &str) -> Vec<&str> {
let mut parts = Vec::new();
let mut start = 0;
let mut depth = 0usize;
for (index, character) in params.char_indices() {
match character {
'[' | '(' => depth += 1,
']' | ')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
let part = params[start..index].trim();
if !part.is_empty() {
parts.push(part);
}
start = index + character.len_utf8();
}
_ => {}
}
}
let part = params[start..].trim();
if !part.is_empty() {
parts.push(part);
}
parts
}
fn local_parameter_type_is_secret(ty: &str) -> bool {
ty.split(|character: char| {
!(character.is_ascii_alphanumeric()
|| character == '_'
|| character == '['
|| character == ']')
})
.any(|token| token == "secret" || token == "Share" || token.contains("secret"))
}
fn ordered_inputs_for_parameters(
inputs: &[(String, Value)],
function_name: &str,
parameters: &[String],
) -> Result<Vec<Value>> {
for (name, _) in inputs {
if !parameters.iter().any(|parameter| parameter == name) {
return Err(Error::InvalidInput(format!(
"unexpected input '{name}' for function '{function_name}'"
)));
}
}
let mut values = Vec::with_capacity(parameters.len());
for parameter in parameters {
let mut matches = inputs
.iter()
.filter(|(name, _)| name == parameter)
.map(|(_, value)| value);
let Some(value) = matches.next() else {
return Err(Error::InvalidInput(format!(
"missing input '{parameter}' for function '{function_name}'"
)));
};
if matches.next().is_some() {
return Err(Error::InvalidInput(format!(
"duplicate input '{parameter}' for function '{function_name}'"
)));
}
values.push(value.clone());
}
Ok(values)
}
fn unique_local_entry_name(program: &Program, base: &str) -> String {
if program.function(base).is_none() {
return base.to_owned();
}
for suffix in 1.. {
let candidate = format!("{base}_{suffix}");
if program.function(&candidate).is_none() {
return candidate;
}
}
unreachable!("unbounded suffix search should always find a unique function name")
}
fn backend_from_bytecode(
backend: stoffel_vm_types::compiled_binary::MpcBackend,
curve: stoffel_vm_types::compiled_binary::MpcCurve,
) -> Result<MpcBackend> {
match backend {
stoffel_vm_types::compiled_binary::MpcBackend::HoneyBadger => Ok(MpcBackend::HoneyBadger),
stoffel_vm_types::compiled_binary::MpcBackend::Avss => Ok(MpcBackend::Avss {
curve: curve_from_bytecode(curve),
}),
}
}
fn curve_from_bytecode(curve: stoffel_vm_types::compiled_binary::MpcCurve) -> Curve {
match curve {
stoffel_vm_types::compiled_binary::MpcCurve::Bls12_381 => Curve::Bls12_381,
stoffel_vm_types::compiled_binary::MpcCurve::Bn254 => Curve::Bn254,
stoffel_vm_types::compiled_binary::MpcCurve::Curve25519 => Curve::Curve25519,
stoffel_vm_types::compiled_binary::MpcCurve::Ed25519 => Curve::Ed25519,
stoffel_vm_types::compiled_binary::MpcCurve::Secp256k1 => Curve::Secp256k1,
stoffel_vm_types::compiled_binary::MpcCurve::Secp256r1 => Curve::Secp256r1,
}
}
fn validate_bytecode_backend(program: &Program, expected: MpcBackend) -> Result<()> {
let actual = program.bytecode_backend();
if actual != expected.compiler_backend() {
return Err(Error::Configuration(format!(
"bytecode MPC backend ({actual:?}) does not match runtime backend ({expected})"
)));
}
let Some(expected_curve) = expected.curve() else {
return Ok(());
};
let actual_curve = curve_from_bytecode(program.bytecode_curve());
if actual_curve != expected_curve {
return Err(Error::Configuration(format!(
"bytecode AVSS curve ({actual_curve}) does not match runtime backend ({expected})"
)));
}
Ok(())
}
fn validate_program_network_config(program: &Program, config: &NetworkConfig) -> Result<()> {
config.validate_server_addresses()?;
program.validate_expected_clients(config.network.expected_clients)
}