crates/monetize-admin/tests/offsite.rs
//! **BK-0's restore test**: a backup nobody restored is not a backup. A box with
//! real ledger history is copied off (pack → age-seal to a THROWAWAY key → the
//! store), then the copy is fetched back, opened with that key and imported into
//! fresh stores under the bundled signing root — the ledger folds back, row for
//! row; the custody's KEK and sealed seeds come back byte for byte.
use std::path::Path;
use monetize::testing::{purchase, Answer, MockCloud, MockPay, MockProduct, NOW};
use monetize::{Ledger, Signer, Transaction};
use monetize_admin::offsite::{self, Outcome, Remote as _};
use monetize_front::{intake, ledger_backup as lb};
use monetize_server::backup as mb;
fn hex(b: &[u8]) -> String {
b.iter().map(|x| format!("{x:02x}")).collect()
}
/// A box root with real history: three tenants bought, monetize's export
/// written where the box's monetize writes it, a custody store, an identity.
/// Answers the export's ledger rows and the signing key's public half.
fn a_box_with_history(root: &Path) -> (usize, String) {
let cfg = monetize_server::config::Config {
data_dir: root.join(intake::DATA_REL),
..Default::default()
};
std::fs::create_dir_all(cfg.data_dir.join("custody")).unwrap();
let signer = Signer::from_bytes(&[0x5A; 32]);
signer.write_to(&cfg.signing_key_path()).unwrap();
let ledger = Ledger::open(&cfg.ledger_path()).unwrap();
let usage = monetize_server::state::UsageStore::open(&cfg.usage_path()).unwrap();
let signups = monetize_server::signup::SignupStore::open(&cfg.signup_path()).unwrap();
let reconcile =
monetize_server::reconcile::ReconcileStore::open(&cfg.reconcile_path()).unwrap();
let nonpay = monetize_server::nonpay::NonpayJournal::open(cfg.nonpay_path()).unwrap();
let (pay, cloud, product) = (MockPay::new(), MockCloud::new(), MockProduct::gunnar());
cloud.answer_all(Answer::Provisioned);
for t in ["alice", "bob", "team/sub"] {
Transaction {
pay: monetize::pay::Vendor::of(&pay),
cloud: &cloud,
product: product.served(),
ledger: &ledger,
signer: &signer,
now_unix_ms: NOW,
}
.run(&purchase(t))
.unwrap();
}
let stores = mb::Stores {
ledger: &ledger,
usage: &usage,
signups: &signups,
reconcile: &reconcile,
nonpay: &nonpay,
};
let m = mb::export(
&root.join(lb::BACKUP_REL),
&stores,
&hex(&signer.public_key()),
)
.unwrap();
std::fs::write(cfg.data_dir.join("custody/kek"), [0xC3; 32]).unwrap();
std::fs::write(
cfg.data_dir.join("custody/acme.sealed"),
b"acme's seeds and CA key, sealed under the kek",
)
.unwrap();
std::fs::write(
root.join(intake::MONETIZE_REL).join("identity-acme"),
b"acme identity",
)
.unwrap();
assert!(m.ledger_rows > 0, "the rig wrote no history");
(m.ledger_rows, hex(&signer.public_key()))
}
fn throwaway() -> (String, String) {
use age::secrecy::ExposeSecret as _;
let id = age::x25519::Identity::generate();
(
id.to_string().expose_secret().to_string(),
id.to_public().to_string(),
)
}
fn fed(recipient: &str) -> offsite::Config {
let feed = [
(offsite::FEED_RECIPIENTS, recipient),
(
offsite::FEED_ENDPOINT,
"https://acct.r2.cloudflarestorage.com",
),
(offsite::FEED_ACCESS_KEY, "throwaway-access"),
(offsite::FEED_SECRET_KEY, "throwaway-secret"),
(offsite::FEED_PREFIX, "one-monetize/test"),
]
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect();
offsite::Config::of(&feed).unwrap()
}
#[test]
fn the_off_box_copy_round_trips_and_the_ledger_reads_back() {
let d = tempfile::tempdir().unwrap();
let (rows, pubkey) = a_box_with_history(d.path());
let (identity, recipient) = throwaway();
let store = offsite::MemRemote::default();
let Outcome::Shipped { object, .. } = offsite::backup_once(
d.path(),
&fed(&recipient),
&store,
&|_| Ok(()),
1_791_698_797,
)
.unwrap() else {
panic!("not shipped")
};
let sealed = store.0.lock().unwrap()[&object].clone();
assert_eq!(store.size(&object).unwrap(), Some(sealed.len() as u64));
let entries = lb::decode(&offsite::open(&identity, &sealed).unwrap()).unwrap();
let scratch = d.path().join("drill");
let v = offsite::verify(&entries, &scratch).unwrap();
assert_eq!(v.ledger_rows, rows as u64, "every ledger row folds back");
assert_eq!(
v.signing_public_key, pubkey,
"the signing root is the box's"
);
assert_eq!(v.custody_sealed, 1);
assert!(
!scratch.exists(),
"the drill's plaintext stores are removed"
);
assert_eq!(entries["data/custody/kek"], vec![0xC3; 32]);
assert_eq!(
entries["data/custody/acme.sealed"],
b"acme's seeds and CA key, sealed under the kek".to_vec()
);
}
#[test]
fn a_copy_without_its_custody_kek_or_with_a_foreign_signing_key_does_not_verify() {
let d = tempfile::tempdir().unwrap();
a_box_with_history(d.path());
let mut entries = offsite::pack(d.path()).unwrap();
entries.remove("data/custody/kek");
let e = offsite::verify(&entries, &d.path().join("s1")).unwrap_err();
assert!(
e.starts_with("[offsite-verify]") && e.contains("kek"),
"{e}"
);
let mut entries = offsite::pack(d.path()).unwrap();
let foreign = d.path().join("foreign.key");
Signer::from_bytes(&[1; 32]).write_to(&foreign).unwrap();
entries.insert("data/signing.key".into(), std::fs::read(&foreign).unwrap());
let e = offsite::verify(&entries, &d.path().join("s2")).unwrap_err();
assert!(e.starts_with("[offsite-verify]"), "{e}");
}
#[test]
fn the_operators_open_writes_the_files_and_refuses_a_used_directory() {
use std::os::unix::fs::PermissionsExt as _;
let d = tempfile::tempdir().unwrap();
let box_root = d.path().join("box");
a_box_with_history(&box_root);
let (identity, recipient) = throwaway();
let store = offsite::MemRemote::default();
let Outcome::Shipped { object, .. } = offsite::backup_once(
&box_root,
&fed(&recipient),
&store,
&|_| Ok(()),
1_791_698_797,
)
.unwrap() else {
panic!()
};
let archive = d.path().join("copy.mfb.age");
std::fs::write(&archive, &store.0.lock().unwrap()[&object]).unwrap();
let id_file = d.path().join("offsite.age-identity");
std::fs::write(&id_file, format!("# throwaway\n{identity}\n")).unwrap();
std::fs::set_permissions(&id_file, std::fs::Permissions::from_mode(0o600)).unwrap();
let into = d.path().join("restored");
let said = offsite::open_archive(&id_file, &archive, &into).unwrap();
assert!(
said.contains("ledger rows") && !said.contains(&identity),
"{said}"
);
let kek = into.join("data/custody/kek");
assert_eq!(std::fs::read(&kek).unwrap(), vec![0xC3; 32]);
assert_eq!(
std::fs::metadata(&kek).unwrap().permissions().mode() & 0o777,
0o600
);
assert!(into.join("export").join(mb::MANIFEST).is_file());
let e = offsite::open_archive(&id_file, &archive, &into).unwrap_err();
assert!(
e.starts_with("[offsite-open]") && e.contains("not empty"),
"{e}"
);
let words: Vec<String> = [
"open",
"--identity",
id_file.to_str().unwrap(),
"--archive",
archive.to_str().unwrap(),
"--into",
d.path().join("again").to_str().unwrap(),
]
.map(String::from)
.to_vec();
assert!(offsite::cli(&words).unwrap().contains("ledger rows"));
}
/// ★ **ST-12: the ONE box comes back from its off-box copy** (STINKY 2026-10-11: there was
/// a pack, a seal, a ship and an operator's scratch import — and no way to put the copy back
/// on a box). The operator opens the copy on their own machine (the vault identity never
/// reaches a box) and puts it on a FRESH box's door (`PUT /restore`); the box stages it,
/// verified, and at its next start — before monetize runs — writes the signing root, the
/// custody, the identities and the export back and imports the ledger. monetize then offers
/// the RECORDED key again: every estate's trust holds. A box that already serves (estates
/// joined, custody sealed) is refused by name: a restore never overwrites a live box.
#[test]
fn an_opened_copy_restores_a_fresh_box_its_signing_root_ledger_and_custody() {
use monetize_admin::restore;
let d = tempfile::tempdir().unwrap();
let old = d.path().join("old");
let (rows, pubkey) = a_box_with_history(&old);
let entries = offsite::pack(&old).unwrap();
let fresh = d.path().join("fresh");
intake::prepare(&fresh).unwrap();
let key_at = fresh.join(intake::DATA_REL).join("signing.key");
assert_ne!(
hex(&Signer::from_path(&key_at).unwrap().public_key()),
pubkey,
"a fresh box minted a key of its own"
);
let said = restore::stage(&fresh, &entries).unwrap();
assert!(
said.contains(&pubkey) && said.contains("next start"),
"{said}"
);
assert!(fresh.join(restore::STAGED_REL).is_file());
assert_ne!(
hex(&Signer::from_path(&key_at).unwrap().public_key()),
pubkey,
"nothing is written while monetize may run"
);
let said = restore::apply_staged(&fresh)
.unwrap()
.expect("a staged copy is applied");
assert!(said.contains(&format!("{rows} ledger rows")), "{said}");
assert_eq!(
hex(&Signer::from_path(&key_at).unwrap().public_key()),
pubkey,
"the signing root every estate trusts is back"
);
assert_eq!(
std::fs::read(fresh.join(intake::DATA_REL).join("custody/kek")).unwrap(),
vec![0xC3; 32]
);
assert!(fresh
.join(intake::MONETIZE_REL)
.join("identity-acme")
.is_file());
let cfg = monetize_server::config::Config {
data_dir: fresh.join(intake::DATA_REL),
..Default::default()
};
let ledger = Ledger::open(&cfg.ledger_path()).unwrap();
let usage = monetize_server::state::UsageStore::open(&cfg.usage_path()).unwrap();
let signups = monetize_server::signup::SignupStore::open(&cfg.signup_path()).unwrap();
let reconcile =
monetize_server::reconcile::ReconcileStore::open(&cfg.reconcile_path()).unwrap();
let nonpay = monetize_server::nonpay::NonpayJournal::open(cfg.nonpay_path()).unwrap();
let stores = mb::Stores {
ledger: &ledger,
usage: &usage,
signups: &signups,
reconcile: &reconcile,
nonpay: &nonpay,
};
let again = mb::export(&d.path().join("re-export"), &stores, &pubkey).unwrap();
assert_eq!(
again.ledger_rows, rows,
"the ledger is the old box's, row for row"
);
assert!(!fresh.join(restore::STAGED_REL).exists(), "applied once");
assert!(fresh.join(restore::APPLIED_REL).is_file());
assert_eq!(
restore::apply_staged(&fresh).unwrap(),
None,
"nothing staged, nothing done"
);
let busy = d.path().join("busy");
a_box_with_history(&busy);
let e = restore::stage(&busy, &entries).unwrap_err();
assert!(
e.contains("[restore-not-fresh]") && e.contains("acme.sealed"),
"{e}"
);
let e = restore::stage(&fresh, &entries).unwrap_err();
assert!(
e.contains("[restore-not-fresh]"),
"a restored box serves: {e}"
);
let mut bad = entries.clone();
bad.remove("data/signing.key");
let other = d.path().join("other");
intake::prepare(&other).unwrap();
assert!(restore::stage(&other, &bad)
.unwrap_err()
.contains("[offsite-verify]"));
let body = serde_json::to_string(
&entries
.iter()
.map(|(k, v)| (k.clone(), hex(v)))
.collect::<std::collections::BTreeMap<_, _>>(),
)
.unwrap();
assert_eq!(
restore::entries_of_json(body.as_bytes()).unwrap(),
entries,
"the door's body: entry → hex"
);
assert!(restore::entries_of_json(b"{\"a\": \"zz\"}")
.unwrap_err()
.contains("[restore-json]"));
}
/// PID 1 applies a staged restore BEFORE monetize's key is prepared and before any child
/// starts: the copy's signing root is the one monetize then offers.
#[test]
fn pid1_applies_a_staged_restore_before_monetize_prepares_or_starts() {
let src = include_str!("../src/main.rs");
let applied = src
.find("restore::apply_staged(&root)")
.expect("PID 1 applies a staged restore");
let prepared = src
.find("monetize_front::intake::prepare(&root)")
.expect("PID 1 prepares monetize's key");
assert!(applied < prepared, "the restore goes first");
}