Skip to main content

oris_evolution_network/
lib.rs

1//! Protocol contracts for the Oris Evolution Network (OEN).
2
3pub mod gossip;
4
5use std::collections::BTreeSet;
6
7use chrono::Utc;
8use serde::{Deserialize, Serialize};
9use sha2::{Digest, Sha256};
10
11use oris_evolution::{Capsule, EvolutionEvent, Gene};
12
13#[derive(Clone, Debug, Serialize, Deserialize)]
14pub enum MessageType {
15    Publish,
16    Fetch,
17    Report,
18    Revoke,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize)]
22#[serde(tag = "kind", rename_all = "snake_case")]
23pub enum NetworkAsset {
24    Gene { gene: Gene },
25    Capsule { capsule: Capsule },
26    EvolutionEvent { event: EvolutionEvent },
27}
28
29#[derive(Clone, Debug, Serialize, Deserialize)]
30pub struct EvolutionEnvelope {
31    pub protocol: String,
32    pub protocol_version: String,
33    pub message_type: MessageType,
34    pub message_id: String,
35    pub sender_id: String,
36    pub timestamp: String,
37    pub assets: Vec<NetworkAsset>,
38    #[serde(default, skip_serializing_if = "Option::is_none")]
39    pub manifest: Option<EnvelopeManifest>,
40    pub content_hash: String,
41}
42
43#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
44pub struct EnvelopeManifest {
45    pub publisher: String,
46    pub sender_id: String,
47    pub asset_ids: Vec<String>,
48    pub asset_hash: String,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize)]
52pub struct PublishRequest {
53    pub sender_id: String,
54    pub assets: Vec<NetworkAsset>,
55    #[serde(default, skip_serializing_if = "Option::is_none")]
56    pub since_cursor: Option<String>,
57    #[serde(default, skip_serializing_if = "Option::is_none")]
58    pub resume_token: Option<String>,
59}
60
61#[derive(Clone, Debug, Serialize, Deserialize)]
62pub struct FetchQuery {
63    pub sender_id: String,
64    pub signals: Vec<String>,
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub since_cursor: Option<String>,
67    #[serde(default, skip_serializing_if = "Option::is_none")]
68    pub resume_token: Option<String>,
69}
70
71#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
72pub struct SyncAudit {
73    pub batch_id: String,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub requested_cursor: Option<String>,
76    pub scanned_count: usize,
77    pub applied_count: usize,
78    pub skipped_count: usize,
79    pub failed_count: usize,
80    #[serde(default, skip_serializing_if = "Vec::is_empty")]
81    pub failure_reasons: Vec<String>,
82}
83
84#[derive(Clone, Debug, Serialize, Deserialize)]
85pub struct FetchResponse {
86    pub sender_id: String,
87    pub assets: Vec<NetworkAsset>,
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub next_cursor: Option<String>,
90    #[serde(default, skip_serializing_if = "Option::is_none")]
91    pub resume_token: Option<String>,
92    #[serde(default)]
93    pub sync_audit: SyncAudit,
94}
95
96#[derive(Clone, Debug, Serialize, Deserialize)]
97pub struct RevokeNotice {
98    pub sender_id: String,
99    pub asset_ids: Vec<String>,
100    pub reason: String,
101}
102
103impl EvolutionEnvelope {
104    pub fn publish(sender_id: impl Into<String>, assets: Vec<NetworkAsset>) -> Self {
105        let sender_id = sender_id.into();
106        let manifest = Some(Self::build_manifest(&sender_id, &assets));
107        let mut envelope = Self {
108            protocol: "oen".into(),
109            protocol_version: "0.1".into(),
110            message_type: MessageType::Publish,
111            message_id: format!(
112                "msg-{:x}",
113                Utc::now().timestamp_nanos_opt().unwrap_or_default()
114            ),
115            sender_id,
116            timestamp: Utc::now().to_rfc3339(),
117            assets,
118            manifest,
119            content_hash: String::new(),
120        };
121        envelope.content_hash = envelope.compute_content_hash();
122        envelope
123    }
124
125    fn build_manifest(sender_id: &str, assets: &[NetworkAsset]) -> EnvelopeManifest {
126        EnvelopeManifest {
127            publisher: sender_id.to_string(),
128            sender_id: sender_id.to_string(),
129            asset_ids: Self::manifest_asset_ids(assets),
130            asset_hash: Self::compute_assets_hash(assets),
131        }
132    }
133
134    fn normalize_manifest_ids(asset_ids: &[String]) -> Vec<String> {
135        let normalized = asset_ids
136            .iter()
137            .map(|value| value.trim())
138            .filter(|value| !value.is_empty())
139            .map(ToOwned::to_owned)
140            .collect::<BTreeSet<_>>();
141        normalized.into_iter().collect()
142    }
143
144    pub fn manifest_asset_ids(assets: &[NetworkAsset]) -> Vec<String> {
145        let ids = assets
146            .iter()
147            .map(Self::manifest_asset_id)
148            .collect::<BTreeSet<_>>();
149        ids.into_iter().collect()
150    }
151
152    fn manifest_asset_id(asset: &NetworkAsset) -> String {
153        match asset {
154            NetworkAsset::Gene { gene } => format!("gene:{}", gene.id),
155            NetworkAsset::Capsule { capsule } => format!("capsule:{}", capsule.id),
156            NetworkAsset::EvolutionEvent { event } => {
157                let payload = serde_json::to_vec(event).unwrap_or_default();
158                let mut hasher = Sha256::new();
159                hasher.update(payload);
160                format!("event:{}", hex::encode(hasher.finalize()))
161            }
162        }
163    }
164
165    pub fn compute_assets_hash(assets: &[NetworkAsset]) -> String {
166        let payload = serde_json::to_vec(assets).unwrap_or_default();
167        let mut hasher = Sha256::new();
168        hasher.update(payload);
169        hex::encode(hasher.finalize())
170    }
171
172    pub fn compute_content_hash(&self) -> String {
173        let payload = (
174            &self.protocol,
175            &self.protocol_version,
176            &self.message_type,
177            &self.message_id,
178            &self.sender_id,
179            &self.timestamp,
180            &self.assets,
181            &self.manifest,
182        );
183        let json = serde_json::to_vec(&payload).unwrap_or_default();
184        let mut hasher = Sha256::new();
185        hasher.update(json);
186        hex::encode(hasher.finalize())
187    }
188
189    pub fn verify_content_hash(&self) -> bool {
190        self.compute_content_hash() == self.content_hash
191    }
192
193    pub fn verify_manifest(&self) -> Result<(), String> {
194        let Some(manifest) = self.manifest.as_ref() else {
195            return Err("missing manifest".into());
196        };
197        if manifest.publisher.trim().is_empty() {
198            return Err("missing manifest publisher".into());
199        }
200        if manifest.sender_id.trim().is_empty() {
201            return Err("missing manifest sender_id".into());
202        }
203        if manifest.sender_id.trim() != self.sender_id.trim() {
204            return Err("manifest sender_id mismatch".into());
205        }
206
207        let expected_asset_ids = Self::manifest_asset_ids(&self.assets);
208        let actual_asset_ids = Self::normalize_manifest_ids(&manifest.asset_ids);
209        if expected_asset_ids != actual_asset_ids {
210            return Err("manifest asset_ids mismatch".into());
211        }
212
213        let expected_hash = Self::compute_assets_hash(&self.assets);
214        if manifest.asset_hash != expected_hash {
215            return Err("manifest asset_hash mismatch".into());
216        }
217
218        Ok(())
219    }
220}
221
222#[cfg(test)]
223mod tests {
224    use super::*;
225    use oris_evolution::{AssetState, Gene};
226
227    fn sample_gene(id: &str) -> Gene {
228        Gene {
229            id: id.to_string(),
230            signals: vec!["docs.fix".to_string()],
231            strategy: vec!["summary=docs fix".to_string()],
232            validation: vec!["cargo test".to_string()],
233            state: AssetState::Promoted,
234        }
235    }
236
237    #[test]
238    fn publish_populates_manifest_and_verifies() {
239        let envelope = EvolutionEnvelope::publish(
240            "node-a",
241            vec![NetworkAsset::Gene {
242                gene: sample_gene("gene-a"),
243            }],
244        );
245        assert!(envelope.verify_content_hash());
246        assert!(envelope.verify_manifest().is_ok());
247        assert!(envelope.manifest.is_some());
248    }
249
250    #[test]
251    fn verify_manifest_detects_sender_mismatch() {
252        let mut envelope = EvolutionEnvelope::publish(
253            "node-a",
254            vec![NetworkAsset::Gene {
255                gene: sample_gene("gene-a"),
256            }],
257        );
258        envelope.sender_id = "node-b".to_string();
259        assert!(envelope.verify_manifest().is_err());
260    }
261
262    #[test]
263    fn verify_manifest_detects_asset_hash_drift() {
264        let mut envelope = EvolutionEnvelope::publish(
265            "node-a",
266            vec![NetworkAsset::Gene {
267                gene: sample_gene("gene-a"),
268            }],
269        );
270        if let Some(NetworkAsset::Gene { gene }) = envelope.assets.first_mut() {
271            gene.id = "gene-b".to_string();
272        }
273        assert!(envelope.verify_manifest().is_err());
274    }
275}