1use std::time::{Duration, Instant};
23
24use serde::{Deserialize, Serialize};
25
26use super::exchange::{
27 ExchangeError, Forward, GrantMsg, LicenceExchange, Receipt, Spend,
28};
29use super::exchange_types::Budget;
30use super::{CheckoutGrant, LicenceError, LicenceEvent, SignedEnvelope};
31use crate::mesh_delivery::PeerCtx;
32use crate::mesh_discovery::MeshPeerEvent;
33use crate::mesh_runtime::{COG_SYNC_TOPIC, PeerControlSink};
34
35pub const SYNC_MAX_ENTRIES: usize = 256;
37pub const SYNC_MAX_BYTES: usize = 256 * 1024;
39pub(super) const SYNC_ENTRIES_PER_SEC: f64 = 8.0;
41pub(super) const SYNC_BURST: f64 = 600.0;
43pub(super) const PENDING_TTL: Duration = Duration::from_secs(120);
45
46#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
48pub struct GrantCursor {
49 pub seq: u64,
51 pub cog_id: String,
53 pub version: String,
55}
56
57#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(tag = "op", rename_all = "snake_case")]
60pub enum SyncMsg {
61 Request {
63 grant_after: Option<GrantCursor>,
65 approval_after: Option<String>,
67 },
68 Response {
70 binding: Option<SignedEnvelope>,
72 grants: Vec<SignedEnvelope>,
74 approvals: Vec<SignedEnvelope>,
76 more_grants: bool,
78 more_approvals: bool,
80 },
81}
82
83pub(super) struct Pending {
85 pub(super) sent: Instant,
86 pub(super) pages: u32,
87 pub(super) grant_after: Option<GrantCursor>,
88 pub(super) approval_after: Option<String>,
89}
90
91pub(super) struct ServeSession {
94 pub(super) opened: Instant,
95 pub(super) pages: u32,
96 pub(super) grant_last: Option<GrantCursor>,
97 pub(super) approval_last: Option<String>,
98}
99
100pub(super) struct Page {
102 pub(super) msg: SyncMsg,
103 pub(super) last_grant: Option<GrantCursor>,
104 pub(super) last_approval: Option<String>,
105}
106
107fn size(e: &SignedEnvelope) -> usize {
108 super::store_sync::env_size(e)
109}
110
111fn not_behind<T: Ord>(req: &Option<T>, last: &Option<T>) -> bool {
113 match (req, last) {
114 (_, None) => true,
115 (None, Some(_)) => false,
116 (Some(r), Some(l)) => r >= l,
117 }
118}
119
120impl LicenceExchange {
121 pub(super) fn build_page(
124 &self,
125 grant_after: &Option<GrantCursor>,
126 approval_after: &Option<String>,
127 ) -> Page {
128 let binding = match grant_after {
129 None => self.store.held_signed_binding().map(|(_, b)| b),
130 Some(_) => None,
131 };
132 let used = binding.as_ref().map(size).unwrap_or(0);
133 let after = grant_after.as_ref().map(|c| (c.seq, c.cog_id.as_str(), c.version.as_str()));
134 let (g, more_grants) =
135 self.store.sync_grants_page(after, SYNC_MAX_ENTRIES / 2, SYNC_MAX_BYTES / 2);
136 let last_grant = g
137 .last()
138 .map(|(seq, cog, ver, _)| GrantCursor { seq: *seq, cog_id: cog.clone(), version: ver.clone() });
139 let grants: Vec<SignedEnvelope> = g.into_iter().map(|(_, _, _, e)| e).collect();
140 let g_bytes: usize = grants.iter().map(size).sum();
141 let (a, more_approvals) = self.approvals.sync_approvals_page(
142 approval_after.as_deref(),
143 SYNC_MAX_ENTRIES - grants.len() - usize::from(binding.is_some()),
144 SYNC_MAX_BYTES.saturating_sub(used + g_bytes),
145 );
146 let last_approval = a.last().map(|(k, _)| k.clone());
147 let approvals = a.into_iter().map(|(_, e)| e).collect();
148 Page {
149 msg: SyncMsg::Response { binding, grants, approvals, more_grants, more_approvals },
150 last_grant,
151 last_approval,
152 }
153 }
154
155 #[cfg(test)]
157 pub(super) fn build_response(
158 &self,
159 grant_after: &Option<GrantCursor>,
160 approval_after: &Option<String>,
161 ) -> SyncMsg {
162 self.build_page(grant_after, approval_after).msg
163 }
164
165 fn is_banned(&self, peer: &str) -> bool {
166 match self.banned.get(peer).map(|t| t.elapsed()) {
167 Some(age) if age < self.config.sync_ban => true,
168 Some(_) => {
169 self.banned.remove(peer);
170 false
171 }
172 None => false,
173 }
174 }
175
176 pub async fn sync_peer(&self, peer: &str) {
179 if self.local().is_err() || self.is_banned(peer) {
180 return;
181 }
182 if self.pending.get(peer).is_some_and(|p| p.sent.elapsed() < PENDING_TTL) {
183 return;
184 }
185 self.pending.insert(
186 peer.to_owned(),
187 Pending { sent: Instant::now(), pages: 0, grant_after: None, approval_after: None },
188 );
189 let req = SyncMsg::Request { grant_after: None, approval_after: None };
190 if let Ok(v) = serde_json::to_value(&req) {
191 self.send_to(peer, COG_SYNC_TOPIC, v).await;
192 }
193 }
194
195 pub async fn resync_all(&self) {
199 self.pending.clear();
200 self.retries.clear();
201 self.sync_all().await;
202 }
203
204 pub async fn sync_all(&self) {
206 for peer in self.runtime.peer_ids() {
207 self.sync_peer(&peer).await;
208 }
209 }
210
211 pub(super) fn spawn_sync_tasks(&self) {
213 let Ok(handle) = tokio::runtime::Handle::try_current() else {
214 return;
215 };
216 let (w, every) = (self.me.clone(), self.config.sync_interval);
217 handle.spawn(async move {
218 loop {
219 tokio::time::sleep(every).await;
220 let Some(me) = w.upgrade() else { break };
221 me.sync_all().await;
222 }
223 });
224 let w = self.me.clone();
225 handle.spawn(async move {
226 loop {
227 tokio::time::sleep(super::exchange_retry::RETRY_SCAN).await;
228 let Some(me) = w.upgrade() else { break };
229 me.retry_expired().await;
230 }
231 });
232 if !self.config.sync_on_connect {
233 return;
234 }
235 let Some(mut rx) = self.runtime.subscribe_peer_events() else { return };
236 let w = self.me.clone();
237 handle.spawn(async move {
238 while let Ok(ev) = rx.recv().await {
239 if let MeshPeerEvent::Joined { node_id, .. } = ev {
240 let Some(me) = w.upgrade() else { break };
241 me.sync_peer(&node_id).await;
242 }
243 }
244 });
245 }
246
247 pub(super) fn serve(&self, ctx: &PeerCtx, conn: u64, req: SyncMsg) -> Vec<serde_json::Value> {
253 let SyncMsg::Request { grant_after, approval_after } = req else {
254 return Vec::new();
255 };
256 if !self.sync_buckets.take(Budget::Sync, conn, 1.0) {
257 return Vec::new();
258 }
259 let peer = &ctx.peer_id;
260 let fresh = grant_after.is_none() && approval_after.is_none();
261 if fresh {
262 let now = Instant::now();
263 if let Some(t) = self.served.get(peer)
264 && now.duration_since(*t) < self.config.sync_min_gap
265 {
266 return Vec::new();
267 }
268 self.served.insert(peer.clone(), now);
269 } else {
270 let ok = self.sessions.get(peer).is_some_and(|s| {
271 s.opened.elapsed() < PENDING_TTL
272 && s.pages < self.config.max_pages
273 && not_behind(&grant_after, &s.grant_last)
274 && not_behind(&approval_after, &s.approval_last)
275 });
276 if !ok {
277 return Vec::new();
278 }
279 }
280 let page = self.build_page(&grant_after, &approval_after);
281 if fresh {
282 self.sessions.insert(
283 peer.clone(),
284 ServeSession {
285 opened: Instant::now(),
286 pages: 1,
287 grant_last: page.last_grant,
288 approval_last: page.last_approval,
289 },
290 );
291 } else if let Some(mut s) = self.sessions.get_mut(peer) {
292 s.pages += 1;
293 s.grant_last = page.last_grant.or_else(|| s.grant_last.take());
294 s.approval_last = page.last_approval.or_else(|| s.approval_last.take());
295 }
296 serde_json::to_value(page.msg).map(|v| vec![v]).unwrap_or_default()
297 }
298
299 fn ban(&self, peer: &str) {
300 self.banned.insert(peer.to_owned(), Instant::now());
301 self.sink.emit(LicenceEvent::SyncBadSignature { peer: peer.to_owned() });
302 tracing::warn!(peer, "sync response carried a bad signature; peer banned from sync");
303 }
304
305 fn step(&self, peer: &str, r: Result<Receipt, ExchangeError>, fwd: Option<Forward>) -> Option<bool> {
310 match r {
311 Ok(Receipt::New) => {
312 if let Some(f) = fwd {
313 self.forward(peer, f);
314 }
315 }
316 Err(ExchangeError::Licence(LicenceError::BadSignature)) => return Some(true),
317 Err(ExchangeError::RateLimited) => return Some(false),
318 _ => {}
319 }
320 None
321 }
322
323 pub(super) fn absorb(&self, peer: &str, conn: u64, p: Pending, msg: SyncMsg) -> Vec<serde_json::Value> {
325 let SyncMsg::Response { binding, grants, approvals, more_grants, more_approvals } = msg
326 else {
327 return Vec::new();
328 };
329 if p.sent.elapsed() >= PENDING_TTL || !self.sync_buckets.take(Budget::Sync, conn, 1.0) {
330 return Vec::new();
331 }
332 let (mut entries, mut bytes) = (0usize, 0usize);
333 let mut within_caps = |e: &SignedEnvelope| {
334 entries += 1;
335 bytes += size(e);
336 entries <= SYNC_MAX_ENTRIES && bytes <= SYNC_MAX_BYTES
337 };
338 let spend = Spend::Sync(conn);
339 let mut abort = None;
340 if let Some(b) = binding
341 && within_caps(&b)
342 {
343 abort = self.step(peer, self.accept_binding(&b, spend), Some(Forward::Binding(b.clone())));
344 }
345 let mut last_grant = None;
346 for g in &grants {
347 if abort.is_some() || !within_caps(g) {
348 break;
349 }
350 let parsed = serde_json::from_str::<CheckoutGrant>(&g.payload).ok();
351 let fwd = parsed
353 .as_ref()
354 .filter(|c| c.is_withdrawal())
355 .map(|_| Forward::Grant(GrantMsg::Grant(g.clone())));
356 abort = self.step(peer, self.accept_grant(g, spend), fwd);
357 last_grant = parsed.map(|c| GrantCursor { seq: c.seq, cog_id: c.cog_id, version: c.version });
358 }
359 let mut last_approval = None;
360 for a in &approvals {
361 if abort.is_some() || !within_caps(a) {
362 break;
363 }
364 let parsed = serde_json::from_str::<super::Approval>(&a.payload).ok();
365 abort = self.step(peer, self.accept_approval(a, spend), None);
366 last_approval = parsed.map(|c| c.content_key());
367 }
368 match abort {
369 Some(true) => self.ban(peer),
370 Some(false) => {}
371 None => return self.next_page(peer, p, more_grants, more_approvals, last_grant, last_approval),
372 }
373 Vec::new()
374 }
375
376 fn next_page(
379 &self,
380 peer: &str,
381 p: Pending,
382 more_grants: bool,
383 more_approvals: bool,
384 last_grant: Option<GrantCursor>,
385 last_approval: Option<String>,
386 ) -> Vec<serde_json::Value> {
387 if !(more_grants || more_approvals) || p.pages + 1 >= self.config.max_pages {
388 return Vec::new();
389 }
390 let grant_after = last_grant.or_else(|| p.grant_after.clone());
391 let approval_after = last_approval.or_else(|| p.approval_after.clone());
392 if grant_after == p.grant_after && approval_after == p.approval_after {
393 return Vec::new(); }
395 self.pending.insert(
396 peer.to_owned(),
397 Pending {
398 sent: Instant::now(),
399 pages: p.pages + 1,
400 grant_after: grant_after.clone(),
401 approval_after: approval_after.clone(),
402 },
403 );
404 serde_json::to_value(SyncMsg::Request { grant_after, approval_after })
405 .map(|v| vec![v])
406 .unwrap_or_default()
407 }
408}
409
410pub(super) struct SyncSink(pub(super) std::sync::Arc<LicenceExchange>);
412
413impl PeerControlSink for SyncSink {
414 fn on_peer_control(&self, ctx: &PeerCtx, conn: u64, payload: &serde_json::Value) -> Vec<serde_json::Value> {
415 let ex = &self.0;
416 if !ex.admission.admitted(ctx) || ex.local().is_err() || ex.is_banned(&ctx.peer_id) {
417 return Vec::new();
418 }
419 match payload.get("op").and_then(|o| o.as_str()) {
420 Some("request") => match SyncMsg::deserialize(payload) {
421 Ok(req) => ex.serve(ctx, conn, req),
422 Err(_) => {
423 tracing::warn!(peer = %ctx.peer_id, "malformed licence sync request");
424 Vec::new()
425 }
426 },
427 Some("response") => {
428 let open = ex.pending.get(&ctx.peer_id).is_some_and(|p| p.sent.elapsed() < PENDING_TTL);
430 if !open {
431 return Vec::new();
432 }
433 let Ok(msg) = SyncMsg::deserialize(payload) else {
434 tracing::warn!(peer = %ctx.peer_id, "malformed licence sync response");
435 return Vec::new();
436 };
437 if let Some((_, p)) = ex.pending.remove(&ctx.peer_id) {
438 ex.retries.remove(&ctx.peer_id);
439 self.apply(ctx.peer_id.clone(), conn, p, msg);
440 }
441 Vec::new()
442 }
443 _ => Vec::new(),
444 }
445 }
446}
447
448impl SyncSink {
449 fn apply(&self, peer: String, conn: u64, p: Pending, msg: SyncMsg) {
453 let ex = self.0.clone();
454 let Ok(handle) = tokio::runtime::Handle::try_current() else {
455 ex.absorb(&peer, conn, p, msg);
456 return;
457 };
458 handle.spawn(async move {
459 let (e2, peer2) = (ex.clone(), peer.clone());
460 match tokio::task::spawn_blocking(move || e2.absorb(&peer2, conn, p, msg)).await {
461 Ok(next) => {
462 for v in next {
463 ex.send_to(&peer, COG_SYNC_TOPIC, v).await;
464 }
465 }
466 Err(e) => tracing::warn!(peer, error = %e, "licence sync page failed"),
467 }
468 });
469 }
470}