1pub 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}