Skip to main content

monetize_embed/
fleet.rs

1//! **The pull channel's signed forms** (5.4, G1-pull): monetize never calls INTO an
2//! estate; every estate calls monetize (`Fleet.Report`, `Fleet.Orders`, `Fleet.Ask`) and
3//! monetize answers with [`SignedOrder`]s the box verifies under `monetize_pubkey`.
4//!
5//! Two forms, each a fixed binary layout (not JSON) so the estate's verifier
6//! (`gunnar_repair::order`) and this crate agree byte for byte with no canonical-JSON
7//! rule to share:
8//!
9//! * [`order_message`]: what monetize signs for ONE order to ONE box (`role`).
10//! * [`fleet_call_message`]: what a box signs with its own Ed25519 appliance key for each
11//!   call it makes to monetize.
12//!
13//! An order is only "please run verb V with these arguments": the estate runs it through the
14//! SAME function its local admin button and its boot check call, and says how it went in the
15//! next [`Report`] ([`OrderAck`]).
16
17use ed25519_dalek::{Signature, VerifyingKey};
18use serde::{Deserialize, Serialize};
19
20/// The domain tag every order signature starts with.
21pub const ORDER_DOMAIN: &[u8] = b"nordisk-order-v1\0";
22/// The domain tag every box's call signature starts with.
23pub const CALL_DOMAIN: &[u8] = b"nordisk-fleet-call-v1\0";
24/// How far a box's call time may be from monetize's clock.
25pub const CALL_FRESH_MS: u64 = 5 * 60 * 1000;
26/// How far ahead of the box's clock an order may be dated (the shared pump's and Gunnar's five
27/// minutes).
28pub const FUTURE_SKEW_MS: u64 = 5 * 60 * 1000;
29/// How long `Fleet.Orders` is held open at most before it answers empty.
30pub const LONG_POLL_MAX_MS: u64 = 25_000;
31/// **How long a front waits on the shop's `SoldEstate.Upgrade`**: the shop's whole keep boot,
32/// 2 h, the ONE constant the shop holds its call by (`monetize_product::KEEP_BOOT_WITHIN`; lane
33/// G9, A7 row 9, Gunnar b74ca801). A front that gave up first would count a slow keep boot as
34/// lost and ask for a Recharge while the shop still works.
35pub use monetize_product::KEEP_BOOT_WITHIN;
36/// **The shop's words for an Upgrade still running** (`an upgrade of <host> is already running
37/// here`, an UNAVAILABLE answer): the front that hears them asks again later (REL-T2-1: the
38/// Upgrade is started, then polled). The ONE box says them; the front's door reads them.
39pub const UPGRADE_RUNNING: &str = "is already running here";
40
41/// The order kinds. The body is JSON; unknown fields are ignored. The relay products' guardian
42/// kinds ([`kind::GUARDIAN`], gunnar-repair's verbs) are defined here; the core's kinds are
43/// `monetize_product::pull`'s one list (A28 M25), re-exported here under their old names.
44pub mod kind {
45    /// `{main, twin, accept_lost, epoch, case?}`: the twin (or a fresh main) pumps the twin's
46    /// log into the main. The same verb the local "Recharge" button and the boot check run.
47    pub const RECHARGE_FROM_TWIN: &str = "recharge_from_twin";
48    /// `{primary, epoch, case?}`: the old primary may not serve past `epoch`.
49    pub const FENCE_PRIMARY: &str = "fence_primary";
50    /// `{twin, principal, case?}`: the primary takes the box at `twin` as its twin.
51    pub const ADOPT_TWIN: &str = "adopt_twin";
52    /// `{case?}`: the box reviews its twin and reports the `TwinReview`.
53    pub const REVIEW_TWIN: &str = "review_twin";
54    /// `{snapshot, case?}`: the money put back on a recharged main. `snapshot` is a
55    /// [`crate::Snapshot`] signed by monetize's key — what monetize's books hold for the
56    /// product. The box verifies it and every fact in it (`signing::verify_snapshot`), applies
57    /// each fact only if newer than the one it holds (a same fact is a no-op), deletes
58    /// nothing, and runs it only while it serves as the primary.
59    pub const RESTATE_ENTITLEMENTS: &str = "restate_entitlements";
60    /// [`super::GrowGoAhead`]: **a sold estate's paid data step** (G16-d, G1-pull row 17),
61    /// queued to the estate's main AND its twin once the step's money is captured. The box
62    /// verifies `go_ahead` under monetize's key (`signing::verify_estate_growth`: its estate,
63    /// good until `until_unix_ms`), then grows its own data disks to `data_gib` a disk: drain,
64    /// unmount, `Fleet.Ask{kind: ask::GROW_SET}` (monetize resizes them at the provider), look
65    /// again, remount. The twin first — its disk holds main's whole set. A newer go-ahead
66    /// supersedes an unacked older one; a growth already done is acked ok, nothing moved.
67    pub use monetize_product::pull::order::GROW_GO_AHEAD;
68    /// [`super::SignInAgain`]: **a sold estate's RESEND sign-in link** (S10, owner 2026-10-11
69    /// 05:53Z), queued to the estate's main when its buyer asks (`EstateOrder.SendSignInAgain`).
70    /// The order names whose passkey, which address and which console host — NO secret: the
71    /// main mints the fresh invite itself (only while the account holds no passkey) and mails
72    /// `https://{host}/login?invite=…&user=…` itself. The shop never holds a resend's login
73    /// secret (the FIRST link stays shop-minted at install, OM-21 decision A). A newer ask
74    /// supersedes an unacked older one.
75    pub use monetize_product::pull::order::SIGN_IN_AGAIN;
76    /// [`super::Refeed`]: **estate keys fed again to an already-laid estate** (MAIL-RELAY +
77    /// NIGHT-PUBREPO, 2026-10-28), queued to the estate's main and twin when the operator asks
78    /// (`EstateOrder.Refeed`). The box takes each key it knows by name ([`super::refeed`]) and
79    /// refuses the order by name otherwise. The body may CARRY A SECRET (the relay's token): never
80    /// logged; a newer one supersedes an unacked older one.
81    pub use monetize_product::pull::order::REFEED;
82
83    /// **The relay products' guardian kinds** (the twin's repair verbs): not core kinds; a
84    /// product whose guardian runs no twin never gets one.
85    pub const GUARDIAN: [&str; 5] = [RECHARGE_FROM_TWIN, FENCE_PRIMARY, ADOPT_TWIN, REVIEW_TWIN, RESTATE_ENTITLEMENTS];
86}
87
88/// The `Fleet.Ask` kinds that are not a remedy: answered at once, no case opened.
89/// A product's own order ([`ProductOrder`]) and the file it may carry ([`Fetch`]), MONETIZE-FIX.
90pub use monetize_product::pull::{Fetch, FetchAsk, ProductOrder};
91
92pub mod ask {
93    /// [`super::GrowSetAsk`]: **size the calling box's own data disks** at monetize's provider
94    /// (G16-d, G1-pull row 18) — no more than is paid for its role, never smaller. The main
95    /// sizes main's disks, the twin (its own key, `role = twin`) the twin's.
96    pub use monetize_product::pull::ask::GROW_SET;
97    /// `monetize_product::pull::FetchAsk` `{id}`: **the box pulls the file a product order
98    /// carries** (MONETIZE-FIX); the bytes come back in `FleetAskAnswer.answer`.
99    pub use monetize_product::pull::ask::FETCH;
100    /// [`super::FrontAsk`] `{role, done}`: **the shop boots ONE standing box once from the
101    /// release's keep medium** (was `SoldEstate.Upgrade`; plan §8, A24 §5.1 row 27). Asked by
102    /// the estate's front; the answer is `EstateUpgradeResponse` in `FleetAskAnswer.answer`.
103    pub const UPGRADE: &str = "upgrade";
104    /// [`super::FrontAsk`] `{role, seed, done}`: **the shop lays ONE lost box again** (was
105    /// `SoldEstate.Recharge`, row 29); the answer is `EstateRechargeResponse`. Not the guardian's
106    /// `recharge_from_twin` remedy: this one is the cloud half, asked by the front.
107    pub const RECHARGE: &str = "recharge";
108    /// [`super::FrontAsk`] `{done}`: **the estate takes its own installer material** (was
109    /// `SoldEstate.TakeMaterial`, row 28); the answer is `EstateTakeMaterialResponse`.
110    pub const MATERIAL: &str = "material";
111    /// [`super::FrontAsk`] `{}`: **the front takes the open update** (was
112    /// `SoldEstate.UpdateTake`, row 25); the answer is `EstateUpdateTakeResponse`.
113    pub const UPDATE_TAKE: &str = "update_take";
114    /// [`super::FrontAsk`] `{role, carries}`: **the front reports one box's update step** (was
115    /// `SoldEstate.UpdateReport`, row 26); the answer is `EstateUpdateReportResponse`.
116    pub const UPDATE_REPORT: &str = "update_report";
117    /// **The front's asks** — the five `SoldEstate` calls folded into `Fleet.Ask` (plan §8,
118    /// decided 2026-10-24). Answered at once, no case opened; asked only by the estate's front.
119    pub const FRONT: [&str; 5] = [UPGRADE, RECHARGE, MATERIAL, UPDATE_TAKE, UPDATE_REPORT];
120
121    /// Pure: **is `kind` one of the front's asks** ([`FRONT`])?
122    pub fn is_front(kind: &str) -> bool {
123        FRONT.contains(&kind)
124    }
125}
126
127/// The calls a box makes ([`FleetCall::call`]).
128pub mod call {
129    pub const REPORT: &str = "report";
130    pub const ORDERS: &str = "orders";
131    pub const ASK: &str = "ask";
132}
133
134/// **One order to one box**, signed by the product's monetize key.
135#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
136pub struct SignedOrder {
137    /// Per instance, strictly rising; the box refuses any `seq` at or below the last it ran.
138    pub seq: u64,
139    pub product: String,
140    pub instance: String,
141    /// The box the order is for: `main` | `twin` | `front`. Signed, so an order for the twin
142    /// is never run by the main.
143    pub role: String,
144    pub kind: String,
145    /// JSON text.
146    pub body: String,
147    pub issued_unix_ms: u64,
148    pub not_after_unix_ms: u64,
149    /// 64-byte Ed25519 over [`order_message`].
150    #[serde(default)]
151    pub signature: Vec<u8>,
152}
153
154/// Why a box refuses an order.
155#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
156pub enum OrderRefusal {
157    #[error("the order is not signed by monetize")]
158    Signature,
159    #[error("order {seq} is not newer than {last}")]
160    Replayed { seq: u64, last: u64 },
161    #[error("order {0} has expired")]
162    Expired(u64),
163    #[error("order {0} is for {1}, not this box")]
164    NotForMe(u64, String),
165    #[error("order {0} is dated more than five minutes ahead of this box's clock")]
166    Ahead(u64),
167}
168
169fn field(out: &mut Vec<u8>, bytes: &[u8]) {
170    out.extend_from_slice(&(bytes.len() as u32).to_be_bytes());
171    out.extend_from_slice(bytes);
172}
173
174/// The bytes an order's signature covers: [`ORDER_DOMAIN`] ‖ seq ‖ issued ‖ not_after (u64 BE)
175/// ‖ then product, instance, role, kind, body, each as u32 BE length ‖ bytes.
176pub fn order_message(o: &SignedOrder) -> Vec<u8> {
177    let mut out = ORDER_DOMAIN.to_vec();
178    out.extend_from_slice(&o.seq.to_be_bytes());
179    out.extend_from_slice(&o.issued_unix_ms.to_be_bytes());
180    out.extend_from_slice(&o.not_after_unix_ms.to_be_bytes());
181    for f in [&o.product, &o.instance, &o.role, &o.kind, &o.body] {
182        field(&mut out, f.as_bytes());
183    }
184    out
185}
186
187/// **The box's check**, as strict as the shared pump's (`monetize_estate_side::pump::check`):
188/// signed under `key` with STRICT Ed25519 (no small-order key or `R`), checked first, so a
189/// forged order is always [`OrderRefusal::Signature`] and never moves the box's sequence; then
190/// for `(product, instance, role)`, newer than `last_seq`, inside its window at `now_unix_ms`
191/// (`not_after` 0 = none), and dated no more than [`FUTURE_SKEW_MS`] ahead of the box's clock.
192pub fn verify_order(o: &SignedOrder, key: &VerifyingKey, me: (&str, &str, &str), now_unix_ms: u64, last_seq: u64) -> Result<(), OrderRefusal> {
193    let (product, instance, role) = me;
194    if !check(key, &order_message(o), &o.signature) {
195        return Err(OrderRefusal::Signature);
196    }
197    if o.product != product || o.instance != instance || o.role != role {
198        return Err(OrderRefusal::NotForMe(o.seq, o.role.clone()));
199    }
200    if o.seq <= last_seq {
201        return Err(OrderRefusal::Replayed { seq: o.seq, last: last_seq });
202    }
203    if o.not_after_unix_ms != 0 && now_unix_ms > o.not_after_unix_ms {
204        return Err(OrderRefusal::Expired(o.seq));
205    }
206    if o.issued_unix_ms > now_unix_ms.saturating_add(FUTURE_SKEW_MS) {
207        return Err(OrderRefusal::Ahead(o.seq));
208    }
209    Ok(())
210}
211
212/// **One call from a box to monetize**, signed by the box's own appliance key.
213#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
214pub struct FleetCall {
215    pub product: String,
216    pub instance: String,
217    pub role: String,
218    /// [`call::REPORT`] | [`call::ORDERS`] | [`call::ASK`].
219    pub call: String,
220    /// JSON text: a [`Report`], an [`OrdersAfter`] or an [`Ask`].
221    pub body: String,
222    pub at_unix_ms: u64,
223}
224
225/// Why monetize refuses a box's call.
226#[derive(Clone, PartialEq, Eq, Debug, thiserror::Error)]
227pub enum CallRefusal {
228    #[error("the call is not signed by the box's key")]
229    Signature,
230    #[error("the call's time is too far from monetize's clock")]
231    Stale,
232}
233
234/// The bytes a box's call signature covers: [`CALL_DOMAIN`] ‖ at (u64 BE) ‖ then product,
235/// instance, role, call, body, each as u32 BE length ‖ bytes.
236pub fn fleet_call_message(c: &FleetCall) -> Vec<u8> {
237    let mut out = CALL_DOMAIN.to_vec();
238    out.extend_from_slice(&c.at_unix_ms.to_be_bytes());
239    for f in [&c.product, &c.instance, &c.role, &c.call, &c.body] {
240        field(&mut out, f.as_bytes());
241    }
242    out
243}
244
245/// Verify a box's call under its appliance `key`, fresh at `now_unix_ms`.
246pub fn verify_fleet_call(c: &FleetCall, signature: &[u8], key: &VerifyingKey, now_unix_ms: u64) -> Result<(), CallRefusal> {
247    if !check(key, &fleet_call_message(c), signature) {
248        return Err(CallRefusal::Signature);
249    }
250    if c.at_unix_ms.abs_diff(now_unix_ms) > CALL_FRESH_MS {
251        return Err(CallRefusal::Stale);
252    }
253    Ok(())
254}
255
256/// STRICT Ed25519 (`verify_strict`): a small-order key or `R` never verifies (T16, A28 M11).
257fn check(key: &VerifyingKey, msg: &[u8], sig: &[u8]) -> bool {
258    Signature::from_slice(sig).map(|s| key.verify_strict(msg, &s).is_ok()).unwrap_or(false)
259}
260
261/// What a box says ran (or did not) — sent back in its next [`Report`].
262#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
263#[serde(default)]
264pub struct OrderAck {
265    pub seq: u64,
266    pub ok: bool,
267    pub detail: String,
268    /// gunnar_repair's `Receipt` / `TwinReview`, JSON text; empty when none.
269    pub receipt_json: String,
270}
271
272/// **The box's heartbeat** (`Fleet.Report` body). Its arrival time is the liveness signal.
273#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
274#[serde(default)]
275pub struct Report {
276    pub build: String,
277    /// The box's own health words, flat: `serving` (`yes` | `no` | `fenced`), `data` (`ok` |
278    /// `lost` | `diverged`), `serves_at` (`host:port`), `twin` (`paired` | `silent` | …),
279    /// `epoch`. Unknown words are kept and shown.
280    pub health: std::collections::BTreeMap<String, String>,
281    pub acked: Vec<OrderAck>,
282    /// The product's twin review (gunnar: `gunnar_repair::TwinReview`), JSON text, when it
283    /// made one.
284    pub twin_review: Option<String>,
285    /// **The product's own state** (G1 pull: tenants and owners, usage, servable, the paid
286    /// door, the room a raise would find), JSON text of `monetize_product::pull::ProductReport`.
287    /// Sent by the box that serves tenants (the main); what monetize used to dial it for.
288    pub product: Option<String>,
289    pub measured_unix_ms: u64,
290}
291
292/// `Fleet.Orders` body: orders after `after`, held up to `wait_ms` (≤ [`LONG_POLL_MAX_MS`]).
293#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
294#[serde(default)]
295pub struct OrdersAfter {
296    pub after: u64,
297    pub wait_ms: u64,
298}
299
300/// Body of a [`kind::GROW_GO_AHEAD`] order.
301#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
302#[serde(default)]
303pub struct GrowGoAhead {
304    /// The [`crate::signing::EstateGrowth`] JSON, signed by monetize's key.
305    pub go_ahead: String,
306    /// GB a data disk THIS box's role is paid to (main: the step; twin: main's whole set).
307    pub data_gib: u64,
308}
309
310/// Body of a [`kind::SIGN_IN_AGAIN`] order. Never a secret.
311#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
312#[serde(default)]
313pub struct SignInAgain {
314    /// The account whose first passkey the invite enrols (the order's account).
315    pub account: String,
316    /// The order's address: where the estate mails the link.
317    pub to: String,
318    /// The console's relying-party host the link points at (`<company>.<apex>`).
319    pub host: String,
320    /// How long the invite lives.
321    pub ttl_secs: u64,
322}
323
324/// **The estate keys a [`kind::REFEED`] order may carry**, in the words of the feed a box is
325/// laid with: the mains' mail relay (`secret.smtp_url`, `secret.smtp_from`, and `secret.smtp_ca`
326/// for a relay on a private CA; monetize-mail `FEED_SMTP_*`) and a site's public repositories
327/// (`public_repositories`, comma-separated). A box takes the ones it knows and refuses any
328/// other by name.
329pub mod refeed {
330    pub const SMTP_URL: &str = "secret.smtp_url";
331    pub const SMTP_FROM: &str = "secret.smtp_from";
332    pub const SMTP_CA: &str = "secret.smtp_ca";
333    pub const PUBLIC_REPOSITORIES: &str = "public_repositories";
334    /// The site's public-site switch, `on` | `off` (GRS-PUBLIC 2026-10-28; the lay's own front
335    /// feed word, monetize-shop-pairing `contract::FEED_PUBLIC_SITE`).
336    pub const PUBLIC_SITE: &str = "console_public_site";
337    /// Every key a refeed may carry.
338    pub const KNOWN: [&str; 5] = [SMTP_URL, SMTP_FROM, SMTP_CA, PUBLIC_REPOSITORIES, PUBLIC_SITE];
339    /// The keys whose values are secrets (never shown).
340    pub fn is_secret(key: &str) -> bool {
341        key.starts_with("secret.")
342    }
343}
344
345/// Body of a [`kind::REFEED`] order (MAIL-RELAY + NIGHT-PUBREPO, 2026-10-28): the estate keys
346/// fed again, key → value. **A `secret.` value is a secret**: the `Debug` names the keys only.
347#[derive(Clone, PartialEq, Eq, Default, Serialize, Deserialize)]
348#[serde(default)]
349pub struct Refeed {
350    pub keys: std::collections::BTreeMap<String, String>,
351}
352
353impl std::fmt::Debug for Refeed {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        f.debug_struct("Refeed").field("keys", &self.keys.keys().collect::<Vec<_>>()).finish()
356    }
357}
358
359/// `Fleet.Ask` body of [`ask::GROW_SET`]: the box's own data disks to `gib` a disk. No
360/// `members` = every laid data disk of the calling box; `gib` 0 = what its role is paid to.
361#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
362#[serde(default)]
363pub struct GrowSetAsk {
364    pub members: Vec<String>,
365    pub gib: u64,
366}
367
368/// **The body of a front's ask** ([`ask::FRONT`]): the fields the old `SoldEstate` request
369/// carried, less the company and the signature — the `FleetSigned` call names the estate
370/// (`instance`) and the box (`role = front`) and is signed by the front's key. `role` is the
371/// box the ask is about (`main` | `twin`; for `update_report`, the box whose step it is);
372/// `carries` is the update report's `<outcome>\n<words>`.
373#[derive(Clone, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
374#[serde(default)]
375pub struct FrontAsk {
376    #[serde(skip_serializing_if = "String::is_empty")]
377    pub role: String,
378    #[serde(skip_serializing_if = "std::ops::Not::not")]
379    pub done: bool,
380    #[serde(skip_serializing_if = "String::is_empty")]
381    pub seed: String,
382    #[serde(skip_serializing_if = "String::is_empty")]
383    pub carries: String,
384}
385
386/// `Fleet.Ask` body: a request from the estate (`new_twin`, `new_server`, `recharge`, …).
387#[derive(Clone, PartialEq, Debug, Default, Serialize, Deserialize)]
388#[serde(default)]
389pub struct Ask {
390    pub kind: String,
391    pub body: serde_json::Value,
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use ed25519_dalek::{Signer as _, SigningKey};
398
399    /// A28 M25: the core kinds are defined once, in `monetize_product::pull`; the kinds here
400    /// are that list's, and the rest of [`kind`] are the relay products' guardian kinds.
401    #[test]
402    fn the_core_kinds_here_are_monetize_products_one_list() {
403        use monetize_product::pull;
404        assert_eq!(kind::GROW_GO_AHEAD, pull::order::GROW_GO_AHEAD);
405        assert_eq!(kind::SIGN_IN_AGAIN, pull::order::SIGN_IN_AGAIN);
406        assert_eq!(kind::REFEED, pull::order::REFEED);
407        assert_eq!(ask::GROW_SET, pull::ask::GROW_SET);
408        for k in kind::GUARDIAN {
409            assert!(!pull::is_core(k), "{k} is a guardian kind, not a core one");
410        }
411        assert_eq!(kind::GUARDIAN, [kind::RECHARGE_FROM_TWIN, kind::FENCE_PRIMARY, kind::ADOPT_TWIN, kind::REVIEW_TWIN, kind::RESTATE_ENTITLEMENTS]);
412    }
413
414    /// ★ Plan §8 (the SoldEstate → Fleet fold): the front's five asks are named once, none of
415    /// them is a guardian remedy or `grow_set`, and their body round-trips through the `Ask`
416    /// JSON with only the fields it carries.
417    #[test]
418    fn the_fronts_asks_are_the_five_soldestate_calls_and_their_body_round_trips() {
419        assert_eq!(ask::FRONT, ["upgrade", "recharge", "material", "update_take", "update_report"]);
420        for k in ask::FRONT {
421            assert!(ask::is_front(k));
422        }
423        for k in [ask::GROW_SET, kind::RECHARGE_FROM_TWIN, kind::REVIEW_TWIN, "new_twin", "new_server", ""] {
424            assert!(!ask::is_front(k), "{k}");
425        }
426        // The ONE box's own door, not a product's kind: none is one of the core's 8 (A28 M25).
427        for k in ask::FRONT {
428            assert!(!monetize_product::pull::is_core(k), "{k}");
429        }
430        let a = FrontAsk { role: "main".into(), seed: "SEED".into(), ..Default::default() };
431        let text = serde_json::to_string(&Ask { kind: ask::RECHARGE.into(), body: serde_json::to_value(&a).unwrap() }).unwrap();
432        assert_eq!(text, r#"{"kind":"recharge","body":{"role":"main","seed":"SEED"}}"#);
433        let back: Ask = serde_json::from_str(&text).unwrap();
434        assert_eq!(serde_json::from_value::<FrontAsk>(back.body).unwrap(), a);
435        assert_eq!(serde_json::from_value::<FrontAsk>(serde_json::json!({})).unwrap(), FrontAsk::default(), "an empty body is the update take's");
436    }
437
438    /// ★ Lane G9, A7 row 9 (Gunnar b74ca801): a front waits on the shop's whole keep boot, 2 h,
439    /// the one constant the shop's cloud crate holds its Upgrade call by.
440    #[test]
441    fn a_front_waits_on_the_shops_whole_keep_boot() {
442        assert_eq!(KEEP_BOOT_WITHIN, std::time::Duration::from_secs(2 * 3600));
443        assert_eq!(KEEP_BOOT_WITHIN, monetize_product::KEEP_BOOT_WITHIN);
444    }
445
446    const GOLDEN_MESSAGE_HEX: &str = "6e6f726469736b2d6f726465722d7631000000000000000003000001a3185c5000000001a3186a0ba00000000667756e6e61720000000461636d65000000047477696e0000001272656368617267655f66726f6d5f7477696e000000517b226163636570745f6c6f7374223a66616c73652c2265706f6368223a352c226d61696e223a2231302e362e302e31303a3530303531222c227477696e223a2231302e362e302e31323a3530303531227d";
447    const GOLDEN_SIGNATURE_HEX: &str = "d4a3233158d9a3fbfa42f9aa80fd2ca420666ec2fe9f7fbf1a61abab533c09f6dab60c67367bc771a1d819d1e6bed28c59d6d56187a4748ba9b4b9988d13d50a";
448
449    fn key() -> SigningKey {
450        SigningKey::from_bytes(&[7u8; 32])
451    }
452
453    fn order() -> SignedOrder {
454        SignedOrder {
455            seq: 3,
456            product: "gunnar".into(),
457            instance: "acme".into(),
458            role: "twin".into(),
459            kind: kind::RECHARGE_FROM_TWIN.into(),
460            body: r#"{"accept_lost":false,"epoch":5,"main":"10.6.0.10:50051","twin":"10.6.0.12:50051"}"#.into(),
461            issued_unix_ms: 1_800_000_000_000,
462            not_after_unix_ms: 1_800_000_900_000,
463            signature: Vec::new(),
464        }
465    }
466
467    fn signed() -> SignedOrder {
468        let mut o = order();
469        o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
470        o
471    }
472
473    /// The golden vector gunnar-repair pastes into its own test: same order, same bytes.
474    #[test]
475    fn the_order_message_is_the_agreed_binary_layout() {
476        let m = order_message(&order());
477        assert!(m.starts_with(ORDER_DOMAIN));
478        let mut want = ORDER_DOMAIN.to_vec();
479        want.extend_from_slice(&3u64.to_be_bytes());
480        want.extend_from_slice(&1_800_000_000_000u64.to_be_bytes());
481        want.extend_from_slice(&1_800_000_900_000u64.to_be_bytes());
482        for f in ["gunnar", "acme", "twin", kind::RECHARGE_FROM_TWIN, &order().body] {
483            want.extend_from_slice(&(f.len() as u32).to_be_bytes());
484            want.extend_from_slice(f.as_bytes());
485        }
486        assert_eq!(m, want);
487        let sig = key().sign(&m).to_bytes();
488        let hex: String = sig.iter().map(|b| format!("{b:02x}")).collect();
489        assert_eq!(hex, GOLDEN_SIGNATURE_HEX, "the golden vector moved: tell gunnar_repair::order");
490        let mhex: String = m.iter().map(|b| format!("{b:02x}")).collect();
491        assert_eq!(mhex, GOLDEN_MESSAGE_HEX);
492    }
493
494    #[test]
495    fn a_box_runs_only_a_fresh_order_signed_for_it() {
496        let pk = key().verifying_key();
497        let me = ("gunnar", "acme", "twin");
498        let now = 1_800_000_100_000;
499        assert_eq!(verify_order(&signed(), &pk, me, now, 2), Ok(()));
500        assert_eq!(verify_order(&signed(), &pk, me, now, 3), Err(OrderRefusal::Replayed { seq: 3, last: 3 }));
501        assert_eq!(verify_order(&signed(), &pk, me, 1_800_000_900_001, 0), Err(OrderRefusal::Expired(3)));
502        assert_eq!(verify_order(&signed(), &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::NotForMe(3, "twin".into())));
503        let mut forged = signed();
504        forged.role = "main".into();
505        assert_eq!(verify_order(&forged, &pk, ("gunnar", "acme", "main"), now, 0), Err(OrderRefusal::Signature));
506        let other = SigningKey::from_bytes(&[9u8; 32]).verifying_key();
507        assert_eq!(verify_order(&signed(), &other, me, now, 0), Err(OrderRefusal::Signature));
508    }
509
510    /// ★ **T16 (A28 M11): this check is as strict as the shared pump's** (`monetize_estate_side::
511    /// pump::check`, P4c). Under a small-order key (the identity point) the all-zero signature
512    /// verifies ANY message with plain `verify`; a strict check refuses it, and a real key's
513    /// signature still passes.
514    #[test]
515    fn a_weak_key_with_the_all_zero_signature_signs_no_order() {
516        let (weak, zero) = weak_key_and_zero_signature();
517        let mut o = order();
518        o.signature = zero;
519        let now = 1_800_000_100_000;
520        assert_eq!(verify_order(&o, &weak, ("gunnar", "acme", "twin"), now, 0), Err(OrderRefusal::Signature), "a weak key verifies nothing");
521        assert_eq!(verify_order(&signed(), &key().verifying_key(), ("gunnar", "acme", "twin"), now, 0), Ok(()));
522    }
523
524    /// ★ The same weak key never vouches for a box's call either.
525    #[test]
526    fn a_weak_box_key_with_the_all_zero_signature_signs_no_call() {
527        let (weak, zero) = weak_key_and_zero_signature();
528        let c = FleetCall { product: "gunnar".into(), instance: "acme".into(), role: "main".into(), call: call::REPORT.into(), body: "{}".into(), at_unix_ms: 1_800_000_000_000 };
529        assert_eq!(verify_fleet_call(&c, &zero, &weak, 1_800_000_000_000), Err(CallRefusal::Signature));
530    }
531
532    /// The identity point as a key, and the signature (R = identity, s = 0) that plain `verify`
533    /// accepts for every message under it.
534    fn weak_key_and_zero_signature() -> (VerifyingKey, Vec<u8>) {
535        let mut identity = [0u8; 32];
536        identity[0] = 1;
537        let weak = VerifyingKey::from_bytes(&identity).expect("the identity point decompresses");
538        let mut zero = vec![0u8; 64];
539        zero[0] = 1;
540        (weak, zero)
541    }
542
543    /// ★ Every signed field is covered: one tampered field, a flipped signature bit, a short or
544    /// missing signature, or another key is refused as not monetize's.
545    #[test]
546    fn a_tampered_order_is_not_monetizes() {
547        let pk = key().verifying_key();
548        let me = ("gunnar", "acme", "twin");
549        let now = 1_800_000_100_000;
550        type Tamper = Box<dyn Fn(&mut SignedOrder)>;
551        let tampered: Vec<(&str, Tamper)> = vec![
552            ("seq", Box::new(|o| o.seq += 1)),
553            ("kind", Box::new(|o| o.kind = kind::FENCE_PRIMARY.into())),
554            ("body", Box::new(|o| o.body = "{}".into())),
555            ("issued", Box::new(|o| o.issued_unix_ms -= 1)),
556            ("not_after", Box::new(|o| o.not_after_unix_ms += 1)),
557            ("bit", Box::new(|o| o.signature[5] ^= 1)),
558            ("short", Box::new(|o| o.signature.truncate(63))),
559            ("none", Box::new(|o| o.signature.clear())),
560        ];
561        for (what, f) in tampered {
562            let mut o = signed();
563            f(&mut o);
564            assert_eq!(verify_order(&o, &pk, me, now, 0), Err(OrderRefusal::Signature), "{what}");
565        }
566    }
567
568    /// ★ The signature is checked FIRST: a forged order addressed to another box is forged (it
569    /// never moves the box's sequence), not "not for me".
570    #[test]
571    fn a_forged_order_for_another_box_is_forged_not_misaddressed() {
572        let mut o = signed();
573        o.role = "main".into();
574        assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_800_000_100_000, 0), Err(OrderRefusal::Signature));
575    }
576
577    /// ★ `not_after` 0 is no window (as the shared pump reads it), not "expired at the epoch".
578    #[test]
579    fn an_order_with_no_window_does_not_expire() {
580        let mut o = order();
581        o.not_after_unix_ms = 0;
582        o.signature = key().sign(&order_message(&o)).to_bytes().to_vec();
583        assert_eq!(verify_order(&o, &key().verifying_key(), ("gunnar", "acme", "twin"), 1_900_000_000_000, 0), Ok(()));
584    }
585
586    /// ★ An order dated more than [`FUTURE_SKEW_MS`] ahead of the box's clock is refused by
587    /// name; five minutes of skew is fine.
588    #[test]
589    fn an_order_dated_ahead_of_the_box_clock_is_refused() {
590        let pk = key().verifying_key();
591        let me = ("gunnar", "acme", "twin");
592        let issued = order().issued_unix_ms;
593        assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS, 0), Ok(()), "five minutes of skew");
594        assert_eq!(verify_order(&signed(), &pk, me, issued - FUTURE_SKEW_MS - 1, 0), Err(OrderRefusal::Ahead(3)));
595    }
596
597    #[test]
598    fn a_call_is_signed_by_the_box_and_fresh() {
599        let box_key = SigningKey::from_bytes(&[3u8; 32]);
600        let c = FleetCall { product: "gunnar".into(), instance: "acme".into(), role: "main".into(), call: call::REPORT.into(), body: "{}".into(), at_unix_ms: 1_800_000_000_000 };
601        let m = fleet_call_message(&c);
602        assert!(m.starts_with(CALL_DOMAIN));
603        let sig = box_key.sign(&m).to_bytes();
604        let pk = box_key.verifying_key();
605        assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_060_000), Ok(()));
606        assert_eq!(verify_fleet_call(&c, &sig, &pk, 1_800_000_000_000 + CALL_FRESH_MS + 1), Err(CallRefusal::Stale));
607        let mut moved = c.clone();
608        moved.role = "twin".into();
609        assert_eq!(verify_fleet_call(&moved, &sig, &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
610        assert_eq!(verify_fleet_call(&c, &sig[..10], &pk, 1_800_000_000_000), Err(CallRefusal::Signature));
611    }
612}