use bytes::Bytes;
use serde::{Deserialize, Serialize};
use tenzro_types::{
Block, SignedTransaction, Hash,
ModelClass, ArtifactCompleteness, ArtifactMetadata, ModelTopology, ExecutionSupport,
RuntimeSupport, NodeNetworkProfile, TrustProfile, WorkerRole,
HardwareCapabilities,
};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct NetworkMessage {
pub payload: MessagePayload,
pub message_id: String,
pub timestamp: i64,
}
impl NetworkMessage {
pub fn new(payload: MessagePayload) -> Self {
Self {
payload,
message_id: uuid::Uuid::new_v4().to_string(),
timestamp: chrono::Utc::now().timestamp_millis(),
}
}
pub fn to_bytes(&self) -> Result<Bytes, bincode::Error> {
let buf = bincode::serialize(self)?;
Ok(Bytes::from(buf))
}
pub fn from_bytes(bytes: &[u8]) -> Result<Self, bincode::Error> {
bincode::deserialize(bytes)
}
pub fn topic(&self) -> &str {
self.payload.topic()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum MessagePayload {
Block(Block),
BlockRequest(Hash),
BlockResponse(Option<Block>),
Transaction(SignedTransaction),
TransactionRequest(Hash),
TransactionResponse(Option<SignedTransaction>),
Attestation(AttestationMessage),
InferenceRequest(InferenceRequestMessage),
InferenceResponse(InferenceResponseMessage),
ModelRegistration(ModelRegistrationMessage),
AgentAnnouncement(AgentAnnouncementMessage),
ProviderAnnouncement(ProviderAnnouncementMessage),
Status(StatusMessage),
Ping,
Pong,
Custom { topic: String, data: Vec<u8> },
}
impl MessagePayload {
pub fn topic(&self) -> &str {
match self {
Self::Block(_) | Self::BlockRequest(_) | Self::BlockResponse(_) => "tenzro/blocks",
Self::Transaction(_) | Self::TransactionRequest(_) | Self::TransactionResponse(_) => {
"tenzro/transactions"
}
Self::Attestation(_) => "tenzro/attestations",
Self::InferenceRequest(_) | Self::InferenceResponse(_) => "tenzro/inference",
Self::ModelRegistration(_) => "tenzro/models",
Self::AgentAnnouncement(_) => "tenzro/agents",
Self::ProviderAnnouncement(_) => "tenzro/providers",
Self::Status(_) | Self::Ping | Self::Pong => "tenzro/status",
Self::Custom { topic, .. } => topic,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum ConsensusMessage {
Proposal {
block: Box<Block>,
proposer: String,
round: u64,
high_qc_view: u64,
timeout_certificate: Option<Vec<u8>>,
no_endorsement_certificate: Option<Vec<u8>>,
},
Vote {
block_hash: Hash,
voter: String,
vote_type: VoteType,
round: u64,
height: u64,
high_qc_view: u64,
signature: Vec<u8>,
public_key: Vec<u8>,
bls_signature: Vec<u8>,
},
Commit {
block_hash: Hash,
signatures: Vec<Vec<u8>>,
},
Timeout {
format_version: u8,
view: u64,
high_qc_view: u64,
finalized_height: u64,
voter: tenzro_types::primitives::Address,
signature: Vec<u8>,
public_key: Vec<u8>,
},
NoEndorsement {
format_version: u8,
view: u64,
voter: tenzro_types::primitives::Address,
signature: Vec<u8>,
public_key: Vec<u8>,
},
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum VoteType {
Prevote,
Precommit,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AttestationMessage {
pub provider_id: String,
pub report: Vec<u8>,
pub signature: Vec<u8>,
pub timestamp: i64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceRequestMessage {
pub request_id: String,
pub model_id: String,
pub input: Vec<u8>,
pub requester: String,
pub payment: PaymentDetails,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InferenceResponseMessage {
pub request_id: String,
pub provider_id: String,
pub output: Vec<u8>,
pub proof: Option<Vec<u8>>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ModelRegistrationMessage {
pub model_id: String,
pub name: String,
pub description: String,
pub modality: String,
#[serde(default)]
pub category: String,
#[serde(default)]
pub parameters: String,
#[serde(default)]
pub context_length: u32,
pub provider: String,
#[serde(default)]
pub peer_id: String,
pub pricing: PricingInfo,
#[serde(default)]
pub schedule: Option<ModelSchedule>,
#[serde(default = "default_visibility")]
pub visibility: String,
#[serde(default = "default_ttl")]
pub ttl_secs: u64,
#[serde(default)]
pub withdrawn: bool,
#[serde(default)]
pub rpc_endpoint: String,
#[serde(default)]
pub model_class: ModelClass,
#[serde(default)]
pub artifact_completeness: ArtifactCompleteness,
#[serde(default)]
pub artifacts: Vec<ArtifactMetadata>,
#[serde(default)]
pub topology: ModelTopology,
#[serde(default)]
pub execution_support: ExecutionSupport,
}
fn default_visibility() -> String {
"network".to_string()
}
fn default_ttl() -> u64 {
120
}
fn default_agent_ttl() -> u64 {
180
}
fn default_provider_ttl() -> u64 {
120
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AgentAnnouncementMessage {
pub agent_id: String,
pub name: String,
#[serde(default)]
pub agent_type: String,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub status: String,
#[serde(default)]
pub origin_peer_id: String,
#[serde(default)]
pub rpc_endpoint: String,
pub timestamp: i64,
#[serde(default = "default_agent_ttl")]
pub ttl_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProviderAnnouncementMessage {
pub peer_id: String,
pub provider_address: String,
#[serde(default)]
pub provider_type: String,
#[serde(default)]
pub served_models: Vec<String>,
#[serde(default)]
pub capabilities: Vec<String>,
#[serde(default)]
pub rpc_endpoint: String,
#[serde(default)]
pub status: String,
pub timestamp: i64,
#[serde(default = "default_provider_ttl")]
pub ttl_secs: u64,
#[serde(default)]
pub runtime_support: RuntimeSupport,
#[serde(default)]
pub network_profile: NodeNetworkProfile,
#[serde(default)]
pub trust_profile: TrustProfile,
#[serde(default)]
pub worker_roles: Vec<WorkerRole>,
#[serde(default)]
pub hardware: HardwareCapabilities,
#[serde(default)]
pub geography: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ModelSchedule {
pub enabled: bool,
#[serde(default)]
pub start_hour: u8,
#[serde(default = "default_end_hour")]
pub end_hour: u8,
#[serde(default = "default_timezone")]
pub timezone: String,
#[serde(default = "default_days")]
pub days_of_week: Vec<u8>,
}
fn default_end_hour() -> u8 {
23
}
fn default_timezone() -> String {
"UTC".to_string()
}
fn default_days() -> Vec<u8> {
vec![0, 1, 2, 3, 4, 5, 6]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PaymentDetails {
pub amount: u64,
pub tx_hash: Option<Hash>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PricingInfo {
pub per_request: u64,
pub per_token: Option<u64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusMessage {
pub peer_id: String,
pub best_block: Hash,
pub height: u64,
pub chain_id: u64,
pub protocol_version: String,
pub tee_capable: bool,
pub tee_vendor: Option<tenzro_types::tee::TeeVendor>,
}
pub fn validate_message(msg: &NetworkMessage) -> crate::error::Result<()> {
let now = chrono::Utc::now().timestamp_millis();
if msg.timestamp > now + 300_000 {
return Err(crate::error::NetworkError::InvalidMessage("Message timestamp is too far in the future".to_string()));
}
if now - msg.timestamp > 3_600_000 {
return Err(crate::error::NetworkError::InvalidMessage("Message is too old".to_string()));
}
match &msg.payload {
MessagePayload::Block(block) => {
if block.header.height.0 == 0 && block.header.prev_hash != Hash::zero() {
return Err(crate::error::NetworkError::InvalidMessage("Genesis block must have zero prev_hash".to_string()));
}
}
MessagePayload::InferenceRequest(req) => {
if req.request_id.is_empty() {
return Err(crate::error::NetworkError::InvalidMessage("Inference request must have a request ID".to_string()));
}
if req.model_id.is_empty() {
return Err(crate::error::NetworkError::InvalidMessage("Inference request must specify a model ID".to_string()));
}
}
_ => {}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_message_serialization() {
let msg = NetworkMessage::new(MessagePayload::Ping);
let bytes = msg.to_bytes().unwrap();
let decoded = NetworkMessage::from_bytes(&bytes).unwrap();
assert_eq!(msg.message_id, decoded.message_id);
assert_eq!(msg.timestamp, decoded.timestamp);
}
#[test]
fn test_message_topics() {
assert_eq!(MessagePayload::Ping.topic(), "tenzro/status");
assert_eq!(
MessagePayload::Custom {
topic: "test/topic".to_string(),
data: vec![]
}
.topic(),
"test/topic"
);
}
}