Skip to main content

oris_evolution_network/
lib.rs

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