crates/monetize-server/src/estate_order/sold.rs
//! **A sold estate grows through its shop** (one shop, 2026-10-07).
//!
//! A private Gunnar sold here holds no cloud token: its disks are this shop's,
//! at this shop's provider account. Its data grows in three moves, and the
//! estate makes the last two.
//!
//! **G16-d (ONE monetize, G1-pull rows 17-18, 2026-10-10):** no estate runs a
//! monetize of its own, so nothing on the estate polls for its go-ahead. The
//! go-ahead is a SIGNED ORDER the estate's boxes pull, and each box asks for
//! its own disks over the same Fleet channel, with its own key:
//!
//! ```text
//! the buyer pays a data step (ChangePlan, `change`) ──▶ captured
//! ──▶ grant(): monetize SIGNS a go-ahead {estate, main GB a disk, until}
//! ──▶ queued: order `grow_go_ahead{go_ahead, data_gib}` to the estate's
//! main (main's paid size) and twin (main's whole set), good until
//! the end of the paid period; a newer step supersedes an unacked one
//! each box pulls it (Fleet.Orders) ──▶ verifies it under monetize's key
//! ──▶ its own growth runs (drain, unmount — the twin first)
//! its growth's cloud step ──▶ Fleet.Ask{grow_set, {members?, gib?}}
//! ──▶ here (grow_own): the caller's fleet key and ROLE, its own LAID data
//! disks only, no more than its role is paid, never smaller
//! ──▶ grow_set at the provider (detach, resize, attach)
//! ──▶ the sizes read back into the run: the renewal prices what is laid
//! ```
//!
//! The estate's own monetize once called `SoldEstate.GrowGoAhead`/`GrowSet`; no estate runs a
//! monetize, and both RPCs went in v2.0 P2 (T12 12.3, A28 H7). What stays is the live pulled
//! path above: the `grow_go_ahead` order, `Fleet.Ask{grow_set}` and the boxes' own executors.
use std::collections::BTreeSet;
use std::sync::Mutex;
use monetize::sign::{verify_estate_call, EstateCall, VerifyingKey};
use monetize_cloud::grow::{DataSet, GrowOutcome, SetSurvey, VolumeId};
use monetize_cloud::{CloudError, ServerId};
use monetize_embed::fleet::{kind, GrowGoAhead};
use monetize_guardian::Guardian;
use super::{iso_date, phase, EstateOrderConfig, EstateRun, EstateSizes, Transition};
use crate::state::AppState;
/// The one sentence a call that does not verify is answered with — an
/// unknown estate, an estate with no key, a wrong key, a stale call.
pub const NOT_SIGNED: &str = "the call is not signed by that estate's key";
/// The call words ([`EstateCall::call`]).
/// F4 (2026-10-09): lay ONE lost box of the estate again (the cloud half of a recharge).
pub const CALL_RECHARGE: &str = "recharge";
/// F4 (2026-10-09): upgrade ONE standing box in place from the release's keep medium.
pub const CALL_UPGRADE: &str = "upgrade";
/// F4 (2026-10-10): the estate TAKES its installer material from the shop (the pull; the shop
/// used to PUT it into the front's door — monetize never dials an estate).
pub const CALL_TAKE_MATERIAL: &str = "take-material";
/// How long after the buyer was told the shop holds a sold estate's installer material for it
/// to take ([`take_material`]) before the hand-off forgets it anyway.
pub const MATERIAL_TAKE_WITHIN_MS: u64 = 24 * 3600 * 1000;
/// **The go-ahead as the run keeps it**: the newest paid data step.
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct Growth {
/// GB a data disk on main, as paid.
pub main_data_gib: u64,
/// Good until then: the end of the period it was paid in.
pub until_ms: u64,
/// The change it was paid on (`estate/<company>/change-<n>`).
pub reference: String,
pub granted_at_ms: u64,
/// The signed JSON (`monetize_embed::signing::EstateGrowth`), as handed out.
pub go_ahead: String,
}
/// Why a sold estate's call was not done.
#[derive(Debug, PartialEq, Eq)]
pub enum SoldRefusal {
/// [`NOT_SIGNED`].
NotSigned,
/// Refused by name: the estate's own disks, sizes and money only.
Refused(String),
/// Transient: another growth of the same estate runs, the provider did not
/// answer. Nothing was changed by this call.
Unavailable(String),
}
/// **Who asks a sold estate's call, and how it was proven** (plan §8: SoldEstate folded into
/// Fleet, the ONE box serving both doors for one release).
#[derive(Clone, Copy, Debug)]
pub enum Caller<'a> {
/// The old `SoldEstate` door: the call is an `EstateCall` signed by the estate's client key,
/// verified here.
Signed { at_ms: i64, signature: &'a [u8] },
/// `Fleet.Ask` (a front's ask, [`monetize_embed::fleet::ask::FRONT`]): the Fleet door has
/// already proven the call is the estate's front's ([`crate::fleet::front_ask`]).
Fleet,
}
impl Caller<'_> {
/// The run the call is about, once the call is proven: refused [`SoldRefusal::NotSigned`]
/// (one sentence that names no estate) for an unknown, gone or never-laid estate either way.
pub(super) fn run(
&self,
run: Option<&EstateRun>,
call: &EstateCall,
now: u64,
) -> Result<EstateRun, SoldRefusal> {
match self {
Caller::Signed { at_ms, signature } => verify(
run,
&EstateCall {
at_unix_ms: *at_ms,
..call.clone()
},
signature,
now,
),
Caller::Fleet => run
.filter(|r| !phase::is_gone(&r.phase) && r.laid.is_some())
.cloned()
.ok_or(SoldRefusal::NotSigned),
}
}
}
/// One data disk, as the estate is told it.
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct DiskLine {
pub member: String,
/// `main` | `twin`.
pub role: String,
pub gib: u64,
/// The size it may be grown to: what is paid.
pub paid_gib: u64,
pub server: String,
pub state: String,
pub attached: bool,
pub address: String,
}
/// What a sold estate is told while the shop makes room for a paid growth
/// (A54): it asks again on its tick; no cap is named.
pub const GROWTH_WAITS: &str = "the shop is preparing this growth; ask again later";
/// [`grow_own`]'s answer.
#[derive(Clone, Debug)]
pub struct GrowSetAnswer {
pub disks: Vec<DiskLine>,
pub outcome: GrowOutcome,
pub words: String,
}
/// **Sign and keep the go-ahead** for `gib` a disk on main: called once the
/// data step's money is captured. Good until the end of the paid period.
pub fn grant(state: &AppState, run: &mut EstateRun, gib: u64, reference: &str, now: u64) {
let until = run.paid_until_ms;
let product = match crate::fleet::run_product(state, run) {
Ok(p) => p,
Err(why) => {
run.history.push(Transition {
phase: String::new(),
at_ms: now,
note: format!(
"the growth go-ahead could not be signed for {}: {why}",
run.company
),
});
return;
}
};
let bytes = state.signer_for(&product).sign_estate_growth(
&run.company,
gib,
until as i64,
reference,
now as i64,
);
run.growth = Some(Growth {
main_data_gib: gib,
until_ms: until,
reference: reference.to_string(),
granted_at_ms: now,
go_ahead: String::from_utf8(bytes).unwrap_or_default(),
});
run.history.push(Transition {
phase: String::new(),
at_ms: now,
note: format!("the growth go-ahead is signed for {}: main's data to {gib} GB a disk, paid order {reference}, good until {}", run.company, iso_date(until)),
});
let note = match queue_go_ahead(state, run, now) {
Ok(roles) => format!(
"the go-ahead ({reference}) is queued for {}'s {} to pull",
run.company,
roles.join(" and ")
),
Err(e) => {
let words = format!(
"the go-ahead ({reference}) was NOT queued for {}'s boxes: {e}",
run.company
);
eprintln!("monetize-server: ALERT sold-estate.grow {words}");
skidbladnir::functional_status(
"monetize-server/sold-estate",
"grow-go-ahead",
false,
&words,
);
words
}
};
run.history.push(Transition {
phase: String::new(),
at_ms: now,
note,
});
}
/// **A captured renewal, as a signed order the estate pulls** (money-flow l): the paid step —
/// main's data at what is paid — is signed again for the NEW period and queued for the main
/// and the twin like any go-ahead ([`grant`]), so the boxes learn the term they are paid to
/// and a paid data step no longer lapses at the old end. A box already at size moves nothing.
/// An estate the fleet does not hold as sold (no guardian, another product) has nothing to
/// pull; that is noted, not alerted.
pub fn renewed(state: &AppState, run: &mut EstateRun, reference: &str, now: u64) {
let held = crate::fleet::run_product(state, run)
.ok()
.is_some_and(|product| {
state.fleet.guardian().is_some_and(|g| {
g.store.product_of(run.fleet_name()).as_deref() == Some(product.as_str())
})
});
if !held {
run.history.push(Transition { phase: String::new(), at_ms: now, note: format!("renewal {reference}: no order is queued — the fleet does not hold {} as a sold estate", run.company) });
return;
}
let gib = paid_main_gib(run, now);
grant(state, run, gib, reference, now);
}
/// **The go-ahead as orders the estate pulls** (G16-d): one `grow_go_ahead` for each laid
/// box with data (main, twin), carrying what THAT role may grow to, signed by monetize's
/// key on the guardian's store, good until the go-ahead is. An unacked older one for the
/// same box is dropped. The roles queued.
fn queue_go_ahead(state: &AppState, run: &EstateRun, now: u64) -> Result<Vec<String>, String> {
let g = state
.fleet
.guardian()
.ok_or("the fleet store is not open")?;
let cfg = state.estates.cfg.as_ref().ok_or("no [estate_orders]")?;
let growth = run.growth.as_ref().ok_or("no go-ahead is signed")?;
let product = crate::fleet::run_product(state, run)?;
let product = product.as_str();
if g.store.product_of(run.fleet_name()).as_deref() != Some(product) {
return Err(format!(
"the fleet does not know {} as a sold estate",
run.company
));
}
let ttl = growth.until_ms.saturating_sub(now);
let mut roles = Vec::new();
for role in data_roles(cfg).iter().map(|d| d.role.as_str()) {
if !run
.laid
.as_ref()
.and_then(|l| l.machine(role))
.is_some_and(|m| m.disks.len() > 1)
{
continue;
}
let body = serde_json::to_string(&GrowGoAhead {
go_ahead: growth.go_ahead.clone(),
data_gib: paid_gib_for(cfg, run, role, now),
})
.map_err(|e| e.to_string())?;
let order = Guardian::order(
product,
&run.company,
role,
kind::GROW_GO_AHEAD,
body,
now,
ttl,
);
g.queue_order(order, |o| o.kind == kind::GROW_GO_AHEAD && o.role == role)
.map_err(|e| e.to_string())?;
roles.push(role.to_string());
}
if roles.is_empty() {
return Err("no laid box has a data disk".into());
}
Ok(roles)
}
/// **What main's data is paid to**, GB a disk: the run's price holds it (a
/// paid step's change sets it, the laid sizes read back raise it), or an open
/// go-ahead's when that is more.
pub fn paid_main_gib(run: &EstateRun, now: u64) -> u64 {
let open = run
.growth
.as_ref()
.filter(|g| g.until_ms > now)
.map(|g| g.main_data_gib)
.unwrap_or(0);
run.quote.sizes.main_data_gib.max(open)
}
/// What a data disk of `role` may be grown to: main's paid size, or — the
/// twin's disk holding main's whole set — the twin's disk at that size.
fn paid_gib_for(cfg: &EstateOrderConfig, run: &EstateRun, role: &str, now: u64) -> u64 {
let main = paid_main_gib(run, now);
if !data_roles(cfg)
.iter()
.any(|d| d.role == role && d.mirrors_primary)
{
return main;
}
let sizes = EstateSizes {
main_data_gib: main,
..run.quote.sizes.clone()
};
let Ok(plan) = super::lifecycle::lay_plan_of(cfg, &run.company, &sizes, &run.quote.twin_plan)
else {
return 0;
};
plan.machines
.iter()
.find(|m| m.role == role)
.and_then(|m| m.disks.get(1))
.map(|d| d.gib)
.unwrap_or(0)
}
/// **The machines whose data a bought set grows** — the product's lifecycle's
/// ([`super::lifecycle::Lifecycle::data_roles`]), the primary first; none for a product
/// with no lifecycle.
fn data_roles(cfg: &EstateOrderConfig) -> Vec<super::lifecycle::DataRole> {
super::lifecycle::of(cfg)
.map(|l| l.data_roles())
.unwrap_or_default()
}
/// Every data disk of the estate as recorded (main's and the twin's), each
/// with what it may be grown to.
fn recorded(cfg: &EstateOrderConfig, run: &EstateRun, now: u64) -> Vec<DiskLine> {
let Some(laid) = run.laid.as_ref() else {
return Vec::new();
};
let mut out = Vec::new();
for role in data_roles(cfg).iter().map(|d| d.role.clone()) {
let role = role.as_str();
let Some(m) = laid.machine(role) else {
continue;
};
let paid = paid_gib_for(cfg, run, role, now);
for d in m.disks.iter().skip(1) {
out.push(DiskLine {
member: d.id.clone(),
role: role.into(),
gib: d.gib,
paid_gib: paid,
server: m.server.clone(),
state: String::new(),
attached: true,
address: d.address.clone(),
});
}
}
out
}
/// The call verified under the run's client key, or [`SoldRefusal::NotSigned`].
pub(super) fn verify(
run: Option<&EstateRun>,
call: &EstateCall,
signature: &[u8],
now: u64,
) -> Result<EstateRun, SoldRefusal> {
let run = run
.filter(|r| !phase::is_gone(&r.phase) && r.laid.is_some())
.ok_or(SoldRefusal::NotSigned)?;
let key = client_key_of(run).ok_or(SoldRefusal::NotSigned)?;
verify_estate_call(call, signature, &key, now as i64).map_err(|_| SoldRefusal::NotSigned)?;
Ok(run.clone())
}
/// **The estate's client key** (the custody minted it at install; its public half on the run):
/// what its front signs with, on the old door and on `Fleet.Ask`. None: none kept, or no key.
pub fn client_key_of(run: &EstateRun) -> Option<VerifyingKey> {
unhex32(&run.client_key).and_then(|k| VerifyingKey::from_bytes(&k).ok())
}
/// **The box that drives `run`'s update and asks the shop for its boxes** (the lifecycle's
/// `front_role`, on the site it was bought on): `front` for Gunnar and Holger.
pub fn front_role_of(state: &AppState, run: &EstateRun) -> String {
let Some(cfg) = state.estates.cfg.as_ref() else {
return "front".into();
};
let terms = super::terms_of(state, cfg, run).unwrap_or_else(|_| cfg.clone());
super::lifecycle::of(&terms)
.map(|l| l.update_plan().front_role)
.unwrap_or_else(|_| "front".into())
}
fn unhex32(s: &str) -> Option<[u8; 32]> {
let s = s.trim();
if s.len() != 64 {
return None;
}
let v: Option<Vec<u8>> = (0..64)
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16).ok())
.collect();
v?.try_into().ok()
}
/// The named members, all of ONE machine's laid data disks: its role.
fn one_machine(
company: &str,
lines: &[DiskLine],
members: &[String],
) -> Result<(String, String), SoldRefusal> {
let mut role: Option<(String, String)> = None;
for m in members {
let line = lines.iter().find(|l| &l.member == m).ok_or_else(|| {
SoldRefusal::Refused(format!("{m} is not one of {company}'s laid data disks"))
})?;
match &role {
None => role = Some((line.role.clone(), line.server.clone())),
Some((r, _)) if *r != line.role => return Err(SoldRefusal::Refused(format!("the disks named are on two machines of {company} (main and twin): one call sizes one machine's"))),
Some(_) => {}
}
}
role.ok_or_else(|| SoldRefusal::Refused("no disk is named".into()))
}
/// The set to grow, under the product the box sells (OM-13: `[estate_orders] product`, never a
/// hard-coded "gunnar"), booked as `<product><suffix>` ([`super::lifecycle::DataRole::set_suffix`]:
/// the twin's `<product>-twin`).
pub(super) fn set_of_as(
product: &str,
suffix: &str,
server: &str,
members: &[String],
gib: u64,
) -> DataSet {
DataSet {
product: format!("{product}{suffix}"),
server: ServerId(server.to_string()),
members: members.iter().map(|m| VolumeId(m.clone())).collect(),
target_gib: gib,
}
}
fn surveyed(lines: &[DiskLine], survey: &SetSurvey) -> Vec<DiskLine> {
survey
.volumes
.iter()
.filter_map(|v| {
let mut l = lines.iter().find(|l| l.member == v.id.0)?.clone();
l.gib = v.gib;
l.state = v.state.clone();
l.attached = v.attached;
l.address = v.address.clone().unwrap_or_default();
Some(l)
})
.collect()
}
/// One growth of one estate's disks at a time, here.
static GROWING: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
struct Growing(String);
impl Drop for Growing {
fn drop(&mut self) {
GROWING
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(&self.0);
}
}
static RECHARGING: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
struct Recharging(String);
impl Drop for Recharging {
fn drop(&mut self) {
RECHARGING
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(&self.0);
}
}
/// [`recharge`]'s answer: the machine as laid again (its new server and addresses).
#[derive(Clone, Debug)]
pub struct RechargeAnswer {
pub machine: monetize_cloud::lay::LaidMachine,
pub words: String,
}
/// **`SoldEstate.Recharge`: the cloud half of a recharge** (F4, the owner 2026-10-09). A sold
/// estate whose `main` or `twin` is LOST asks its shop to lay that ONE box again: the shop's
/// account holds its machines, the estate holds no cloud token. The estate re-admits the box
/// itself (its front, from the installer material in its own sealed slot); the shop lays it
/// from the release medium with `seed` as its user_data and answers where it stands.
///
/// Refused by name: a role that is not `main` or `twin`, an estate not delivered, and a box
/// that still STANDS at the provider (a standing box is upgraded, never laid again). `done`:
/// the box was re-admitted — the shop gives its release clone back and turns its metadata
/// off, as after the first hand-off. Signed by the estate's client key, the seed inside the
/// signed call.
pub fn recharge(
state: &AppState,
company: &str,
role: &str,
seed: &str,
done: bool,
at_ms: i64,
signature: &[u8],
) -> Result<RechargeAnswer, SoldRefusal> {
recharge_as(
state,
company,
role,
seed,
done,
Caller::Signed { at_ms, signature },
)
}
/// [`recharge`], asked by `by` (the old door's signed call, or the front's `Fleet.Ask{recharge}`).
pub fn recharge_as(
state: &AppState,
company: &str,
role: &str,
seed: &str,
done: bool,
by: Caller<'_>,
) -> Result<RechargeAnswer, SoldRefusal> {
let cfg = state.estates.cfg.as_ref().ok_or(SoldRefusal::NotSigned)?;
let now = state.now();
let carries = if done {
String::new()
} else {
seed.to_string()
};
let call = EstateCall {
v: 1,
call: CALL_RECHARGE.into(),
estate: company.to_string(),
members: vec![role.to_string()],
gib: u64::from(done),
at_unix_ms: 0,
carries,
};
let run = by.run(state.estates.get(company).as_ref(), &call, now)?;
// ★ ST-5: named on the site the estate was bought on.
let site_terms = super::terms_of(state, cfg, &run).unwrap_or_else(|_| cfg.clone());
let cfg = &site_terms;
let host = cfg.host_of(company);
let again = super::lifecycle::of(cfg)
.map_err(SoldRefusal::Refused)?
.update_plan()
.upgraded_by_shop;
if !again.iter().any(|r| r == role) {
return Err(SoldRefusal::Refused(format!(
"{role:?} is not a box the shop lays again: {}",
again.join(" or ")
)));
}
if !phase::is_term(&run.phase) {
return Err(SoldRefusal::Refused(format!(
"{host} is not delivered ({}): nothing is laid again",
run.phase
)));
}
// An adopted site's machines are its kit's: a lost box is laid again by the kit.
if super::adopt::is_adopted(&run) {
return Err(SoldRefusal::Refused(super::adopt::not_laid_here(
&run,
&format!("laying its {role} again"),
)));
}
let laid = run
.laid
.clone()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has nothing laid")))?;
let was = laid
.machine(role)
.cloned()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has no {role} laid")))?;
let cloud = state.registry.find_cloud(&cfg.cloud).ok_or_else(|| {
SoldRefusal::Unavailable(format!("the shop's cloud {:?} is not linked", cfg.cloud))
})?;
let journal = state
.estates
.journal(company)
.map_err(|e| SoldRefusal::Unavailable(format!("the lay journal: {e}")))?;
{
let mut g = RECHARGING.lock().unwrap_or_else(|p| p.into_inner());
if !g.insert(company.to_string()) {
return Err(SoldRefusal::Unavailable(format!(
"a recharge of {host} is already running here"
)));
}
}
let _recharging = Recharging(company.to_string());
if done {
let only = monetize_cloud::lay::LaidEstate {
machines: vec![was.clone()],
..laid.clone()
};
let gone = monetize_cloud::ask::forget_media(&cloud, &only, &journal).map_err(|e| {
SoldRefusal::Unavailable(format!(
"the release clone of {role} was not given back yet: {e}"
))
})?;
let mut words = format!(
"{host}'s {role} is re-admitted: its release clone given back ({}), its metadata off",
if gone.is_empty() {
"none was left".into()
} else {
gone.join(", ")
}
);
// RA5-37: a main's kept relay (the `mail_relay` refeed) lived on the lost box alone, and
// the new one was re-admitted from its first lay's material: it is fed the relay of its
// own product again, as `site refeed` feeds it.
if role == "main" {
match super::refeed::feed(
state,
company,
&[super::refeed::group::MAIL_RELAY.to_string()],
) {
Ok(asked) => words.push_str(&format!("; fed again: {}", asked.words)),
Err(why) => words.push_str(&format!("; its mail relay was not fed again ({why})")),
}
}
note(state, cfg, company, &words, now);
return Ok(RechargeAnswer {
machine: was,
words,
});
}
if seed.trim().is_empty() {
return Err(SoldRefusal::Refused(
"a box is laid again with its seed: none was sent".into(),
));
}
let id = monetize_cloud::ResourceId {
provider: cloud.name().into(),
id: was.server.clone(),
};
match monetize_cloud::ask::status(&cloud, &id) {
Ok(monetize_cloud::ResourceStatus::Gone) | Err(CloudError::UnknownResource(_)) => {}
// Down, not gone (CODE RED audit 4): started from its disk, never laid again.
Ok(monetize_cloud::ResourceStatus::Stopped) => {
let red = format!("recharge:{company}:{role}");
if let Err(e) = monetize_cloud::ask::set_power(&cloud, &id, true) {
let words = e.to_string();
super::code_red::raise(state, &red, &host, role, &format!("the {role} is stopped at the provider and would not start"), &words, &format!("start {host}'s {role} from its disk at the provider, or ask the recharge again"));
return Err(SoldRefusal::Unavailable(format!("{host}'s {role} is stopped at the provider and its start was refused: {words}")));
}
super::code_red::clear(state, &red);
let words = format!("{host}'s {role} was stopped at the provider: started from its disk (nothing laid again)");
note(state, cfg, company, &words, now);
return Ok(RechargeAnswer { machine: was, words });
}
Ok(st) => return Err(SoldRefusal::Refused(format!("{host}'s {role} still stands at the provider ({st:?}): a standing box is upgraded, never laid again"))),
Err(e) => return Err(SoldRefusal::Unavailable(format!("the provider could not say whether {host}'s {role} stands: {e}"))),
}
// The run's whole plan — the standing boxes are ADOPTED by their labels, nothing laid for
// them — with the release's medium and the seed for THIS role only.
let life = super::lifecycle::of(cfg).map_err(SoldRefusal::Refused)?;
let mut plan = super::lifecycle::plan_of(
life.as_ref(),
cfg,
company,
&run.quote.sizes,
&run.quote.twin_plan,
)
.map_err(SoldRefusal::Refused)?;
for line in super::dress_plan(state, cfg, life.as_ref(), &mut plan, now)
.map_err(SoldRefusal::Unavailable)?
{
if line.starts_with(&format!("{role}:")) {
eprintln!("monetize-server: estate order {company}: {line}");
}
}
for m in plan.machines.iter_mut() {
if m.role == role {
m.user_data = monetize_cloud::lay::UserData(seed.to_string());
} else {
m.medium.clear();
m.medium_object = None;
m.template.clear();
}
}
let relaid = monetize_cloud::Cloud::lay(&cloud, &plan, &journal).map_err(|e| match e {
CloudError::Refused(w) => SoldRefusal::Refused(format!(
"the provider refused to lay {host}'s {role} again: {w}"
)),
other => SoldRefusal::Unavailable(format!("the provider: {other}")),
})?;
let now_m = relaid
.machines
.into_iter()
.find(|m| m.role == role)
.ok_or_else(|| SoldRefusal::Unavailable(format!("the provider laid no {role}")))?;
let mut words = format!(
"{host}'s {role} was laid again (it was gone at the provider): {} at {} / {}",
now_m.server, now_m.public_ipv4, now_m.utility_ipv4
);
// MONETIZE-V2-REFEED-FLEET: the new box calls home with a NEW fleet key, named by nobody.
// The lost box's key is forgotten and a pin window opened, as the guardian's own re-lay
// does: the new box pins its key on its first call (every product, a Gunnar main as any).
// The box stands either way: a failure here is said, never a refusal (that lays it twice).
if let Some(g) = state.fleet.guardian() {
let fleet = run.fleet_name();
match g
.store
.forget_box(fleet, role)
.and_then(|()| g.store.open_pin(fleet, role, now + g.cfg.pin_window_ms))
{
Ok(()) => words.push_str("; its key is pinned on its first call home"),
Err(e) => {
eprintln!("monetize-server: ALERT estate-orders.pin {company}: the new {role} was laid, but its pin window was not opened ({e}): `site open-pin` opens it");
words.push_str(&format!("; its pin window was NOT opened ({e}): the operator opens it with `site open-pin`"));
}
}
}
{
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return Err(SoldRefusal::Unavailable(format!(
"{host} is being worked on; ask again"
)));
};
if let Some(mut r) = state.estates.get(company) {
if let Some(l) = r.laid.as_mut() {
if let Some(m) = l.machines.iter_mut().find(|m| m.role == role) {
*m = now_m.clone();
}
}
r.history.push(Transition {
phase: String::new(),
at_ms: now,
note: words.clone(),
});
// The box stands at the provider now: it is answered either way (a refusal would
// lay it a second time), and an unwritten run is said loud for the operator.
if let Err(e) = super::save(state, &r) {
eprintln!("monetize-server: ALERT estate-orders.unwritten {}: {words}, but the run could not be written ({e}): its laid {role} is not named in run.json", r.reference);
}
}
}
eprintln!("monetize-server/sold-estate: {words}");
Ok(RechargeAnswer {
machine: now_m,
words,
})
}
/// One upgrade running here ([`super::EstateOrders::upgrading`]): its company, let go on drop.
struct Upgrading<'a>(
&'a std::sync::Mutex<std::collections::BTreeSet<String>>,
String,
);
impl Drop for Upgrading<'_> {
fn drop(&mut self) {
self.0
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(&self.1);
}
}
/// [`upgrade`]'s answer: whether the box was booted from the keep medium now (`false`: it
/// already runs this release, or `done` was recorded), and the machine as it stands.
#[derive(Clone, Debug)]
pub struct UpgradeAnswer {
pub upgraded: bool,
pub machine: monetize_cloud::lay::LaidMachine,
pub words: String,
}
/// **`SoldEstate.Upgrade`: the cloud half of an in-place upgrade** (F4, 2026-10-09). A sold
/// estate's front asks its shop to boot ONE standing box (`main` or `twin`) once from the
/// release's KEEP medium ([`super::KEEP_MEDIUM`]): its system rewritten, its `/data` kept, the
/// box powered off by the medium and started again from its disk
/// ([`monetize_cloud::Cloud::keep_boot`]). The front re-admits it (`readmit_kept`)
/// and then calls `done`: the shop records that the box runs this release.
///
/// Answered `upgraded = false` (nothing done) when the box already runs this release. Refused by
/// name: another role, an estate not delivered, no update open on this box (F-U-02: the
/// operator's `UpdateEstate` alone opens one), a box that does NOT stand (a lost box is laid
/// again — `Recharge`), a release with no keep medium. Signed by the estate's client key.
pub fn upgrade(
state: &AppState,
company: &str,
role: &str,
done: bool,
at_ms: i64,
signature: &[u8],
) -> Result<UpgradeAnswer, SoldRefusal> {
upgrade_as(
state,
company,
role,
done,
Caller::Signed { at_ms, signature },
)
}
/// [`upgrade`], asked by `by` (the old door's signed call, or the front's `Fleet.Ask{upgrade}`).
pub fn upgrade_as(
state: &AppState,
company: &str,
role: &str,
done: bool,
by: Caller<'_>,
) -> Result<UpgradeAnswer, SoldRefusal> {
let cfg = state.estates.cfg.as_ref().ok_or(SoldRefusal::NotSigned)?;
let now = state.now();
let call = EstateCall {
v: 1,
call: CALL_UPGRADE.into(),
estate: company.to_string(),
members: vec![role.to_string()],
gib: u64::from(done),
at_unix_ms: 0,
carries: String::new(),
};
let run = by.run(state.estates.get(company).as_ref(), &call, now)?;
// ★ ST-5: named on the site the estate was bought on.
let site_terms = super::terms_of(state, cfg, &run).unwrap_or_else(|_| cfg.clone());
// While an update is open, a box is upgraded to the release it was OPENED with (2026-10-17).
let site_terms = match run.update.as_ref().filter(|u| u.is_open()) {
Some(u) => super::update::as_opened(&site_terms, u),
None => site_terms,
};
let cfg = &site_terms;
let host = cfg.host_of(company);
let plan = super::lifecycle::of(cfg)
.map_err(SoldRefusal::Refused)?
.update_plan();
if !plan.upgraded_by_shop.iter().any(|r| r == role) {
return Err(SoldRefusal::Refused(format!(
"{role:?} is not a box the shop upgrades: {} (the {} upgrades last, by itself)",
plan.upgraded_by_shop.join(" or "),
plan.front_role
)));
}
// F-U-02 (A17): only while the operator's update is on this very box (its data kept).
super::update::upgrade_admits(&run, &host, role).map_err(SoldRefusal::Refused)?;
let laid = run
.laid
.clone()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has nothing laid")))?;
let was = laid
.machine(role)
.cloned()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has no {role} laid")))?;
let release = super::release_id(cfg, &plan.app_medium);
if done {
let words = format!(
"{host}'s {role} is re-admitted after its upgrade: it runs release {}",
short(&release)
);
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return Err(SoldRefusal::Unavailable(format!(
"{host} is being worked on; ask again"
)));
};
if let Some(mut r) = state.estates.get(company) {
r.release_of.insert(role.to_string(), release.clone());
r.history.push(Transition {
phase: String::new(),
at_ms: now,
note: words.clone(),
});
super::save(state, &r).map_err(|e| {
SoldRefusal::Unavailable(format!("the run was not saved: {e}; ask done again"))
})?;
}
eprintln!("monetize-server/sold-estate: {words}");
return Ok(UpgradeAnswer {
upgraded: false,
machine: was,
words,
});
}
if !release.is_empty() && run.release_of.get(role) == Some(&release) {
return Ok(UpgradeAnswer {
upgraded: false,
words: format!(
"{host}'s {role} already runs release {}: nothing to upgrade",
short(&release)
),
machine: was,
});
}
let (now_m, words) = keep_boot_box(state, cfg, company, role, &laid, &plan.keep_medium, now)?;
// ★ F-U-07 (A17): the box runs the kept release from here on, `done` or not. A front whose
// re-admit or port wait fails before `done` asks again and is answered "already runs".
if !release.is_empty() {
match state.estates.pen_for_a_call(cfg, company) {
Ok(_pen) => {
if let Some(mut r) = state.estates.get(company) {
r.release_of.insert(role.to_string(), release.clone());
if let Err(e) = super::save(state, &r) {
eprintln!("monetize-server/sold-estate: ALERT sold-estate.keep-boot-unsaved {host}'s {role} was keep-booted to release {} but the run was NOT saved: {e}", short(&release));
}
}
}
Err(_) => eprintln!("monetize-server/sold-estate: ALERT sold-estate.keep-boot-unsaved {host}'s {role} was keep-booted to release {} but the run is being worked on: recorded on `done`", short(&release)),
}
}
Ok(UpgradeAnswer {
upgraded: true,
machine: now_m,
words,
})
}
/// **`role` of `company` booted once from the release's keep medium** (`keep_medium`, of `cfg` —
/// the release an open update was opened with), its system rewritten, its data kept, started
/// again from its disk: the shop's keep boot, for the front's `Upgrade` and for the ONE box's own
/// step of an update (F9). A box gone at the provider is refused (it is recharged, never
/// upgraded); a failure is raised CODE RED with how the box is left.
pub(crate) fn keep_boot_box(
state: &AppState,
cfg: &EstateOrderConfig,
company: &str,
role: &str,
laid: &monetize_cloud::lay::LaidEstate,
keep_medium: &str,
now: u64,
) -> Result<(monetize_cloud::lay::LaidMachine, String), SoldRefusal> {
let host = cfg.host_of(company);
let was = laid
.machine(role)
.cloned()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has no {role} laid")))?;
let objects = super::presigned_objects(
cfg,
state.estates.custody.as_ref(),
state.estates.media_key_fed.as_deref(),
now / 1_000,
)
.map_err(SoldRefusal::Unavailable)?;
let medium = cfg
.media
.get(keep_medium)
.map(|u| u.trim().to_string())
.unwrap_or_default();
let medium_object = if medium.is_empty() {
objects
.get(keep_medium)
.filter(|o| !o.url.trim().is_empty())
.cloned()
} else {
None
};
if medium.is_empty() && medium_object.is_none() {
return Err(SoldRefusal::Refused(format!("this shop's release names no `{keep_medium}` medium: a sold box is upgraded only from the release's keep medium")));
}
let cloud = state.registry.find_cloud(&cfg.cloud).ok_or_else(|| {
SoldRefusal::Unavailable(format!("the shop's cloud {:?} is not linked", cfg.cloud))
})?;
let id = monetize_cloud::ResourceId {
provider: cloud.name().into(),
id: was.server.clone(),
};
match monetize_cloud::ask::status(&cloud, &id) {
Ok(monetize_cloud::ResourceStatus::Gone) | Err(CloudError::UnknownResource(_)) => {
return Err(SoldRefusal::Refused(format!("{host}'s {role} does not stand at the provider: a lost box is laid again (Recharge), never upgraded")));
}
Ok(_) => {}
Err(e) => {
return Err(SoldRefusal::Unavailable(format!(
"the provider could not say whether {host}'s {role} stands: {e}"
)))
}
}
let journal = state
.estates
.journal(company)
.map_err(|e| SoldRefusal::Unavailable(format!("the lay journal: {e}")))?;
{
let mut g = state
.estates
.upgrading
.lock()
.unwrap_or_else(|p| p.into_inner());
if !g.insert(company.to_string()) {
return Err(SoldRefusal::Unavailable(format!(
"an upgrade of {host} {}",
monetize_embed::fleet::UPGRADE_RUNNING
)));
}
}
let _upgrading = Upgrading(&state.estates.upgrading, company.to_string());
let keep = monetize_cloud::lay::KeepBoot {
reference: laid.reference.clone(),
zone: laid.zone.clone(),
machine: was,
medium,
medium_object,
};
let now_m = match monetize_cloud::ask::keep_boot(&cloud, &keep, &journal) {
Ok(m) => {
super::code_red::clear(state, &format!("upgrade:{company}:{role}"));
m
}
Err(e) => {
let refusal = match e {
CloudError::Refused(w) => SoldRefusal::Refused(format!(
"the provider refused to upgrade {host}'s {role}: {w}"
)),
other => SoldRefusal::Unavailable(format!("the provider: {other}")),
};
let words = match &refusal {
SoldRefusal::Refused(w) | SoldRefusal::Unavailable(w) => w.clone(),
SoldRefusal::NotSigned => String::new(),
};
// How the box is left, read from the provider's words (CODE RED audit 2, 2026-10-18).
let (what, next) = if words.contains("the medium left in") {
(format!("the {role}'s keep medium did not finish in time: stopped hard with the medium in, its disk NOT booted"), format!("look at {host}'s {role} console at the provider: the medium may have half-written its system; boot its disk only after a human looked"))
} else if words.contains("put back: the medium out") {
(format!("the {role}'s keep upgrade failed; the box was put back on its disk and runs its old release"), "ask the update again".to_string())
} else {
(format!("the {role}'s keep upgrade failed: the box may be stopped or half-done"), format!("look at {host}'s {role} at the provider: start it from its disk if it is stopped, then ask the update again"))
};
super::code_red::raise(
state,
&format!("upgrade:{company}:{role}"),
&host,
role,
&what,
&words,
&next,
);
return Err(refusal);
}
};
let words = format!("{host}'s {role} was booted from the release's keep medium: its system upgraded, its data kept, started again at {} / {}", now_m.public_ipv4, now_m.utility_ipv4);
note(state, cfg, company, &words, now);
Ok((now_m, words))
}
/// Who asks a lost front back ([`restore_front`]). **Never the front itself**: a dead front
/// signs nothing, so the trigger is its buyer (signed in with the passkey, the account's
/// `estate` ticket) or the operator (a guardian Press).
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum RestoreBy {
/// The account the estate is a row on.
Buyer(String),
/// The operator's principal.
Operator(String),
}
impl RestoreBy {
fn words(&self) -> String {
match self {
RestoreBy::Buyer(a) => format!("its buyer ({a})"),
RestoreBy::Operator(p) => format!("the operator ({p})"),
}
}
}
/// [`restore_front`]'s answer.
#[derive(Clone, Debug)]
pub struct RestoreAnswer {
/// The front as it stands now.
pub machine: monetize_cloud::lay::LaidMachine,
/// `true`: a new server (new addresses; DNS written again, main and twin follow it).
pub moved: bool,
/// The backup it was restored from.
pub backup: String,
/// The boxes changed AFTER the backup was taken (a main or twin laid again, or upgraded):
/// the restored front does not know them as they are now and re-admits them.
pub stale: Vec<String>,
pub words: String,
}
/// **What is left when no backup of the front stands** — the named fallback (OM-6): the
/// operator lays the front again from the release medium with a ONE-TIME token, the estate
/// re-admits it with its installer material (`admit_front`, `TakeMaterial`).
pub const RESTORE_FALLBACK: &str =
"re-lay the front from the release medium with a one-time token (OM-6, the operator's)";
/// ★ ACCT-6 (2026-10-29): **a front silent this long needs repair.** A front that carries the
/// update taker asks every 2 minutes, and its ask is written down at most hourly
/// ([`super::update::TAKEN_KEPT_EVERY_MS`]): three hours unwritten is a front that stopped.
pub const FRONT_SILENT_MS: u64 = 3 * super::update::TAKEN_KEPT_EVERY_MS;
/// Pure: ★ ACCT-6 — **why this estate's front needs a repair** ([`restore_front`]), in the
/// words a purchase row shows; empty while it stands. Only a RUNNING estate (`captured` or
/// `grace`), laid here (never adopted), with a front, whose front once asked for its update
/// and has been silent past [`FRONT_SILENT_MS`]. A front that never asked (a release with no
/// taker) is unknown, not lost: no repair on a guess.
pub fn repair_words(run: &EstateRun, host: &str, now_ms: u64) -> String {
let running = matches!(run.phase.as_str(), phase::CAPTURED | phase::GRACE);
let has_front = run
.laid
.as_ref()
.is_some_and(|l| l.machine("front").is_some());
if !running || !has_front || super::adopt::is_adopted(run) || run.update_taken_at_ms == 0 {
return String::new();
}
let silent = now_ms.saturating_sub(run.update_taken_at_ms);
if silent <= FRONT_SILENT_MS {
return String::new();
}
format!("{host}'s front has not answered for {} hours. Restore it from its daily backup, with its keys.", silent / 3_600_000)
}
static RESTORING: Mutex<BTreeSet<String>> = Mutex::new(BTreeSet::new());
struct Restoring(String);
impl Drop for Restoring {
fn drop(&mut self) {
RESTORING
.lock()
.unwrap_or_else(|p| p.into_inner())
.remove(&self.0);
}
}
/// **S8: a sold estate's lost FRONT restored from its daily backup** (BACKUP-AUDIT §3,
/// 2026-10-11). The front holds the estate's own keys (its CA, its link keys, the pins of
/// main and twin); S6 lays it with the provider's daily backup of its one disk, and a front
/// restored from that backup keeps every key — nothing is re-trusted, nothing re-paired, no
/// installer material taken again. The trigger is `by` — the buyer or the operator; the dead
/// front cannot sign its own request.
///
/// In place when its server stands (stopped, the backup written over its disk, started); made
/// again when it is gone (its disk from the backup, a new server, the estate's firewall written
/// for the new address, the estate's DNS written again here). Main and twin follow a moved
/// front by their own re-feed (gunnar's installer listener re-points its link).
///
/// Refused by name: an estate not running (`captured` or `grace`), no front laid, and an estate
/// whose main and twin have not been heard within the guardian's silence window — a front
/// alone is not restored into an estate that is down: the front does not report on Fleet, so
/// the boxes that do are the proof the rest stands (code, not the audit: the audit's "front
/// silent ≥ N min" cannot be read). No backup: refused naming [`RESTORE_FALLBACK`], nothing
/// changed. Boxes laid again or upgraded after the backup are named in [`RestoreAnswer::stale`].
pub fn restore_front(
state: &AppState,
company: &str,
by: RestoreBy,
) -> Result<RestoreAnswer, SoldRefusal> {
let cfg = state
.estates
.cfg
.as_ref()
.ok_or_else(|| SoldRefusal::Refused("this monetize sells no estates".into()))?;
let now = state.now();
let run = state
.estates
.get(company)
.ok_or_else(|| SoldRefusal::Refused(format!("{company} is no sold estate here")))?;
// ★ ST-5: named on the site the estate was bought on.
let site_terms = super::terms_of(state, cfg, &run).unwrap_or_else(|_| cfg.clone());
let cfg = &site_terms;
let host = cfg.host_of(company);
if !matches!(run.phase.as_str(), phase::CAPTURED | phase::GRACE) {
return Err(SoldRefusal::Refused(format!(
"{host} is not running ({}): its front is not restored",
run.phase
)));
}
if super::adopt::is_adopted(&run) {
return Err(SoldRefusal::Refused(super::adopt::not_laid_here(
&run,
"restoring its front",
)));
}
let laid = run
.laid
.clone()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has nothing laid")))?;
let update_plan = super::lifecycle::of(cfg)
.map_err(SoldRefusal::Refused)?
.update_plan();
let front = update_plan.front_role.as_str();
let was = laid
.machine(front)
.cloned()
.ok_or_else(|| SoldRefusal::Refused(format!("{host} has no {front} laid")))?;
let silent_after = state
.fleet
.guardian()
.map(|g| g.cfg.silent_after_ms)
.ok_or_else(|| {
SoldRefusal::Unavailable(
"the fleet store is not open: monetize cannot hear the estate".into(),
)
})?;
let others: Vec<String> = laid
.machines
.iter()
.filter(|m| m.role != front)
.map(|m| m.role.clone())
.collect();
let heard = crate::fleet::heard_since(state, company, &others, now.saturating_sub(silent_after)).map_err(|e| {
SoldRefusal::Refused(format!("{host}'s {} have not been heard in the last {} s ({e}): a {front} is restored only into an estate whose boxes stand", others.join(" and "), silent_after / 1_000))
})?;
let plan = super::lifecycle::lay_plan_of(cfg, company, &run.quote.sizes, &run.quote.twin_plan)
.map_err(SoldRefusal::Refused)?;
let (narrate_from, shape) = (plan.narrate_from.clone(), plan.shape.clone());
let front_plan = plan
.machines
.into_iter()
.find(|m| m.role == front)
.ok_or_else(|| SoldRefusal::Unavailable(format!("{host}'s plan names no {front}")))?;
let cloud = state.registry.find_cloud(&cfg.cloud).ok_or_else(|| {
SoldRefusal::Unavailable(format!("the shop's cloud {:?} is not linked", cfg.cloud))
})?;
let journal = state
.estates
.journal(company)
.map_err(|e| SoldRefusal::Unavailable(format!("the lay journal: {e}")))?;
{
let mut g = RESTORING.lock().unwrap_or_else(|p| p.into_inner());
if !g.insert(company.to_string()) {
return Err(SoldRefusal::Unavailable(format!(
"a restore of {host}'s front is already running here"
)));
}
}
let _restoring = Restoring(company.to_string());
let ask = monetize_cloud::lay::RestoreFromBackup {
reference: laid.reference.clone(),
zone: laid.zone.clone(),
machine: was.clone(),
plan: front_plan,
narrate_from,
shape,
};
let restored =
monetize_cloud::ask::restore_from_backup(&cloud, &ask, &journal).map_err(|e| match e {
CloudError::Refused(w) => SoldRefusal::Refused(format!(
"the provider refused to restore {host}'s front: {w}"
)),
other => SoldRefusal::Unavailable(format!("the provider: {other}")),
})?;
let Some(restored) = restored else {
return Err(SoldRefusal::Refused(format!("no backup of {host}'s front stands at the provider: nothing was changed; {RESTORE_FALLBACK}")));
};
let stale: Vec<String> = update_plan
.upgraded_by_shop
.iter()
.map(String::as_str)
.filter(|role| {
run.history.iter().any(|t| {
t.at_ms >= restored.backup_created_ms
&& t.note.starts_with(&format!("{host}'s {role} "))
&& (t.note.contains("was laid again")
|| t.note.contains("booted from the release's keep medium"))
})
})
.map(String::from)
.collect();
let now_m = restored.machine.clone();
let dns = if restored.moved {
let records = super::records_for(company, &cfg.apex, &now_m.public_ipv4)
.map_err(SoldRefusal::Refused)?;
match state.estates.dns_on(&run.estate).write(&records) {
Ok(line) => Some((records, line)),
Err(e) => {
let words = format!("{host}'s front was restored from backup {} at a NEW server {} ({}), but its DNS was not written: {e}", restored.backup, now_m.server, now_m.public_ipv4);
save_front(
state,
cfg,
company,
front,
&now_m,
None,
Transition {
phase: String::new(),
at_ms: now,
note: words.clone(),
},
)?;
return Err(SoldRefusal::Unavailable(words));
}
}
} else {
None
};
let mut words = format!(
"{host}'s front was restored from its backup {} (taken {}), asked by {}; {}: its own keys kept, nothing re-trusted",
restored.backup,
if restored.backup_created_ms > 0 { iso_date(restored.backup_created_ms) } else { "at an unread time".into() },
by.words(),
if restored.moved { format!("made again at {} / {} (main and twin follow it)", now_m.public_ipv4, now_m.utility_ipv4) } else { format!("in place on {}", now_m.server) },
);
if let Some((_, line)) = &dns {
words.push_str(&format!("; {line}"));
}
if !stale.is_empty() {
words.push_str(&format!(
"; changed after the backup, re-admit: {}",
stale.join(", ")
));
}
words.push_str(&format!(" (heard: {})", heard.join(", ")));
save_front(
state,
cfg,
company,
front,
&now_m,
dns.map(|(r, _)| r),
Transition {
phase: String::new(),
at_ms: now,
note: words.clone(),
},
)?;
Ok(RestoreAnswer {
machine: now_m,
moved: restored.moved,
backup: restored.backup,
stale,
words,
})
}
/// The run names the front as it stands (and its DNS, when written again), with one line
/// (`line`, its history entry).
fn save_front(
state: &AppState,
cfg: &EstateOrderConfig,
company: &str,
role: &str,
front: &monetize_cloud::lay::LaidMachine,
dns: Option<Vec<super::DnsRecord>>,
line: Transition,
) -> Result<(), SoldRefusal> {
let words = line.note.clone();
let host = cfg.host_of(company);
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return Err(SoldRefusal::Unavailable(format!(
"{host} is being worked on; its restored front is not written down yet: {words}"
)));
};
if let Some(mut r) = state.estates.get(company) {
if let Some(m) = r
.laid
.as_mut()
.and_then(|l| l.machines.iter_mut().find(|m| m.role == role))
{
*m = front.clone();
}
if let Some(d) = dns {
r.dns = d;
// DNS-WRITER: written again by the site's writer of now (`dns_on`, the restore's).
r.dns_writer = state.estates.dns_name_on(&r.estate);
}
r.history.push(line);
super::save(state, &r)
.map_err(|e| SoldRefusal::Unavailable(format!("the run was not saved: {e}")))?;
}
eprintln!("monetize-server/sold-estate: {words}");
Ok(())
}
/// [`take_material`]'s answer: the bundle (empty with `done`) and one sentence.
#[derive(Clone, Debug)]
pub struct MaterialAnswer {
pub bundle: Vec<u8>,
pub words: String,
}
/// Whether the hand-off's forget waits for the estate to take its installer material: the
/// pairing handed one back, the estate has not taken it, and the buyer was told less than
/// [`MATERIAL_TAKE_WITHIN_MS`] ago.
pub fn material_waits(custody: &crate::custody::Custody, run: &EstateRun, now: u64) -> bool {
run.material_taken_at_ms == 0
&& now < run.told_tried_at_ms.saturating_add(MATERIAL_TAKE_WITHIN_MS)
&& custody
.installer_material(&run.company)
.ok()
.flatten()
.is_some()
}
/// **`SoldEstate.TakeMaterial`: the estate takes its own installer material** (F4, 2026-10-10).
/// The shop's pairing hands the bundle (the estate's CA and its key, the installer's
/// certificate, the link keys, the seeds, the ledger and the plan) back into the custody; the
/// sold estate's FRONT pulls it here, keeps it in its primary's sealed slot, and calls `done` —
/// only then does the hand-off forget it with the rest ([`material_waits`]). Before, the shop
/// PUT it into the front's door (`/slot/installer-material`): a call INTO an estate, gone.
///
/// Refused by name: an estate not paired nor delivered ([`material_admits`]), a shop with no custody, a bundle never handed
/// back or already forgotten. Signed by the estate's client key.
pub fn take_material(
state: &AppState,
company: &str,
done: bool,
at_ms: i64,
signature: &[u8],
) -> Result<MaterialAnswer, SoldRefusal> {
take_material_as(state, company, done, Caller::Signed { at_ms, signature })
}
/// [`take_material`], asked by `by` (the old door's signed call, or `Fleet.Ask{material}`).
pub fn take_material_as(
state: &AppState,
company: &str,
done: bool,
by: Caller<'_>,
) -> Result<MaterialAnswer, SoldRefusal> {
let cfg = state.estates.cfg.as_ref().ok_or(SoldRefusal::NotSigned)?;
let now = state.now();
let call = EstateCall {
v: 1,
call: CALL_TAKE_MATERIAL.into(),
estate: company.to_string(),
members: Vec::new(),
gib: u64::from(done),
at_unix_ms: 0,
carries: String::new(),
};
let run = by.run(state.estates.get(company).as_ref(), &call, now)?;
// ★ ST-5: named on the site the estate was bought on.
let site_terms = super::terms_of(state, cfg, &run).unwrap_or_else(|_| cfg.clone());
let cfg = &site_terms;
let host = cfg.host_of(company);
material_admits(&run, &host).map_err(SoldRefusal::Refused)?;
let custody = state.estates.custody.as_ref().ok_or_else(|| {
SoldRefusal::Refused("this shop keeps no custody: it holds no installer material".into())
})?;
if done {
let words =
format!("{host} took its installer material: the shop forgets it at the hand-off");
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return Err(SoldRefusal::Unavailable(format!(
"{host} is being worked on; ask again"
)));
};
if let Some(mut r) = state.estates.get(company) {
if r.material_taken_at_ms == 0 {
r.material_taken_at_ms = now.max(1);
r.history.push(Transition {
phase: String::new(),
at_ms: now,
note: words.clone(),
});
super::save(state, &r).map_err(|e| {
SoldRefusal::Unavailable(format!("the run was not saved: {e}; ask done again"))
})?;
}
}
eprintln!("monetize-server/sold-estate: {words}");
return Ok(MaterialAnswer {
bundle: Vec::new(),
words,
});
}
if run.secrets_forgotten_at_ms > 0 || !custody.holds(company) {
return Err(SoldRefusal::Refused(format!("the shop forgot {host}'s secrets at the hand-off: its installer material is the estate's own (its primary's slot)")));
}
match custody.installer_material(company) {
Ok(Some(b)) => Ok(MaterialAnswer { bundle: b.into_bytes(), words: format!("{host}'s installer material, as its pairing left it") }),
Ok(None) => Err(SoldRefusal::Refused(format!("{host}'s pairing handed no installer material back (an estate paired before F4, or by the kit)"))),
Err(e) => Err(SoldRefusal::Unavailable(format!("the custody: {e}"))),
}
}
/// Pure: **may `run`'s front take its installer material in its phase?** (F8, 2026-10-21) A
/// delivered estate (`captured`, `grace`, `suspended`, `deleting`) does; so does a `paired` one —
/// every box PAIRED (its installer's PASS, or adopted on a re-check): the material is the
/// estate's own and its payment is held. Refused before, a paired estate could never take it:
/// its primary keeps none, so its update fails on the twin ("no installer material is kept in
/// … monetize-backup yet"), and its main reports — the capture — only after that update
/// (test3-holger-rs). The hand-off still forgets the custody's copy only once the estate is
/// delivered ([`material_waits`]). An install still running and every other phase (a terminated
/// estate among them) are refused by name. Gunnar's and Holger's alike: one lifecycle.
pub fn material_admits(run: &EstateRun, host: &str) -> Result<(), String> {
if phase::is_term(&run.phase) || run.phase == phase::PAIRED {
return Ok(());
}
if matches!(run.phase.as_str(), phase::LAYING | phase::INSTALLING) {
return Err(format!("{host} is not delivered ({}): its install is still running, and no installer material is handed over before every box is PAIRED", run.phase));
}
Err(format!(
"{host} is not delivered ({}): its installer material is taken once every box is PAIRED",
run.phase
))
}
/// A release identity, short, for a sentence.
fn short(release: &str) -> &str {
if release.is_empty() {
"(unnamed)"
} else {
&release[..release.len().min(12)]
}
}
/// One line in the run's history, under its pen.
fn note(state: &AppState, cfg: &EstateOrderConfig, company: &str, words: &str, now: u64) {
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return;
};
if let Some(mut r) = state.estates.get(company) {
r.history.push(Transition {
phase: String::new(),
at_ms: now,
note: words.to_string(),
});
if let Err(e) = super::save(state, &r) {
eprintln!("monetize-server: ALERT estate-orders.unwritten {}: a history line could not be written ({e}): {words}", r.reference);
}
}
eprintln!("monetize-server/sold-estate: {words}");
}
/// **`Fleet.Ask{grow_set}`** (G16-d): the calling box — already authenticated by its fleet
/// key as `role` of `company` — sizes ITS OWN laid data disks: `members` (none = all of
/// them) to `gib` a disk (0 = what its role is paid to): no more than is paid, never smaller, at
/// this shop's provider, and the sizes read back into the run.
pub fn grow_own(
state: &AppState,
company: &str,
role: &str,
members: &[String],
gib: u64,
) -> Result<GrowSetAnswer, SoldRefusal> {
let cfg = state
.estates
.cfg
.as_ref()
.ok_or_else(|| SoldRefusal::Unavailable("this monetize sells no estates".into()))?;
let now = state.now();
let run = state
.estates
.get(company)
.filter(|r| !phase::is_gone(&r.phase) && r.laid.is_some())
.ok_or_else(|| SoldRefusal::Refused(format!("{company} is no laid sold estate here")))?;
// ★ ST-5: named on the site the estate was bought on.
let site_terms = super::terms_of(state, cfg, &run).unwrap_or_else(|_| cfg.clone());
let cfg = &site_terms;
let own: Vec<String> = recorded(cfg, &run, now)
.into_iter()
.filter(|l| l.role == role)
.map(|l| l.member)
.collect();
if own.is_empty() {
return Err(SoldRefusal::Refused(format!(
"the {role} of {company} has no laid data disk"
)));
}
if let Some(m) = members.iter().find(|m| !own.contains(m)) {
return Err(SoldRefusal::Refused(format!(
"the {role} sizes its own data disks: {m} is not one of them"
)));
}
let members = if members.is_empty() {
own
} else {
members.to_vec()
};
let gib = if gib == 0 {
paid_gib_for(cfg, &run, role, now)
} else {
gib
};
grow_verified(state, cfg, &run, company, &members, gib, now)
}
/// The caller is who it says: its named disks to `gib`, at the provider, read back.
fn grow_verified(
state: &AppState,
cfg: &EstateOrderConfig,
run: &EstateRun,
company: &str,
members: &[String],
gib: u64,
now: u64,
) -> Result<GrowSetAnswer, SoldRefusal> {
let lines = recorded(cfg, run, now);
let (role, server) = one_machine(company, &lines, members)?;
let host = cfg.host_of(company);
let named: Vec<&DiskLine> = lines
.iter()
.filter(|l| members.contains(&l.member))
.collect();
let paid = paid_gib_for(cfg, run, &role, now);
if gib > paid {
return Err(SoldRefusal::Refused(format!(
"{host}'s {role} data is paid to {paid} GB a disk; {gib} GB is bought at {} first",
cfg.apex
)));
}
if let Some(bigger) = named.iter().find(|l| l.gib > gib) {
return Err(SoldRefusal::Refused(format!(
"a disk never shrinks: {} is {} GB, and {gib} GB was asked",
bigger.member, bigger.gib
)));
}
let from = named.iter().map(|l| l.gib).min().unwrap_or(0);
{
let mut g = GROWING.lock().unwrap_or_else(|p| p.into_inner());
if !g.insert(company.to_string()) {
return Err(SoldRefusal::Unavailable(format!(
"a growth of {host}'s disks is already running here"
)));
}
}
let _growing = Growing(company.to_string());
let cloud = state.registry.find_cloud(&cfg.cloud).ok_or_else(|| {
SoldRefusal::Unavailable(format!("the shop's cloud {:?} is not linked", cfg.cloud))
})?;
let dir = state
.estates
.run_dir(company)
.ok_or_else(|| SoldRefusal::Unavailable("no estate-orders directory".into()))?;
let journal = monetize_cloud::grow::Journal::open(dir.join(format!("grow-{role}.journal")))
.map_err(|e| SoldRefusal::Unavailable(format!("the growth journal: {e}")))?;
let suffix = data_roles(cfg)
.into_iter()
.find(|d| d.role == role)
.map(|d| d.set_suffix)
.unwrap_or_default();
let set = set_of_as(&cfg.product, &suffix, &server, members, gib);
// On the provider's estate lane: a sold estate's resize never holds the
// shop's own purchases back (2026-10-08).
let outcome = monetize_cloud::ask::grow(&cloud, &set, true, &journal).map_err(|e| match e {
// **The shop's cap holds a PAID step back** (A54, the owner
// 2026-10-08): the estate is told to ask again — UNAVAILABLE, in words
// that name no cap — and the operator is told to raise it.
CloudError::OverCap(w) => {
super::cap_alert(
&run.reference,
&format!("{}: {w}", super::ORDERS_WAIT),
false,
state.now(),
);
SoldRefusal::Unavailable(GROWTH_WAITS.to_string())
}
CloudError::Refused(w) => SoldRefusal::Refused(format!("the provider refused: {w}")),
other => SoldRefusal::Unavailable(format!("the provider: {other}")),
})?;
// What is laid now, as the provider answers it: read back into the run.
let survey = monetize_cloud::ask::survey(
&cloud,
&DataSet {
target_gib: 0,
..set.clone()
},
)
.ok();
let reference = run
.growth
.as_ref()
.map(|g| g.reference.clone())
.unwrap_or_else(|| run.reference.clone());
let to = survey
.as_ref()
.map(|s| s.current_gib)
.filter(|g| *g > 0)
.unwrap_or(match &outcome {
GrowOutcome::Done { to_gib, .. } => *to_gib,
_ => from,
});
let words = match &outcome {
GrowOutcome::Done { .. } if to > from => format!("{company} grew {role} data {from} → {to} GiB per disk, paid order {reference}"),
GrowOutcome::Done { .. } => format!("{company}'s {role} data is {to} GiB per disk already (paid order {reference}): nothing to grow"),
GrowOutcome::Partial { why, .. } => format!("{company}'s {role} data grew in part toward {gib} GiB per disk (paid order {reference}): {why}"),
GrowOutcome::Untouched { why } => format!("{company}'s {role} data was not grown (paid order {reference}): {why}"),
};
eprintln!("monetize-server/sold-estate: {words}");
skidbladnir::functional_status(
"monetize-server/sold-estate",
outcome.word(),
!matches!(outcome, GrowOutcome::Untouched { .. }),
&words,
);
if let Some(s) = survey.as_ref() {
read_back(state, cfg, company, s, &words, now);
}
let disks = survey
.as_ref()
.map(|s| surveyed(&lines, s))
.unwrap_or_else(|| named.into_iter().cloned().collect());
Ok(GrowSetAnswer {
disks,
outcome,
words,
})
}
/// The provider's sizes into the run's laid disks (only ever up), and the
/// quoted data size with main's — under the run's pen.
fn read_back(
state: &AppState,
cfg: &EstateOrderConfig,
company: &str,
survey: &SetSurvey,
words: &str,
now: u64,
) {
let Ok(_pen) = state.estates.pen_for_a_call(cfg, company) else {
return;
};
let Some(mut run) = state.estates.get(company) else {
return;
};
let Some(laid) = run.laid.as_mut() else {
return;
};
let mut moved = false;
for m in laid.machines.iter_mut() {
for d in m.disks.iter_mut().skip(1) {
if let Some(v) = survey.volumes.iter().find(|v| v.id.0 == d.id) {
if v.gib > d.gib {
d.gib = v.gib;
moved = true;
}
}
}
}
let primary = data_roles(cfg)
.into_iter()
.next()
.map(|d| d.role)
.unwrap_or_default();
if let Some(main) = run.laid.as_ref().and_then(|l| l.machine(&primary)) {
let smallest = main.disks.iter().skip(1).map(|d| d.gib).min().unwrap_or(0);
if smallest > run.quote.sizes.main_data_gib {
run.quote.sizes.main_data_gib = smallest;
}
}
if moved {
run.history.push(Transition {
phase: String::new(),
at_ms: now,
note: words.to_string(),
});
if let Err(e) = super::save(state, &run) {
eprintln!("monetize-server: ALERT estate-orders.unwritten {}: the survey's sizes could not be written ({e}): {words}", run.reference);
}
}
}
#[cfg(test)]
mod tests {
/// ★ **OM-13: a grown set is booked under the product the box sells** —
/// `grow/<product>/<server>`, the twin's `<product>-twin`.
#[test]
fn a_grown_set_is_booked_under_the_configured_product() {
let set = super::set_of_as("gunnar-test2", "", "srv-1", &["v1".into()], 20);
assert_eq!(
(set.product.as_str(), set.resource_key()),
("gunnar-test2", "grow/gunnar-test2/srv-1".to_string())
);
assert_eq!(
super::set_of_as("gunnar-test2", "-twin", "srv-2", &[], 0).product,
"gunnar-test2-twin"
);
}
}