use libp2p::{
request_response::{self, ProtocolSupport},
PeerId, StreamProtocol,
};
use serde::{Deserialize, Serialize};
use std::time::Duration;
pub const MPC_RELAY_PROTOCOL: &str = "/tenzro/mpc/req-resp/1.0.0";
pub const MPC_RELAY_GOSSIP_TOPIC_PREFIX: &str = "/tenzro/mpc/session/";
pub const MAX_INFLIGHT_REQUESTS_PER_PEER: usize = 32;
pub const MAX_INBOUND_STREAMS_PER_PEER: usize = 8;
pub const REQUEST_TIMEOUT: Duration = Duration::from_secs(10);
pub fn session_topic(instance_id_hex: &str) -> String {
format!("{}{}", MPC_RELAY_GOSSIP_TOPIC_PREFIX, instance_id_hex)
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MpcRelayRequest {
pub instance_id: [u8; 32],
pub phase: u8,
pub round_index: u32,
pub from_did: String,
pub to_did: String,
pub payload: Vec<u8>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MpcRelayResponse {
Ack,
Error(MpcRelayError),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, thiserror::Error)]
pub enum MpcRelayError {
#[error("server busy — exceeded {limit} concurrent inbound streams from this peer")]
ServerBusy { limit: usize },
#[error("no MPC subscriber attached")]
NoSubscriber,
#[error("sender DID does not match connection peer ID")]
UnknownSender,
#[error("server error: {0}")]
Internal(String),
}
pub type MpcRelayBehaviour =
request_response::cbor::Behaviour<MpcRelayRequest, MpcRelayResponse>;
pub fn new_behaviour() -> MpcRelayBehaviour {
let protocol = StreamProtocol::new(MPC_RELAY_PROTOCOL);
let cfg = request_response::Config::default().with_request_timeout(REQUEST_TIMEOUT);
request_response::cbor::Behaviour::new(
std::iter::once((protocol, ProtocolSupport::Full)),
cfg,
)
}
pub trait MpcDidResolver: Send + Sync {
fn peer_id_for_did(&self, did: &str) -> Option<PeerId>;
fn did_for_peer_id(&self, peer_id: &PeerId) -> Option<String>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn protocol_id_is_namespaced() {
assert!(MPC_RELAY_PROTOCOL.starts_with("/tenzro/mpc/"));
assert!(MPC_RELAY_GOSSIP_TOPIC_PREFIX.starts_with("/tenzro/mpc/"));
}
#[test]
fn session_topic_formats() {
let t = session_topic("deadbeef");
assert_eq!(t, "/tenzro/mpc/session/deadbeef");
}
#[test]
fn request_round_trip() {
let req = MpcRelayRequest {
instance_id: [7u8; 32],
phase: 1,
round_index: 3,
from_did: "did:tenzro:machine:p1".to_string(),
to_did: "did:tenzro:machine:p2".to_string(),
payload: vec![0xAA, 0xBB, 0xCC],
};
let bytes = bincode::serialize(&req).expect("encode");
let decoded: MpcRelayRequest = bincode::deserialize(&bytes).expect("decode");
assert_eq!(req.instance_id, decoded.instance_id);
assert_eq!(req.phase, decoded.phase);
assert_eq!(req.round_index, decoded.round_index);
assert_eq!(req.from_did, decoded.from_did);
assert_eq!(req.to_did, decoded.to_did);
assert_eq!(req.payload, decoded.payload);
}
#[test]
fn response_round_trip() {
let cases = vec![
MpcRelayResponse::Ack,
MpcRelayResponse::Error(MpcRelayError::ServerBusy {
limit: MAX_INBOUND_STREAMS_PER_PEER,
}),
MpcRelayResponse::Error(MpcRelayError::NoSubscriber),
MpcRelayResponse::Error(MpcRelayError::UnknownSender),
MpcRelayResponse::Error(MpcRelayError::Internal("downstream queue full".to_string())),
];
for resp in cases {
let bytes = bincode::serialize(&resp).expect("encode");
let decoded: MpcRelayResponse = bincode::deserialize(&bytes).expect("decode");
assert_eq!(format!("{:?}", resp), format!("{:?}", decoded));
}
}
#[test]
fn behaviour_constructs() {
let _ = new_behaviour();
}
}