Skip to main content

holger_fleet/
exec.rs

1//! **The order executor**: one verified order → one of holger's own verbs → one ack.
2//!
3//! The host hands a [`HolgerBox`] (holger-server-lib's `fleet_box`, a test's fake). Every
4//! verb defaults to a refusal BY NAME, so a box that cannot do one says why instead of
5//! the guardian waiting. The money is checked HERE as well as in the box: an entitlement,
6//! a snapshot and a growth step are verified under the monetize key before the box is
7//! asked to move anything.
8
9use std::collections::BTreeMap;
10
11use monetize_embed::fleet::{GrowGoAhead, OrderAck};
12use monetize_product::pull::{AddPrincipal, GrowRoom, PurgeAnswer, PurgeTenant};
13use monetize_product::EntitlementFact;
14
15use crate::order::Order;
16
17pub use crate::order::Recharge as RechargeAsk;
18
19/// 512-byte sectors in a GiB.
20pub const SECTORS_PER_GIB: u64 = 2_097_152;
21
22/// What an entitlement did on the box.
23#[derive(Debug, Clone, PartialEq, Eq)]
24pub enum Applied {
25    /// Stored: the words say what.
26    New(String),
27    /// The box already holds this fact or a newer one: nothing moved.
28    Held(String),
29}
30
31/// Where the grow machine stands, as a pulled growth reads it.
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum GrowState {
34    Running(String),
35    /// `/data` is off; the machine waits for the members to come back larger.
36    WaitingMembers(String),
37    Done(String),
38    Failed(String),
39}
40
41/// **`Fleet.Ask{grow_set}`**: monetize sizes THIS box's own data disks.
42pub trait GrowSet {
43    fn grow_set(&mut self, gib: u64) -> Result<String, String>;
44}
45
46/// A caller with no channel to ask on.
47pub struct NoAsk;
48impl GrowSet for NoAsk {
49    fn grow_set(&mut self, _: u64) -> Result<String, String> {
50        Err("this box has no pull channel to ask monetize for its disks".into())
51    }
52}
53
54/// **Holger's verbs, as an order reaches them.** Sync: the executor runs on a jera job's
55/// blocking thread.
56pub trait HolgerBox {
57    /// This box serves as the primary (not a twin, not awaiting its recharge).
58    fn serving(&self) -> bool;
59    /// The box's health words for `Fleet.Report` (`serving`, `data`, `serves_at`, `twin`, `epoch`).
60    fn health(&self) -> BTreeMap<String, String> {
61        BTreeMap::new()
62    }
63    /// `monetize_product::pull::ProductReport` as JSON — the main that serves sends it.
64    fn product_report(&self) -> Option<String> {
65        None
66    }
67    /// A twin reviews itself on every report (G12): its review as JSON.
68    fn own_review(&self) -> Option<String> {
69        None
70    }
71    /// Store one VERIFIED fact, newer wins.
72    fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
73        Err(format!(
74            "this box holds no entitlements, so the fact for {} is not applied",
75            fact.tenant.0
76        ))
77    }
78    /// Purge one repository (or answer its purge's state).
79    fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
80        Err(format!(
81            "this box purges no repositories, so {} is not purged",
82            p.tenant
83        ))
84    }
85    /// **A sold estate's resend sign-in link** (Gunnar's S10): mint a fresh first-login invite
86    /// for the order's account and mail it to the order's address. Default: this box mints none.
87    fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
88        let _ = (&s.to, &s.host, s.ttl_secs);
89        Err(format!(
90            "this box mints no sign-in links, so it cannot run sign_in_again for {}",
91            s.account
92        ))
93    }
94    /// **Keep the estate keys fed again** (monetize MAIL-RELAY + NIGHT-PUBREPO 2026-10-28): kept
95    /// beside the feed the box was laid with, which they go over. Default: this box keeps none.
96    fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
97        Err(format!(
98            "this box keeps no estate keys fed again, so {} is not kept",
99            r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
100        ))
101    }
102    /// **Keep one generic setting** (HOL-PUBSITE-FEED: a refeed's public-site switch, kept as
103    /// [`crate::order::PUBLIC_SITE_SETTING`]): stored as an administrator's write to
104    /// `/-/settings/<name>` is, audited, answering what it replaced. Default: this box keeps none.
105    fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
106        let _ = value;
107        Err(format!("this box keeps no settings, so {name} is not kept"))
108    }
109    /// A paid signup's repository (`a.username`), bound to `a.signup` (idempotent); answers the
110    /// namespace made. A name bound to another signup is refused starting `taken`.
111    fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
112        Err(format!(
113            "this box makes no repositories for a signup, so {} is not made",
114            a.username
115        ))
116    }
117    /// Keep the room wanted past the set and tell the operator.
118    fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
119        Err(format!(
120            "this box grows no data set, so {} bytes wanted cannot be kept",
121            g.need_bytes
122        ))
123    }
124    /// Do the box's data disks already carry `data_gib` a disk?
125    fn grow_carried(&self, data_gib: u64) -> Result<bool, String> {
126        Err(format!(
127            "this box grows no data set, so it cannot grow to {data_gib} GiB"
128        ))
129    }
130    /// The twin first (a main only): Ok once the twin holds main's whole set.
131    fn twin_holds(&self, data_gib: u64) -> Result<String, String> {
132        Err(format!(
133            "this box cannot see whether its twin holds {data_gib} GiB a disk"
134        ))
135    }
136    /// Start the growth to `data_gib` a disk under a one-time `nonce` (the step was verified).
137    fn grow_start(&self, data_gib: u64, nonce: &str) -> Result<GrowState, String> {
138        let _ = (data_gib, nonce);
139        Err("this box grows no data set".into())
140    }
141    /// Wait for the grow machine to stop moving (past `waiting-members` when told).
142    fn grow_settle(&self, past_waiting: bool) -> GrowState {
143        let _ = past_waiting;
144        GrowState::Failed("this box grows no data set".into())
145    }
146    /// Look at the members again (`give_up`: mount back what is there).
147    fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
148        let _ = give_up;
149        Err("this box grows no data set".into())
150    }
151    /// Fill `r.main` from `r.twin` (holger's recharge lane).
152    fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
153        Err(format!(
154            "this box runs no recharge lane, so {} is not filled from {}",
155            r.main, r.twin
156        ))
157    }
158    /// Fence the old primary at `primary` (this box itself when it names it): it refuses
159    /// every write from now on. Refused by name by default.
160    fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
161        let _ = epoch;
162        Err(format!("this box cannot fence the primary at {primary}: the guardian fences it at the cloud (the recharge's `fenced` proof)"))
163    }
164    /// Certify the twin at `twin` for this primary.
165    fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
166        let _ = principal;
167        Err(format!(
168            "this box cannot certify the twin at {twin}: the front's certification does"
169        ))
170    }
171    /// Take the twin role: follow `primary` (a FRESH box only, exactly once).
172    fn become_twin(&self, primary: &str) -> Result<String, String> {
173        Err(format!("this box cannot become the twin of {primary}"))
174    }
175    /// Review the twin (on a twin: itself), as JSON.
176    fn review(&self) -> Result<serde_json::Value, String> {
177        Err("this box cannot review a twin".into())
178    }
179}
180
181/// What the executor knows about the order's context: the shared pump's own (P4a).
182pub use monetize_estate_side::pump::Ctx;
183
184/// **A `refeed`, kept group by group** (Gunnar's `gunnar-repair` `exec::refeed`): the keys kept
185/// beside the feed first ([`HolgerBox::keep_refeed`]: the relay, the public repositories), then
186/// each generic setting it sets ([`HolgerBox::keep_setting`]: the public-site switch). The first
187/// refusal is the answer, saying what was kept before it; else every group's words.
188fn refeed<B: HolgerBox + ?Sized>(b: &B, r: &crate::order::Refeed) -> Result<String, String> {
189    let mut said: Vec<String> = Vec::new();
190    let kept = r.kept_keys();
191    if !kept.keys.is_empty() {
192        said.push(b.keep_refeed(&kept)?);
193    }
194    for (name, value) in r.settings() {
195        match b.keep_setting(name, &value) {
196            Ok(words) => said.push(words),
197            Err(why) if !said.is_empty() => {
198                return Err(format!("{why} (kept before it: {})", said.join("; ")))
199            }
200            Err(why) => return Err(why),
201        }
202    }
203    Ok(said.join("; "))
204}
205
206fn said(seq: u64, r: Result<String, String>) -> OrderAck {
207    match r {
208        Ok(detail) => OrderAck {
209            seq,
210            ok: true,
211            detail,
212            receipt_json: String::new(),
213        },
214        Err(detail) => OrderAck {
215            seq,
216            ok: false,
217            detail,
218            receipt_json: String::new(),
219        },
220    }
221}
222
223/// **Run one verified order** and say what came of it — the ack the next Report carries.
224/// Never panics, never retries: the guardian reads the ack.
225pub fn execute<B: HolgerBox + ?Sized>(
226    b: &B,
227    ctx: &Ctx,
228    seq: u64,
229    order: &Order,
230    ask: &mut dyn GrowSet,
231) -> OrderAck {
232    let not_serving = || {
233        Err::<String, String>("this box does not serve as the primary (a twin, or awaiting its recharge): nothing applied".into())
234    };
235    match order {
236        Order::Entitlement(fact) => {
237            if !b.serving() {
238                return said(seq, not_serving());
239            }
240            if let Err(e) = monetize_embed::signing::verify_fact(fact, ctx.key) {
241                return said(seq, Err(format!("the fact for {} does not verify under this box's monetize key ({e}): nothing moved", fact.tenant.0)));
242            }
243            said(
244                seq,
245                b.entitle(fact).map(|a| match a {
246                    Applied::New(w) | Applied::Held(w) => w,
247                }),
248            )
249        }
250        Order::RestateEntitlements(snap) => {
251            if !b.serving() {
252                return said(seq, not_serving());
253            }
254            if let Err(e) = monetize_embed::signing::verify_snapshot(snap, ctx.key) {
255                return said(seq, Err(format!("the snapshot does not verify under this box's monetize key ({e}): nothing restated")));
256            }
257            let (mut new, mut held) = (0usize, 0usize);
258            for fact in &snap.facts {
259                match b.entitle(fact) {
260                    Ok(Applied::New(_)) => new += 1,
261                    Ok(Applied::Held(_)) => held += 1,
262                    Err(e) => {
263                        return said(
264                            seq,
265                            Err(format!(
266                                "restated {new} before {} was refused: {e}",
267                                fact.tenant.0
268                            )),
269                        )
270                    }
271                }
272            }
273            said(seq, Ok(format!("restated {new} entitlement fact(s) ({held} already held as new or newer); nothing deleted")))
274        }
275        Order::PurgeTenant(p) => {
276            if !b.serving() {
277                return said(seq, not_serving());
278            }
279            match b.purge(p) {
280                Ok(answer) => OrderAck {
281                    seq,
282                    ok: true,
283                    detail: serde_json::to_string(&answer).unwrap_or_default(),
284                    receipt_json: String::new(),
285                },
286                Err(e) => said(seq, Err(e)),
287            }
288        }
289        Order::GrowRoom(g) => said(seq, b.want_room(g)),
290        Order::AddPrincipal(a) => {
291            if !b.serving() {
292                return said(seq, not_serving());
293            }
294            said(seq, b.add_principal(a))
295        }
296        Order::SignInAgain(s) => {
297            if !b.serving() {
298                return said(seq, not_serving());
299            }
300            said(seq, b.sign_in_again(s))
301        }
302        // Any box the order is for keeps it (the pump checked the role): a twin that serves later
303        // mails through it too.
304        Order::Refeed(r) => said(seq, refeed(b, r)),
305        Order::GrowGoAhead(g) => grow(b, ctx, seq, g, ask),
306        Order::RechargeFromTwin(r) => {
307            // Fence before recharge (Gunnar's order): the old primary the order names is
308            // fenced first, and only a fence that held lets the recharge run beside it. A
309            // guardian that fenced it AT THE CLOUD sends its proof (`fenced`) instead. With
310            // neither, the box's recharge lane refuses `not_fenced` (A7 row 4: the shared
311            // `nordisk_twin::fence::fence_gate`), so a bare order never makes two writers.
312            let fenced = match r.fence.as_deref().map(str::trim) {
313                Some(old) if old == r.main.trim() => {
314                    return said(seq, Err(format!("order {seq} names the main {} as the primary to fence: a recharge never fences the box it fills", r.main)));
315                }
316                Some(old) => match b.fence(old, r.epoch) {
317                    Ok(_) => Some(old.to_string()),
318                    Err(e) => return said(seq, Err(format!("order {seq} fences {old} first and that fence did not hold ({e}): nothing was recharged"))),
319                },
320                None => None,
321            };
322            let prefix = match (&fenced, r.fenced.as_deref()) {
323                (Some(f), _) => format!("fenced {f}; "),
324                (None, Some(p)) => format!("cloud-fenced ({p}); "),
325                (None, None) => String::new(),
326            };
327            said(
328                seq,
329                b.recharge(r)
330                    .map(|w| format!("{prefix}{w}"))
331                    .map_err(|e| format!("{prefix}{e}")),
332            )
333        }
334        Order::FencePrimary { primary, epoch } => said(seq, b.fence(primary, *epoch)),
335        Order::AdoptTwin { twin, principal } => said(seq, b.adopt(twin, principal)),
336        Order::BecomeTwin { primary } => said(seq, b.become_twin(primary)),
337        Order::ReviewTwin => match b.review() {
338            Ok(v) => OrderAck {
339                seq,
340                ok: true,
341                detail: v
342                    .get("verdict")
343                    .and_then(|w| w.as_str())
344                    .unwrap_or("reviewed")
345                    .to_string(),
346                receipt_json: v.to_string(),
347            },
348            Err(e) => said(seq, Err(e)),
349        },
350    }
351}
352
353/// **A pulled `grow_go_ahead`** (G16-d): verify monetize's step for THIS estate and role,
354/// the twin first, drain and unmount, ask monetize to size this box's own disks, look
355/// again, remount. A growth already at size is acked ok with nothing moved.
356fn grow<B: HolgerBox + ?Sized>(
357    b: &B,
358    ctx: &Ctx,
359    seq: u64,
360    g: &GrowGoAhead,
361    ask: &mut dyn GrowSet,
362) -> OrderAck {
363    let fail = |w: String| said(seq, Err(w));
364    let step = match monetize_embed::signing::verify_estate_growth(g.go_ahead.as_bytes(), ctx.key) {
365        Ok(s) => s,
366        Err(e) => {
367            return fail(format!(
368                "the paid step does not verify under this box's monetize key ({e}): nothing moved"
369            ))
370        }
371    };
372    if step.estate != ctx.me.instance {
373        return fail(format!(
374            "the paid step is estate {}'s; this box pulls as {}: nothing moved",
375            step.estate, ctx.me.instance
376        ));
377    }
378    if step.until_unix_ms <= ctx.now_unix_ms as i64 {
379        return fail(format!(
380            "the paid step was good until {} (unix ms): its period is over, nothing moved",
381            step.until_unix_ms
382        ));
383    }
384    let twin = ctx.me.role == "twin";
385    let main_gib = step.main_data_gib_per_member;
386    if !twin && g.data_gib != main_gib {
387        return fail(format!("the order sizes main's disks to {} GiB, the paid step to {main_gib} GiB: nothing moved", g.data_gib));
388    }
389    if twin && g.data_gib < main_gib {
390        return fail(format!("the order sizes the twin's disk to {} GiB, under main's {main_gib} GiB a disk: nothing moved", g.data_gib));
391    }
392    match b.grow_carried(g.data_gib) {
393        Ok(true) => {
394            return said(
395                seq,
396                Ok(format!(
397                    "already {} GiB a data disk: nothing moved",
398                    g.data_gib
399                )),
400            )
401        }
402        Ok(false) => {}
403        Err(e) => return fail(e),
404    }
405    if !twin {
406        if let Err(e) = b.twin_holds(g.data_gib) {
407            return fail(e);
408        }
409    }
410    match b.grow_start(g.data_gib, &format!("pull-{seq}-{}", step.reference)) {
411        Ok(GrowState::Done(d)) => return said(seq, Ok(d)),
412        Ok(GrowState::Failed(f)) => return fail(f),
413        Ok(_) => {}
414        Err(e) => return fail(e),
415    }
416    match b.grow_settle(false) {
417        GrowState::WaitingMembers(_) => {}
418        GrowState::Done(d) => return said(seq, Ok(d)),
419        GrowState::Failed(f) | GrowState::Running(f) => return fail(f),
420    }
421    let asked = ask.grow_set(g.data_gib);
422    if let Err(e) = b.grow_look_again(asked.is_err()) {
423        return fail(format!("the grow machine would not look again: {e}"));
424    }
425    let end = b.grow_settle(true);
426    match (asked, end) {
427        (Ok(words), GrowState::Done(d)) => said(seq, Ok(format!("{d} (monetize: {words})"))),
428        (
429            Ok(words),
430            GrowState::Failed(f) | GrowState::Running(f) | GrowState::WaitingMembers(f),
431        ) => fail(format!("{f} (monetize: {words})")),
432        (
433            Err(e),
434            GrowState::Done(s)
435            | GrowState::Failed(s)
436            | GrowState::Running(s)
437            | GrowState::WaitingMembers(s),
438        ) => fail(format!(
439            "monetize did not size this box's disks ({e}); /data: {s}"
440        )),
441    }
442}
443
444#[cfg(test)]
445pub(crate) mod tests {
446    use super::*;
447    use crate::order::tests::{me, monetize_key, MONETIZE_SEED};
448    use ed25519_dalek::Signer as _;
449    use monetize_product::{State, TenantId};
450    use std::cell::RefCell;
451
452    /// A Holger box in memory: what it holds and every verb it was asked.
453    #[derive(Default)]
454    pub struct Fake {
455        pub twin: bool,
456        pub facts: RefCell<BTreeMap<String, EntitlementFact>>,
457        pub did: RefCell<Vec<String>>,
458        pub carried: bool,
459        pub settle: RefCell<Vec<GrowState>>,
460    }
461
462    impl HolgerBox for Fake {
463        fn serving(&self) -> bool {
464            !self.twin
465        }
466        fn entitle(&self, fact: &EntitlementFact) -> Result<Applied, String> {
467            let mut f = self.facts.borrow_mut();
468            if f.get(&fact.tenant.0)
469                .is_some_and(|h| h.issued_unix_ms >= fact.issued_unix_ms)
470            {
471                return Ok(Applied::Held(format!("{}: held", fact.tenant.0)));
472            }
473            f.insert(fact.tenant.0.clone(), fact.clone());
474            Ok(Applied::New(format!(
475                "{} is {:?}",
476                fact.tenant.0, fact.state
477            )))
478        }
479        fn purge(&self, p: &PurgeTenant) -> Result<PurgeAnswer, String> {
480            self.did.borrow_mut().push(format!("purge {}", p.tenant));
481            Ok(PurgeAnswer {
482                job: format!("purge:{}", p.tenant),
483                detail: "3 artifacts".into(),
484                ..Default::default()
485            })
486        }
487        fn want_room(&self, g: &GrowRoom) -> Result<String, String> {
488            self.did.borrow_mut().push(format!("room {}", g.need_bytes));
489            Ok("kept".into())
490        }
491        fn sign_in_again(&self, s: &crate::order::SignInAgain) -> Result<String, String> {
492            self.did.borrow_mut().push(format!(
493                "sign-in {} {} {} {}",
494                s.account, s.to, s.host, s.ttl_secs
495            ));
496            Ok(format!(
497                "a new sign-in link for {} was mailed to {}",
498                s.account, s.to
499            ))
500        }
501        fn keep_refeed(&self, r: &crate::order::Refeed) -> Result<String, String> {
502            self.did.borrow_mut().push(format!(
503                "refeed {}",
504                r.keys.keys().cloned().collect::<Vec<_>>().join(",")
505            ));
506            Ok(format!(
507                "kept {}",
508                r.keys.keys().cloned().collect::<Vec<_>>().join(", ")
509            ))
510        }
511        fn keep_setting(&self, name: &str, value: &[u8]) -> Result<String, String> {
512            let v = String::from_utf8_lossy(value).into_owned();
513            self.did.borrow_mut().push(format!("setting {name} {v}"));
514            Ok(format!("{name} is {v}"))
515        }
516        fn add_principal(&self, a: &AddPrincipal) -> Result<String, String> {
517            self.did
518                .borrow_mut()
519                .push(format!("signup {} {}", a.username, a.signup));
520            if a.username == "theirs" {
521                Err("taken: theirs is another signup's".into())
522            } else {
523                Ok(a.username.clone())
524            }
525        }
526        fn grow_carried(&self, _: u64) -> Result<bool, String> {
527            Ok(self.carried)
528        }
529        fn twin_holds(&self, gib: u64) -> Result<String, String> {
530            self.did.borrow_mut().push(format!("twin_holds {gib}"));
531            Ok("the twin holds it".into())
532        }
533        fn grow_start(&self, gib: u64, nonce: &str) -> Result<GrowState, String> {
534            self.did.borrow_mut().push(format!("start {gib} {nonce}"));
535            Ok(GrowState::Running("draining".into()))
536        }
537        fn grow_settle(&self, past: bool) -> GrowState {
538            self.did.borrow_mut().push(format!("settle {past}"));
539            let mut s = self.settle.borrow_mut();
540            if s.is_empty() {
541                if past {
542                    GrowState::Done("grown".into())
543                } else {
544                    GrowState::WaitingMembers("waiting-members".into())
545                }
546            } else {
547                s.remove(0)
548            }
549        }
550        fn grow_look_again(&self, give_up: bool) -> Result<(), String> {
551            self.did.borrow_mut().push(format!("look_again {give_up}"));
552            Ok(())
553        }
554        fn recharge(&self, r: &RechargeAsk) -> Result<String, String> {
555            self.did.borrow_mut().push(format!(
556                "recharge {} from {} lost={}",
557                r.main, r.twin, r.accept_lost
558            ));
559            Ok("recharged".into())
560        }
561        fn fence(&self, primary: &str, epoch: u64) -> Result<String, String> {
562            self.did
563                .borrow_mut()
564                .push(format!("fence {primary} {epoch}"));
565            if primary.starts_with("stuck") {
566                Err(format!("{primary} answers nothing"))
567            } else {
568                Ok(format!("{primary} is fenced at epoch {epoch}"))
569            }
570        }
571        fn adopt(&self, twin: &str, principal: &str) -> Result<String, String> {
572            self.did
573                .borrow_mut()
574                .push(format!("adopt {twin} {principal}"));
575            Ok("follows".into())
576        }
577        fn review(&self) -> Result<serde_json::Value, String> {
578            Ok(serde_json::json!({"verdict": "the twin is whole", "complete": true}))
579        }
580        fn become_twin(&self, primary: &str) -> Result<String, String> {
581            self.did.borrow_mut().push(format!("become_twin {primary}"));
582            Ok("elected".into())
583        }
584    }
585
586    pub fn fact(repo: &str, issued: u64, signer: [u8; 32]) -> EntitlementFact {
587        let mut f = EntitlementFact {
588            tenant: TenantId(repo.into()),
589            plan: "o-1".into(),
590            state: State::Paid,
591            paid_until_unix_ms: Some(99_000_000),
592            caps: Default::default(),
593            source: "payment:stripe:o-1".into(),
594            signature: Vec::new(),
595            issued_unix_ms: (issued != 0).then_some(issued),
596            issued_signature: Vec::new(),
597        };
598        let key = ed25519_dalek::SigningKey::from_bytes(&signer);
599        f.signature = key
600            .sign(&monetize_embed::signing::fact_message(&f))
601            .to_bytes()
602            .to_vec();
603        if issued != 0 {
604            f.issued_signature = key
605                .sign(&monetize_embed::signing::fact_message_issued(&f))
606                .to_bytes()
607                .to_vec();
608        }
609        f
610    }
611
612    fn run(b: &dyn HolgerBox, role: &str, o: Order) -> OrderAck {
613        let (m, k) = (me(role), monetize_key());
614        execute(
615            b,
616            &Ctx {
617                me: &m,
618                key: &k,
619                now_unix_ms: 10_000,
620            },
621            5,
622            &o,
623            &mut NoAsk,
624        )
625    }
626
627    struct Asked(Vec<u64>, bool);
628    impl GrowSet for Asked {
629        fn grow_set(&mut self, gib: u64) -> Result<String, String> {
630            self.0.push(gib);
631            if self.1 {
632                Ok("resized".into())
633            } else {
634                Err("no answer".into())
635            }
636        }
637    }
638
639    fn growth(estate: &str, gib: u64, until: i64) -> String {
640        let mut g = monetize_embed::signing::EstateGrowth {
641            v: 1,
642            signer: "shop".into(),
643            estate: estate.into(),
644            main_data_gib_per_member: gib,
645            until_unix_ms: until,
646            reference: "o-7".into(),
647            issued_unix_ms: 1,
648            signature: String::new(),
649        };
650        let sig = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
651            .sign(&monetize_embed::signing::estate_growth_message(&g));
652        g.signature = monetize_embed::signing::base64_encode(&sig.to_bytes());
653        serde_json::to_string(&g).unwrap()
654    }
655
656    #[test]
657    fn entitlement_is_applied_newer_wins_and_a_forgery_moves_nothing() {
658        let b = Fake::default();
659        let a = run(
660            &b,
661            "main",
662            Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
663        );
664        assert!(a.ok, "{a:?}");
665        assert!(b.facts.borrow().contains_key("libs"));
666        let again = run(
667            &b,
668            "main",
669            Order::Entitlement(fact("libs", 4, MONETIZE_SEED)),
670        );
671        assert!(
672            again.ok && again.detail.contains("held"),
673            "an older fact is held, not refused: {again:?}"
674        );
675        let forged = run(&b, "main", Order::Entitlement(fact("other", 6, [9u8; 32])));
676        assert!(
677            !forged.ok && forged.detail.contains("does not verify"),
678            "{forged:?}"
679        );
680        assert!(!b.facts.borrow().contains_key("other"));
681        let twin = Fake {
682            twin: true,
683            ..Default::default()
684        };
685        let refused = run(
686            &twin,
687            "twin",
688            Order::Entitlement(fact("libs", 5, MONETIZE_SEED)),
689        );
690        assert!(
691            !refused.ok && refused.detail.contains("does not serve"),
692            "{refused:?}"
693        );
694    }
695
696    /// ★ **A resend sign-in link reaches the box that serves** (A16, Gunnar's S10): the order's
697    /// words go to the host as they are; a twin mints nothing; a box that cannot mint says so.
698    #[test]
699    fn a_sign_in_again_reaches_the_serving_box_and_a_twin_mints_nothing() {
700        let s = crate::order::SignInAgain {
701            account: "acme".into(),
702            to: "buyer@mock.invalid".into(),
703            host: "acme.holger.mock.invalid".into(),
704            ttl_secs: 86_400,
705        };
706        let b = Fake::default();
707        let a = run(&b, "main", Order::SignInAgain(s.clone()));
708        assert!(
709            a.ok && a.detail.contains("mailed to buyer@mock.invalid"),
710            "{a:?}"
711        );
712        assert_eq!(
713            b.did.borrow().as_slice(),
714            ["sign-in acme buyer@mock.invalid acme.holger.mock.invalid 86400"]
715        );
716        let twin = Fake {
717            twin: true,
718            ..Default::default()
719        };
720        let refused = run(&twin, "twin", Order::SignInAgain(s.clone()));
721        assert!(
722            !refused.ok
723                && refused.detail.contains("does not serve")
724                && twin.did.borrow().is_empty(),
725            "{refused:?}"
726        );
727        struct Bare;
728        impl HolgerBox for Bare {
729            fn serving(&self) -> bool {
730                true
731            }
732        }
733        let none = run(&Bare, "main", Order::SignInAgain(s));
734        assert!(
735            !none.ok && none.detail.contains("mints no sign-in link"),
736            "{none:?}"
737        );
738    }
739
740    /// ★ **HOL-PUBSITE-FEED (Gunnar's GRS-PUBLIC): a refeed's public-site switch is kept as the
741    /// box's generic setting** `console.public-site` (where an administrator's flip is kept), after
742    /// the keys kept beside the feed; the switch alone reaches only the setting. A box that keeps
743    /// no settings refuses by name and says what it kept before.
744    #[test]
745    fn a_refeeds_public_site_switch_is_kept_as_the_consoles_setting() {
746        let keys = |kv: &[(&str, &str)]| crate::order::Refeed {
747            keys: kv
748                .iter()
749                .map(|(k, v)| (k.to_string(), v.to_string()))
750                .collect(),
751        };
752        let b = Fake::default();
753        let a = run(
754            &b,
755            "main",
756            Order::Refeed(keys(&[
757                ("console_public_site", "on"),
758                ("public_repositories", "crates"),
759            ])),
760        );
761        assert!(a.ok && a.detail.contains("console.public-site"), "{a:?}");
762        assert_eq!(
763            b.did.borrow().as_slice(),
764            [
765                "refeed public_repositories",
766                "setting console.public-site on"
767            ]
768        );
769        let b = Fake::default();
770        let a = run(
771            &b,
772            "twin",
773            Order::Refeed(keys(&[("console_public_site", "off")])),
774        );
775        assert!(a.ok, "{a:?}");
776        assert_eq!(
777            b.did.borrow().as_slice(),
778            ["setting console.public-site off"],
779            "the switch alone keeps no key beside the feed"
780        );
781        struct KeepsKeysOnly;
782        impl HolgerBox for KeepsKeysOnly {
783            fn serving(&self) -> bool {
784                true
785            }
786            fn keep_refeed(&self, _: &crate::order::Refeed) -> Result<String, String> {
787                Ok("kept public_repositories".into())
788            }
789        }
790        let none = run(
791            &KeepsKeysOnly,
792            "main",
793            Order::Refeed(keys(&[
794                ("console_public_site", "on"),
795                ("public_repositories", "crates"),
796            ])),
797        );
798        assert!(
799            !none.ok
800                && none.detail.contains("keeps no settings")
801                && none
802                    .detail
803                    .contains("kept before it: kept public_repositories"),
804            "{none:?}"
805        );
806    }
807
808    /// ★ **MAIL-RELAY: a refeed reaches the box's keep**; a box that keeps none says so.
809    #[test]
810    fn a_refeed_reaches_the_boxes_keep_and_a_box_that_keeps_none_refuses() {
811        let r = crate::order::Refeed {
812            keys: [
813                ("secret.smtp_url", "smtps://u:tok@relay.mock.invalid:465"),
814                ("secret.smtp_from", "holger@mock.invalid"),
815                ("public_repositories", "crates"),
816            ]
817            .iter()
818            .map(|(k, v)| (k.to_string(), v.to_string()))
819            .collect(),
820        };
821        let b = Fake::default();
822        let a = run(&b, "main", Order::Refeed(r.clone()));
823        assert!(
824            a.ok && a.detail.contains("public_repositories") && !a.detail.contains("tok@"),
825            "{a:?}"
826        );
827        assert_eq!(
828            b.did.borrow().as_slice(),
829            ["refeed public_repositories,secret.smtp_from,secret.smtp_url"]
830        );
831        struct Bare;
832        impl HolgerBox for Bare {
833            fn serving(&self) -> bool {
834                true
835            }
836        }
837        let none = run(&Bare, "main", Order::Refeed(r));
838        assert!(
839            !none.ok && none.detail.contains("keeps no estate keys"),
840            "{none:?}"
841        );
842    }
843
844    #[test]
845    fn restate_entitlements_verifies_the_snapshot_and_counts_new_and_held() {
846        let b = Fake::default();
847        run(&b, "main", Order::Entitlement(fact("a", 5, MONETIZE_SEED)));
848        let facts = vec![fact("a", 5, MONETIZE_SEED), fact("b", 5, MONETIZE_SEED)];
849        let mut snap = monetize_embed::Snapshot {
850            issued_unix_ms: 7,
851            facts,
852            signature: Vec::new(),
853        };
854        snap.signature = ed25519_dalek::SigningKey::from_bytes(&MONETIZE_SEED)
855            .sign(&monetize_embed::signing::snapshot_message(
856                snap.issued_unix_ms,
857                &snap.facts,
858            ))
859            .to_bytes()
860            .to_vec();
861        let a = run(&b, "main", Order::RestateEntitlements(snap.clone()));
862        assert!(
863            a.ok && a.detail.contains("restated 1") && a.detail.contains("1 already held"),
864            "{a:?}"
865        );
866        snap.signature = vec![0; 64];
867        let bad = run(&b, "main", Order::RestateEntitlements(snap));
868        assert!(!bad.ok && bad.detail.contains("does not verify"), "{bad:?}");
869    }
870
871    #[test]
872    fn purge_tenant_answers_monetizes_purge_answer() {
873        let b = Fake::default();
874        let a = run(
875            &b,
876            "main",
877            Order::PurgeTenant(PurgeTenant {
878                tenant: "libs".into(),
879                reason: "nonpay".into(),
880                job: String::new(),
881            }),
882        );
883        assert!(a.ok, "{a:?}");
884        let answer: PurgeAnswer = serde_json::from_str(&a.detail).unwrap();
885        assert_eq!(answer.job, "purge:libs");
886        assert_eq!(b.did.borrow().as_slice(), ["purge libs"]);
887    }
888
889    #[test]
890    fn add_principal_makes_a_repository_on_a_plan_and_says_taken_by_name() {
891        let b = Fake::default();
892        let a = run(
893            &b,
894            "main",
895            Order::AddPrincipal(AddPrincipal {
896                username: "crates".into(),
897                kind: "org".into(),
898                signup: "s-1".into(),
899            }),
900        );
901        assert!(
902            a.ok && a.detail == "crates",
903            "the namespace made is the repository: {a:?}"
904        );
905        let t = run(
906            &b,
907            "main",
908            Order::AddPrincipal(AddPrincipal {
909                username: "theirs".into(),
910                kind: "org".into(),
911                signup: "s-2".into(),
912            }),
913        );
914        assert!(!t.ok && t.detail.starts_with("taken"), "{t:?}");
915        let twin = Fake {
916            twin: true,
917            ..Default::default()
918        };
919        assert!(
920            !run(
921                &twin,
922                "twin",
923                Order::AddPrincipal(AddPrincipal {
924                    username: "crates".into(),
925                    kind: "org".into(),
926                    signup: "s-1".into()
927                })
928            )
929            .ok,
930            "only the main that serves"
931        );
932    }
933
934    #[test]
935    fn grow_room_is_kept_for_the_operator() {
936        let b = Fake::default();
937        let a = run(
938            &b,
939            "main",
940            Order::GrowRoom(GrowRoom {
941                need_bytes: 1 << 30,
942                reason: "libs".into(),
943            }),
944        );
945        assert!(a.ok, "{a:?}");
946        assert_eq!(b.did.borrow().as_slice(), ["room 1073741824"]);
947    }
948
949    #[test]
950    fn grow_go_ahead_runs_twin_first_asks_monetize_and_remounts() {
951        let b = Fake::default();
952        let (m, k) = (me("main"), monetize_key());
953        let mut ask = Asked(Vec::new(), true);
954        let o = Order::GrowGoAhead(GrowGoAhead {
955            go_ahead: growth("acme", 120, 99_000),
956            data_gib: 120,
957        });
958        let a = execute(
959            &b,
960            &Ctx {
961                me: &m,
962                key: &k,
963                now_unix_ms: 10_000,
964            },
965            6,
966            &o,
967            &mut ask,
968        );
969        assert!(a.ok, "{a:?}");
970        assert_eq!(
971            b.did.borrow().as_slice(),
972            [
973                "twin_holds 120",
974                "start 120 pull-6-o-7",
975                "settle false",
976                "look_again false",
977                "settle true"
978            ]
979        );
980        assert_eq!(ask.0, [120]);
981
982        // Monetize does not answer: /data is mounted back and the ack says why.
983        let b = Fake::default();
984        let mut deaf = Asked(Vec::new(), false);
985        b.settle.borrow_mut().extend([
986            GrowState::WaitingMembers("w".into()),
987            GrowState::Failed("mounted back".into()),
988        ]);
989        let a = execute(
990            &b,
991            &Ctx {
992                me: &m,
993                key: &k,
994                now_unix_ms: 10_000,
995            },
996            7,
997            &o,
998            &mut deaf,
999        );
1000        assert!(
1001            !a.ok && a.detail.contains("no answer") && a.detail.contains("mounted back"),
1002            "{a:?}"
1003        );
1004        assert!(b.did.borrow().contains(&"look_again true".to_string()));
1005    }
1006
1007    #[test]
1008    fn grow_go_ahead_refuses_another_estate_a_lapsed_step_and_a_forgery_and_skips_a_done_one() {
1009        let b = Fake::default();
1010        for (go, why) in [
1011            (growth("other", 120, 99_000), "estate other"),
1012            (growth("acme", 120, 5_000), "period is over"),
1013            (
1014                growth("acme", 120, 99_000).replace("\"o-7\"", "\"o-8\""),
1015                "does not verify",
1016            ),
1017        ] {
1018            let a = run(
1019                &b,
1020                "main",
1021                Order::GrowGoAhead(GrowGoAhead {
1022                    go_ahead: go,
1023                    data_gib: 120,
1024                }),
1025            );
1026            assert!(!a.ok && a.detail.contains(why), "{why}: {a:?}");
1027        }
1028        assert!(b.did.borrow().is_empty(), "nothing moved");
1029        let done = Fake {
1030            carried: true,
1031            ..Default::default()
1032        };
1033        let a = run(
1034            &done,
1035            "main",
1036            Order::GrowGoAhead(GrowGoAhead {
1037                go_ahead: growth("acme", 120, 99_000),
1038                data_gib: 120,
1039            }),
1040        );
1041        assert!(a.ok && a.detail.contains("nothing moved"), "{a:?}");
1042        let twin = Fake {
1043            twin: true,
1044            ..Default::default()
1045        };
1046        let a = run(
1047            &twin,
1048            "twin",
1049            Order::GrowGoAhead(GrowGoAhead {
1050                go_ahead: growth("acme", 120, 99_000),
1051                data_gib: 240,
1052            }),
1053        );
1054        assert!(a.detail.contains("no pull channel"), "{a:?}");
1055        assert!(
1056            !twin
1057                .did
1058                .borrow()
1059                .iter()
1060                .any(|d| d.starts_with("twin_holds")),
1061            "the twin grows first, asking no twin: {a:?}"
1062        );
1063    }
1064
1065    #[test]
1066    fn recharge_from_twin_runs_the_lane_on_a_cloud_fence_proof() {
1067        let b = Fake::default();
1068        let r = RechargeAsk {
1069            main: "main:1".into(),
1070            twin: "twin:1".into(),
1071            accept_lost: true,
1072            fenced: Some("upcloud: gone".into()),
1073            ..Default::default()
1074        };
1075        let a = run(&b, "twin", Order::RechargeFromTwin(r));
1076        assert!(a.ok && a.detail.starts_with("cloud-fenced"), "{a:?}");
1077        assert_eq!(
1078            b.did.borrow().as_slice(),
1079            ["recharge main:1 from twin:1 lost=true"]
1080        );
1081    }
1082
1083    /// ★ **Fence before recharge, by the pulled order** (M2, Gunnar's pattern): the old
1084    /// primary the order names is fenced first, and only a fence that held lets the
1085    /// recharge run; a fence that did not hold runs nothing; the main it fills is never
1086    /// the box it fences.
1087    #[test]
1088    fn recharge_from_twin_fences_the_old_primary_first() {
1089        let b = Fake::default();
1090        let r = RechargeAsk {
1091            main: "main:2".into(),
1092            twin: "twin:1".into(),
1093            accept_lost: false,
1094            epoch: 4,
1095            fence: Some("old:1".into()),
1096            ..Default::default()
1097        };
1098        let a = run(&b, "twin", Order::RechargeFromTwin(r.clone()));
1099        assert!(a.ok && a.detail.starts_with("fenced old:1; "), "{a:?}");
1100        assert_eq!(
1101            b.did.borrow().as_slice(),
1102            ["fence old:1 4", "recharge main:2 from twin:1 lost=false"]
1103        );
1104
1105        let stuck = Fake::default();
1106        let a = run(
1107            &stuck,
1108            "twin",
1109            Order::RechargeFromTwin(RechargeAsk {
1110                fence: Some("stuck:1".into()),
1111                ..r.clone()
1112            }),
1113        );
1114        assert!(
1115            !a.ok
1116                && a.detail.contains("stuck:1 answers nothing")
1117                && a.detail.contains("nothing was recharged"),
1118            "{a:?}"
1119        );
1120        assert_eq!(
1121            stuck.did.borrow().as_slice(),
1122            ["fence stuck:1 4"],
1123            "no recharge beside an unfenced primary"
1124        );
1125
1126        let same = Fake::default();
1127        let a = run(
1128            &same,
1129            "twin",
1130            Order::RechargeFromTwin(RechargeAsk {
1131                fence: Some("main:2".into()),
1132                ..r
1133            }),
1134        );
1135        assert!(
1136            !a.ok && a.detail.contains("never fences the box it fills"),
1137            "{a:?}"
1138        );
1139        assert!(same.did.borrow().is_empty(), "nothing ran");
1140    }
1141
1142    #[test]
1143    fn become_twin_runs_holgers_election() {
1144        let b = Fake {
1145            twin: true,
1146            ..Default::default()
1147        };
1148        assert!(
1149            run(
1150                &b,
1151                "twin",
1152                Order::BecomeTwin {
1153                    primary: "main:1".into()
1154                }
1155            )
1156            .ok
1157        );
1158        assert_eq!(b.did.borrow().as_slice(), ["become_twin main:1"]);
1159        struct Bare;
1160        impl HolgerBox for Bare {
1161            fn serving(&self) -> bool {
1162                false
1163            }
1164        }
1165        let none = run(
1166            &Bare,
1167            "twin",
1168            Order::BecomeTwin {
1169                primary: "main:1".into(),
1170            },
1171        );
1172        assert!(
1173            !none.ok && none.detail.contains("cannot become"),
1174            "the default refuses by name: {none:?}"
1175        );
1176    }
1177
1178    #[test]
1179    fn fence_primary_runs_the_boxes_fence_adopt_and_review_run() {
1180        let b = Fake::default();
1181        let a = run(
1182            &b,
1183            "twin",
1184            Order::FencePrimary {
1185                primary: "old:1".into(),
1186                epoch: 3,
1187            },
1188        );
1189        assert!(
1190            a.ok && b.did.borrow().contains(&"fence old:1 3".to_string()),
1191            "{a:?}"
1192        );
1193        struct Bare;
1194        impl HolgerBox for Bare {
1195            fn serving(&self) -> bool {
1196                false
1197            }
1198        }
1199        let none = run(
1200            &Bare,
1201            "twin",
1202            Order::FencePrimary {
1203                primary: "old:1".into(),
1204                epoch: 3,
1205            },
1206        );
1207        assert!(
1208            !none.ok && none.detail.contains("cannot fence"),
1209            "the default refuses by name: {none:?}"
1210        );
1211        let a = run(
1212            &b,
1213            "main",
1214            Order::AdoptTwin {
1215                twin: "twin:1".into(),
1216                principal: "twin".into(),
1217            },
1218        );
1219        assert!(
1220            a.ok && b.did.borrow().contains(&"adopt twin:1 twin".to_string()),
1221            "{a:?}"
1222        );
1223        let a = run(&b, "twin", Order::ReviewTwin);
1224        assert!(
1225            a.ok && a.detail == "the twin is whole" && a.receipt_json.contains("complete"),
1226            "{a:?}"
1227        );
1228    }
1229}