Skip to main content

monetize_estate_side/pump/
puller.rs

1//! **The puller**: report, pull, verify, run, ack — the box's whole side of
2//! `monetize.v1.Fleet`, over any [`Link`] (the gRPC client in [`super::grpc`], an in-process
3//! guardian in the tests), running each order through the product's [`PumpBox`].
4//!
5//! Moved from Holger's `holger-fleet/src/puller.rs` (P4a). What was Holger's there and is the
6//! product's here: the order kinds and their bodies ([`PumpBox::run`]), the health words.
7
8use std::collections::BTreeMap;
9use std::path::{Path, PathBuf};
10
11use monetize_embed::fleet::{ask, call, kind, order_message, Ask, FleetCall, OrderAck, OrdersAfter, Report, SignedOrder, LONG_POLL_MAX_MS};
12use monetize_embed::VerifyingKey;
13use serde::{Deserialize, Serialize};
14
15use super::key::BoxKey;
16
17/// What the puller remembers across restarts.
18pub const STATE_REL: &str = "fleet/pulled.json";
19
20/// Who this box is on the wire.
21#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
22pub struct Addressee {
23    pub product: String,
24    pub instance: String,
25    /// `main` | `twin`.
26    pub role: String,
27}
28
29/// Why a call failed: monetize's word (`unauthenticated`, `unavailable`, …) and its sentence.
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Fault {
32    pub code: String,
33    pub message: String,
34}
35
36impl Fault {
37    pub fn new(code: impl Into<String>, message: impl Into<String>) -> Fault {
38        Fault { code: code.into(), message: message.into() }
39    }
40}
41
42impl std::fmt::Display for Fault {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        write!(f, "{}: {}", self.code, self.message)
45    }
46}
47
48/// One signed call as it goes on the wire.
49#[derive(Debug, Clone)]
50pub struct Signed {
51    pub call: FleetCall,
52    pub signature: Vec<u8>,
53    /// The box's public key, sent until monetize has heard the box once (its pin).
54    pub box_key: Vec<u8>,
55}
56
57/// **What an ask is answered**: the case it opened (a remedy), monetize's words, and a page
58/// to open (empty for every ask a box makes today).
59#[derive(Debug, Clone, Default, PartialEq, Eq)]
60pub struct Asked {
61    pub case_id: String,
62    pub words: String,
63    pub url: String,
64}
65
66/// **The three calls**, over whatever carries them.
67pub trait Link {
68    /// Answers monetize's `next_report_ms`.
69    fn report(&mut self, s: Signed) -> Result<u64, Fault>;
70    fn orders(&mut self, s: Signed) -> Result<Vec<SignedOrder>, Fault>;
71    fn ask(&mut self, s: Signed) -> Result<Asked, Fault>;
72    /// [`Link::ask`] with the answer's BYTES (`FleetAskAnswer.answer`): the file a `fetch` ask
73    /// pulls (MONETIZE-FIX). A link that carries no bytes answers none.
74    fn ask_bytes(&mut self, s: Signed) -> Result<(Asked, Vec<u8>), Fault> {
75        self.ask(s).map(|a| (a, Vec::new()))
76    }
77}
78
79/// **A `Fleet.Ask` from inside a running order** (a pulled growth's `grow_set`), signed by this
80/// box, on the link the order came on.
81pub trait Asker {
82    fn ask(&mut self, a: &Ask) -> Result<Asked, Fault>;
83    /// [`Asker::ask`] with the answer's bytes ([`Link::ask_bytes`]).
84    fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
85        self.ask(a).map(|x| (x, Vec::new()))
86    }
87}
88
89/// **A pulled growth's disk resize** (`Fleet.Ask{grow_set}`, every member, `gib` a disk): monetize's
90/// words, or the case it opened when it said none.
91pub fn grow_set(asker: &mut dyn Asker, gib: u64) -> Result<String, String> {
92    let a = Ask { kind: ask::GROW_SET.into(), body: serde_json::json!({ "members": [], "gib": gib }) };
93    let Asked { case_id: case, words, .. } = asker.ask(&a).map_err(|f| f.to_string())?;
94    Ok(if words.is_empty() { case } else { words })
95}
96
97/// **The file a product order carries, pulled** (MONETIZE-FIX 2026-11-01: the ONE box never
98/// pushes a file into an estate): `Fleet.Ask{fetch}` on the order's own link. The ONE box answers
99/// only a file an order queued for this box names, checked against its sha256 there; its size
100/// is checked here before it is handed to the product's executor.
101pub fn fetch(asker: &mut dyn Asker, f: &monetize_embed::fleet::Fetch) -> Result<Vec<u8>, String> {
102    let a = Ask { kind: ask::FETCH.into(), body: serde_json::json!({ "id": f.id }) };
103    let (_, bytes) = asker.ask_bytes(&a).map_err(|e| e.to_string())?;
104    if bytes.len() as u64 != f.bytes {
105        return Err(format!("the file pulled for {} is {} bytes, and the order names {}", f.param, bytes.len(), f.bytes));
106    }
107    Ok(bytes)
108}
109
110/// What the product's executor knows about the order's context.
111pub struct Ctx<'a> {
112    pub me: &'a Addressee,
113    /// The monetize key the order verified under (a fact or snapshot inside it is verified again).
114    pub key: &'a VerifyingKey,
115    pub now_unix_ms: u64,
116}
117
118/// **The product's half of the pump**: what its box reports, and what it does with an order.
119pub trait PumpBox {
120    /// The box's health words for `Fleet.Report` (`serving`, `data`, `serves_at`, `twin`, `epoch`).
121    fn health(&self) -> BTreeMap<String, String> {
122        BTreeMap::new()
123    }
124    /// `monetize_product::pull::ProductReport` as JSON — the main that serves sends it.
125    fn product_report(&self) -> Option<String> {
126        None
127    }
128    /// A twin reviews itself on every report (G12): its review as JSON.
129    fn own_review(&self) -> Option<String> {
130        None
131    }
132    /// **Run one order that passed [`check`]** (signed by monetize, for this box, newer, in its
133    /// window): decode its body, run it through the product's own verbs, and answer its ack. A
134    /// kind or body the product does not take is an ack not-ok, by name. `ask` is
135    /// `Fleet.Ask` on the order's own link ([`grow_set`]).
136    fn run(&self, ctx: &Ctx, order: &SignedOrder, ask: &mut dyn Asker) -> OrderAck;
137}
138
139/// How far ahead of this box's clock an order may be dated (Gunnar's order check: five minutes).
140/// ONE constant with monetize-embed's `fleet::verify_order` (T16, A28 M11).
141pub use monetize_embed::fleet::FUTURE_SKEW_MS;
142
143/// What [`check`] made of one wire order, before its body is read.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub enum Checked {
146    /// Not monetize's: never run, never acked, the sequence does not move.
147    Forged(String),
148    /// Monetize's, but not for this box to run now: acked not-ok with these words.
149    Refused(String),
150    /// Monetize's, for this box, newer than the last one taken, in its window: run it.
151    Fresh,
152}
153
154/// **The box's check of one order** (the order's body is the product's): signed by monetize
155/// (monetize-embed [`order_message`] under `key`), then for this box, newer than the last order
156/// taken, inside its window (`not_after` 0 = none), dated no more than [`FUTURE_SKEW_MS`] ahead
157/// of this box's clock. An order that does not verify is FORGED —
158/// never run, never acked, and it never moves the sequence (a forged high seq must not silence
159/// the real ones). Everything else wrong with it is a REFUSAL, acked by name.
160pub fn check(o: &SignedOrder, me: &Addressee, key: &VerifyingKey, last_seq: u64, now_unix_ms: u64) -> Checked {
161    // STRICT (P4c, A28 M11): no small-order key or `R`, as Gunnar's own check always was.
162    let signed = ed25519_dalek::Signature::from_slice(&o.signature).map(|s| key.verify_strict(&order_message(o), &s).is_ok()).unwrap_or(false);
163    if !signed {
164        return Checked::Forged(format!("order {} ({}) is not signed by this product's monetize key", o.seq, o.kind));
165    }
166    if o.product != me.product || o.instance != me.instance || o.role != me.role {
167        return Checked::Refused(format!("order {} is for {}/{}/{}, and this box is {}/{}/{}", o.seq, o.product, o.instance, o.role, me.product, me.instance, me.role));
168    }
169    if o.seq <= last_seq {
170        return Checked::Refused(format!("order {} is not newer than {last_seq}, the last one this box took: a replay", o.seq));
171    }
172    if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
173        return Checked::Refused(format!("order {} expired {} s ago", o.seq, (now_unix_ms - o.not_after_unix_ms) / 1000));
174    }
175    if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
176        return Checked::Refused(format!("order {} is dated {} s in the future; whose clock is wrong?", o.seq, (o.issued_unix_ms - now_unix_ms) / 1000));
177    }
178    Checked::Fresh
179}
180
181/// What is kept across restarts.
182#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
183#[serde(default)]
184pub struct Pulled {
185    /// The last order TAKEN (persisted before it runs: an order runs at most once).
186    pub last_seq: u64,
187    /// Acks not yet carried by a Report monetize answered.
188    pub pending: Vec<OrderAck>,
189    /// A review a `review_twin` order made: it rides the next Report only.
190    pub twin_review: Option<String>,
191}
192
193/// What one pull did.
194#[derive(Debug, Clone, Default, PartialEq, Eq)]
195pub struct Pull {
196    pub acks: Vec<OrderAck>,
197    pub forged: Vec<String>,
198}
199
200pub struct Puller {
201    pub me: Addressee,
202    pub build: String,
203    key: BoxKey,
204    state: Option<PathBuf>,
205    pub pulled: Pulled,
206    heard: bool,
207}
208
209impl Puller {
210    /// A puller that remembers nothing on disk (tests).
211    pub fn new(me: Addressee, key: BoxKey, build: impl Into<String>) -> Puller {
212        Puller { me, build: build.into(), key, state: None, pulled: Pulled::default(), heard: false }
213    }
214
215    /// The box's puller over its data root: its key (minted on first use) and what it took.
216    pub fn at(root: &Path, me: Addressee, build: impl Into<String>) -> std::io::Result<Puller> {
217        let key = BoxKey::load_or_mint(root)?;
218        let state = root.join(STATE_REL);
219        let pulled = match std::fs::read(&state) {
220            Ok(bytes) => serde_json::from_slice(&bytes).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, format!("{}: {e}", state.display())))?,
221            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Pulled::default(),
222            Err(e) => return Err(e),
223        };
224        Ok(Puller { me, build: build.into(), key, state: Some(state), pulled, heard: false })
225    }
226
227    pub fn box_public(&self) -> [u8; 32] {
228        self.key.public()
229    }
230
231    fn save(&self) -> Result<(), Fault> {
232        let Some(path) = &self.state else { return Ok(()) };
233        let bytes = serde_json::to_vec(&self.pulled).map_err(|e| Fault::new("internal", e.to_string()))?;
234        crate::write_atomic(path, &bytes, false).map_err(|e| Fault::new("io", format!("{}: {e}", path.display())))
235    }
236
237    fn signed(&self, which: &str, body: String, now_unix_ms: u64) -> Signed {
238        let c = FleetCall { product: self.me.product.clone(), instance: self.me.instance.clone(), role: self.me.role.clone(), call: which.to_owned(), body, at_unix_ms: now_unix_ms };
239        let signature = self.key.sign(&c);
240        Signed { call: c, signature, box_key: if self.heard { Vec::new() } else { self.key.public().to_vec() } }
241    }
242
243    /// **Report** this box's health, the product's own part (the main that serves), its own
244    /// twin review (a twin, every report) and every pending ack. The acks leave the queue
245    /// once monetize answered. Returns monetize's `next_report_ms`.
246    pub fn report(&mut self, link: &mut dyn Link, health: BTreeMap<String, String>, product: Option<String>, own_review: Option<String>, now_unix_ms: u64) -> Result<u64, Fault> {
247        let sent = self.pulled.pending.len();
248        let report = Report {
249            build: self.build.clone(),
250            health,
251            acked: self.pulled.pending.clone(),
252            twin_review: own_review.or_else(|| self.pulled.twin_review.clone()),
253            product,
254            measured_unix_ms: now_unix_ms,
255        };
256        let body = serde_json::to_string(&report).map_err(|e| Fault::new("internal", e.to_string()))?;
257        let next = link.report(self.signed(call::REPORT, body, now_unix_ms))?;
258        self.heard = true;
259        self.pulled.pending.drain(..sent);
260        self.pulled.twin_review = None;
261        self.save()?;
262        Ok(next)
263    }
264
265    /// **Pull** the orders after the last one taken, held up to `wait_ms` (≤ 25 s); [`check`]
266    /// each under `monetize_pubkey`, run it through `b` and queue its ack. Without a key nothing
267    /// is pulled: nothing could be verified. A `review_twin` that ran rides the next report.
268    pub fn pull(&mut self, link: &mut dyn Link, b: &dyn PumpBox, monetize_pubkey: Option<&[u8; 32]>, wait_ms: u64, now_unix_ms: u64) -> Result<Pull, Fault> {
269        let Some(raw) = monetize_pubkey else {
270            return Err(Fault::new("no_key", "this box has no monetize key yet, so no order could be verified: none is pulled"));
271        };
272        let key = VerifyingKey::from_bytes(raw).map_err(|e| Fault::new("no_key", format!("the monetize key is not an Ed25519 point: {e}")))?;
273        let body = serde_json::to_string(&OrdersAfter { after: self.pulled.last_seq, wait_ms: wait_ms.min(LONG_POLL_MAX_MS) }).map_err(|e| Fault::new("internal", e.to_string()))?;
274        let mut orders = link.orders(self.signed(call::ORDERS, body, now_unix_ms))?;
275        self.heard = true;
276        orders.sort_by_key(|o| o.seq);
277        let mut out = Pull::default();
278        for o in orders {
279            let checked = check(&o, &self.me, &key, self.pulled.last_seq, now_unix_ms);
280            if let Checked::Forged(why) = checked {
281                out.forged.push(why);
282                continue;
283            }
284            if o.seq <= self.pulled.last_seq {
285                continue;
286            }
287            self.pulled.last_seq = o.seq;
288            self.save()?;
289            let ack = match checked {
290                Checked::Fresh => {
291                    let me = self.me.clone();
292                    let ctx = Ctx { me: &me, key: &key, now_unix_ms };
293                    let ack = {
294                        let mut via = AskVia { puller: &mut *self, link: &mut *link, now_unix_ms };
295                        b.run(&ctx, &o, &mut via)
296                    };
297                    if o.kind == kind::REVIEW_TWIN && ack.ok {
298                        self.pulled.twin_review = Some(ack.receipt_json.clone());
299                    }
300                    ack
301                }
302                Checked::Refused(detail) => OrderAck { seq: o.seq, ok: false, detail, receipt_json: String::new() },
303                Checked::Forged(_) => unreachable!("forged orders were skipped"),
304            };
305            self.pulled.pending.push(ack.clone());
306            self.save()?;
307            out.acks.push(ack);
308        }
309        Ok(out)
310    }
311
312    /// **Ask** monetize (`grow_set`, or a remedy: it opens a case, never approves one).
313    pub fn ask(&mut self, link: &mut dyn Link, a: &Ask, now_unix_ms: u64) -> Result<Asked, Fault> {
314        let body = serde_json::to_string(a).map_err(|e| Fault::new("internal", e.to_string()))?;
315        let answer = link.ask(self.signed(call::ASK, body, now_unix_ms))?;
316        self.heard = true;
317        Ok(answer)
318    }
319
320    /// [`Puller::ask`] with the answer's bytes ([`Link::ask_bytes`]).
321    pub fn ask_bytes(&mut self, link: &mut dyn Link, a: &Ask, now_unix_ms: u64) -> Result<(Asked, Vec<u8>), Fault> {
322        let body = serde_json::to_string(a).map_err(|e| Fault::new("internal", e.to_string()))?;
323        let answer = link.ask_bytes(self.signed(call::ASK, body, now_unix_ms))?;
324        self.heard = true;
325        Ok(answer)
326    }
327}
328
329/// An order's `Fleet.Ask`, signed by this box, on the link the order came on.
330struct AskVia<'a> {
331    puller: &'a mut Puller,
332    link: &'a mut dyn Link,
333    now_unix_ms: u64,
334}
335
336impl Asker for AskVia<'_> {
337    fn ask(&mut self, a: &Ask) -> Result<Asked, Fault> {
338        self.puller.ask(self.link, a, self.now_unix_ms)
339    }
340    fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
341        self.puller.ask_bytes(self.link, a, self.now_unix_ms)
342    }
343}
344
345#[cfg(test)]
346pub(crate) mod tests {
347    use super::*;
348    use ed25519_dalek::Signer as _;
349    use std::cell::RefCell;
350
351    pub const MONETIZE_SEED: [u8; 32] = [7u8; 32];
352
353    pub fn me(role: &str) -> Addressee {
354        Addressee { product: "a-product".into(), instance: "acme".into(), role: role.into() }
355    }
356
357    pub fn monetize_key() -> VerifyingKey {
358        ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).verifying_key()
359    }
360
361    fn mkey() -> [u8; 32] {
362        monetize_key().to_bytes()
363    }
364
365    /// One order for `role`, signed by `seed`.
366    pub fn signed(seq: u64, role: &str, kind: &str, body: &str, seed: [u8; 32]) -> SignedOrder {
367        let mut o = SignedOrder { seq, product: "a-product".into(), instance: "acme".into(), role: role.into(), kind: kind.into(), body: body.into(), issued_unix_ms: 9_000, not_after_unix_ms: 0, signature: Vec::new() };
368        o.signature = ed25519_dalek::SigningKey::from_bytes(&seed).sign(&order_message(&o)).to_bytes().to_vec();
369        o
370    }
371
372    /// Monetize in memory: a queue, and every call it heard.
373    #[derive(Default)]
374    pub struct Guardian {
375        pub queue: Vec<SignedOrder>,
376        pub heard: Vec<Signed>,
377    }
378
379    impl Link for Guardian {
380        fn report(&mut self, s: Signed) -> Result<u64, Fault> {
381            let r: Report = serde_json::from_str(&s.call.body).unwrap();
382            self.queue.retain(|o| !r.acked.iter().any(|a| a.seq == o.seq));
383            self.heard.push(s);
384            Ok(30_000)
385        }
386        fn orders(&mut self, s: Signed) -> Result<Vec<SignedOrder>, Fault> {
387            let after: OrdersAfter = serde_json::from_str(&s.call.body).unwrap();
388            self.heard.push(s);
389            Ok(self.queue.iter().filter(|o| o.seq > after.after).cloned().collect())
390        }
391        fn ask(&mut self, s: Signed) -> Result<Asked, Fault> {
392            let _: Ask = serde_json::from_str(&s.call.body).unwrap();
393            self.heard.push(s);
394            Ok(Asked { case_id: "c-1".into(), words: "resized".into(), url: String::new() })
395        }
396    }
397
398    /// A product box: it runs `note` (its words) and `grow` (asks `grow_set`), reviews as a
399    /// twin, and refuses any other kind by name.
400    #[derive(Default)]
401    pub struct Fake {
402        pub ran: RefCell<Vec<(u64, String)>>,
403    }
404
405    impl PumpBox for Fake {
406        fn run(&self, ctx: &Ctx, o: &SignedOrder, ask: &mut dyn Asker) -> OrderAck {
407            self.ran.borrow_mut().push((o.seq, o.kind.clone()));
408            let said = match o.kind.as_str() {
409                "note" => Ok(format!("noted for {}", ctx.me.role)),
410                "grow" => grow_set(ask, 64),
411                kind::REVIEW_TWIN => return OrderAck { seq: o.seq, ok: true, detail: "reviewed".into(), receipt_json: r#"{"complete":"yes"}"#.into() },
412                other => Err(format!("order {}: this product does not know the order kind {other:?}", o.seq)),
413            };
414            match said {
415                Ok(detail) => OrderAck { seq: o.seq, ok: true, detail, receipt_json: String::new() },
416                Err(detail) => OrderAck { seq: o.seq, ok: false, detail, receipt_json: String::new() },
417            }
418        }
419    }
420
421    #[test]
422    fn an_order_is_pulled_run_and_acked_in_the_next_report_and_never_twice() {
423        let b = Fake::default();
424        let mut g = Guardian::default();
425        g.queue.push(signed(4, "main", "note", "{}", MONETIZE_SEED));
426        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
427        let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
428        assert_eq!(pulled.acks.len(), 1);
429        assert!(pulled.acks[0].ok, "{pulled:?}");
430        assert_eq!(pulled.acks[0].detail, "noted for main");
431        p.report(&mut g, BTreeMap::from([("serving".into(), "yes".into())]), Some("{}".into()), None, 10_001).unwrap();
432        let r: Report = serde_json::from_str(&g.heard.last().unwrap().call.body).unwrap();
433        assert_eq!(r.acked.iter().map(|a| (a.seq, a.ok)).collect::<Vec<_>>(), vec![(4, true)]);
434        assert_eq!((r.product.as_deref(), r.build.as_str()), (Some("{}"), "test"));
435        assert!(g.queue.is_empty() && p.pulled.pending.is_empty());
436        assert!(p.pull(&mut g, &b, Some(&mkey()), 0, 10_002).unwrap().acks.is_empty(), "nothing runs twice");
437        assert_eq!(b.ran.borrow().len(), 1);
438    }
439
440    /// ★ **The one order check is STRICT Ed25519** (P4c, A28 M11: Gunnar's gunnar-repair copy
441    /// always was, `verify_strict`). Under a small-order key (the identity point) the all-zero
442    /// `R = identity, S = 0` signature satisfies the plain equation for ANY message; a key fed
443    /// wrong must not turn every order into monetize's. A real key still verifies.
444    #[test]
445    fn a_weak_key_signs_no_order_and_a_real_one_still_does() {
446        let mut identity = [0u8; 32];
447        identity[0] = 1;
448        let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
449        let mut o = signed(3, "main", "recharge_from_twin", "{}", MONETIZE_SEED);
450        o.signature = { let mut s = vec![0u8; 64]; s[0] = 1; s };
451        assert!(matches!(check(&o, &me("main"), &weak, 0, 9_500), Checked::Forged(_)), "a weak key verifies nothing");
452        let real = signed(3, "main", "recharge_from_twin", "{}", MONETIZE_SEED);
453        assert_eq!(check(&real, &me("main"), &monetize_key(), 0, 9_500), Checked::Fresh);
454    }
455
456    /// ★ **An order dated more than five minutes ahead of this box's clock is refused** (P4c,
457    /// A28 M11: Gunnar's order check always refused it). Five minutes of skew still runs.
458    #[test]
459    fn an_order_dated_ahead_of_the_box_clock_is_refused_by_name() {
460        let mut ahead = signed(3, "main", "note", "{}", MONETIZE_SEED);
461        ahead.issued_unix_ms = 9_000 + FUTURE_SKEW_MS + 1;
462        ahead.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).sign(&order_message(&ahead)).to_bytes().to_vec();
463        match check(&ahead, &me("main"), &monetize_key(), 0, 9_000) {
464            Checked::Refused(why) => assert!(why.contains("in the future"), "{why}"),
465            other => panic!("{other:?}"),
466        }
467        let mut skew = ahead.clone();
468        skew.issued_unix_ms = 9_000 + FUTURE_SKEW_MS;
469        skew.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).sign(&order_message(&skew)).to_bytes().to_vec();
470        assert_eq!(check(&skew, &me("main"), &monetize_key(), 0, 9_000), Checked::Fresh);
471    }
472
473    #[test]
474    fn a_forged_order_never_runs_never_acks_and_never_moves_the_sequence() {
475        let b = Fake::default();
476        let mut g = Guardian::default();
477        g.queue.push(signed(u64::MAX, "main", "note", "{}", [9u8; 32]));
478        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
479        let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
480        assert!(pulled.acks.is_empty() && pulled.forged.len() == 1);
481        assert_eq!(p.pulled.last_seq, 0);
482        assert!(b.ran.borrow().is_empty());
483    }
484
485    /// ★ The check before the body: another box's order, an expired one and a replay are acked
486    /// not-ok by name and never reach the product; a kind the product does not take is ITS
487    /// refusal, acked by name too.
488    #[test]
489    fn another_boxs_an_expired_or_an_unknown_order_is_refused_by_name() {
490        let k = monetize_key();
491        let twin = signed(5, "twin", "note", "{}", MONETIZE_SEED);
492        assert!(matches!(check(&twin, &me("main"), &k, 0, 10_000), Checked::Refused(w) if w.contains("for a-product/acme/twin")));
493        let mut late = signed(5, "main", "note", "{}", MONETIZE_SEED);
494        late.not_after_unix_ms = 1_000;
495        let late = { let mut l = late.clone(); l.signature = signed_like(&l); l };
496        assert!(matches!(check(&late, &me("main"), &k, 0, 61_000), Checked::Refused(w) if w.contains("expired 60 s ago")));
497        assert!(matches!(check(&signed(5, "main", "note", "{}", MONETIZE_SEED), &me("main"), &k, 5, 1), Checked::Refused(w) if w.contains("a replay")));
498        assert_eq!(check(&signed(6, "main", "note", "{}", MONETIZE_SEED), &me("main"), &k, 5, 1), Checked::Fresh);
499        let b = Fake::default();
500        let mut g = Guardian::default();
501        g.queue.push(twin);
502        g.queue.push(signed(7, "main", "reboot_the_moon", "{}", MONETIZE_SEED));
503        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
504        let pulled = p.pull(&mut g, &b, Some(&mkey()), 0, 10_000).unwrap();
505        assert_eq!(pulled.acks.iter().map(|a| (a.seq, a.ok)).collect::<Vec<_>>(), vec![(5, false), (7, false)]);
506        assert!(pulled.acks[1].detail.contains("does not know the order kind \"reboot_the_moon\""), "{pulled:?}");
507        assert_eq!(*b.ran.borrow(), vec![(7, "reboot_the_moon".to_string())], "only the box's own order reaches the product");
508    }
509
510    fn signed_like(o: &SignedOrder) -> Vec<u8> {
511        ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED).sign(&order_message(o)).to_bytes().to_vec()
512    }
513
514    #[test]
515    fn without_a_monetize_key_nothing_is_pulled_and_monetize_is_not_even_asked() {
516        let mut g = Guardian::default();
517        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
518        assert_eq!(p.pull(&mut g, &Fake::default(), None, 0, 10_000).unwrap_err().code, "no_key");
519        assert!(g.heard.is_empty());
520    }
521
522    #[test]
523    fn the_first_call_carries_the_box_key_and_later_ones_do_not() {
524        let mut g = Guardian::default();
525        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
526        p.report(&mut g, BTreeMap::new(), None, None, 1).unwrap();
527        p.report(&mut g, BTreeMap::new(), None, None, 2).unwrap();
528        assert_eq!(g.heard[0].box_key, p.box_public().to_vec());
529        assert!(g.heard[1].box_key.is_empty());
530    }
531
532    #[test]
533    fn a_twins_own_review_rides_every_report_and_an_ordered_one_the_next_only() {
534        let mut g = Guardian::default();
535        let mut p = Puller::new(me("twin"), BoxKey::from_seed([4u8; 32]), "test");
536        g.queue.push(signed(1, "twin", "review_twin", "{}", MONETIZE_SEED));
537        p.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10).unwrap();
538        p.report(&mut g, BTreeMap::new(), None, None, 11).unwrap();
539        p.report(&mut g, BTreeMap::new(), None, None, 12).unwrap();
540        p.report(&mut g, BTreeMap::new(), None, Some(r#"{"own":1}"#.into()), 13).unwrap();
541        let said: Vec<Option<String>> = g.heard.iter().filter(|s| s.call.call == "report").map(|s| serde_json::from_str::<Report>(&s.call.body).unwrap().twin_review).collect();
542        assert!(said[0].as_deref().is_some_and(|r| r.contains("complete")), "{said:?}");
543        assert_eq!(said[1], None);
544        assert_eq!(said[2].as_deref(), Some(r#"{"own":1}"#));
545    }
546
547    /// ★ An order's `Fleet.Ask` goes on the order's own link, signed by this box.
548    #[test]
549    fn an_order_asks_monetize_on_its_own_link_signed_by_the_box() {
550        let mut g = Guardian::default();
551        g.queue.push(signed(2, "main", "grow", "{}", MONETIZE_SEED));
552        let mut p = Puller::new(me("main"), BoxKey::from_seed([3u8; 32]), "test");
553        let pulled = p.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10_000).unwrap();
554        assert_eq!(pulled.acks[0].detail, "resized");
555        let asked = g.heard.iter().find(|s| s.call.call == "ask").expect("asked");
556        let a: Ask = serde_json::from_str(&asked.call.body).unwrap();
557        assert_eq!((a.kind.as_str(), a.body["gib"].as_u64()), ("grow_set", Some(64)));
558        let vk = VerifyingKey::from_bytes(&p.box_public()).unwrap();
559        monetize_embed::fleet::verify_fleet_call(&asked.call, &asked.signature, &vk, 10_000).expect("the ask is the box's");
560    }
561
562    #[test]
563    fn the_puller_remembers_what_it_took_across_a_restart() {
564        let dir = tempfile::tempdir().unwrap();
565        let mut g = Guardian::default();
566        g.queue.push(signed(6, "main", "note", "{}", MONETIZE_SEED));
567        let mut p = Puller::at(dir.path(), me("main"), "test").unwrap();
568        let key = p.box_public();
569        p.pull(&mut g, &Fake::default(), Some(&mkey()), 0, 10_000).unwrap();
570        drop(p);
571        let p = Puller::at(dir.path(), me("main"), "test").unwrap();
572        assert_eq!((p.box_public(), p.pulled.last_seq, p.pulled.pending.len()), (key, 6, 1));
573    }
574
575    /// ★ MONETIZE-FIX: **a product order's file is pulled with a `fetch` ask**, naming the file;
576    /// bytes of another size than the order names are refused, never handed on.
577    #[test]
578    fn a_product_orders_file_is_pulled_by_a_fetch_ask_and_checked() {
579        struct Shop(Vec<u8>, Vec<Ask>);
580        impl Asker for Shop {
581            fn ask(&mut self, _: &Ask) -> Result<Asked, Fault> {
582                unreachable!("a fetch asks for its bytes")
583            }
584            fn ask_bytes(&mut self, a: &Ask) -> Result<(Asked, Vec<u8>), Fault> {
585                self.1.push(a.clone());
586                Ok((Asked::default(), self.0.clone()))
587            }
588        }
589        let f = monetize_embed::fleet::Fetch { id: "ab".repeat(32), bytes: 3, param: "bundle".into() };
590        let mut shop = Shop(b"abc".to_vec(), Vec::new());
591        assert_eq!(fetch(&mut shop, &f).unwrap(), b"abc");
592        assert_eq!((shop.1[0].kind.as_str(), shop.1[0].body["id"].as_str()), (ask::FETCH, Some(f.id.as_str())));
593        let mut short = Shop(b"ab".to_vec(), Vec::new());
594        assert!(fetch(&mut short, &f).unwrap_err().contains("names 3"));
595    }
596}