use std::sync::Arc;
use std::time::Duration;
use gunnar_repair::exec::{GrowCheck, GrowState};
use gunnar_repair::{Refusal, RefusalCode};
use crate::grow::GrowMachine;
pub const SECTORS_PER_GIB: u64 = 2_097_152;
pub(crate) fn refuse(code: RefusalCode, message: impl Into<String>) -> Refusal {
Refusal { code, message: message.into() }
}
pub const GROWTH_SIGNER_SHOP: &str = "shop";
#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct EstateGrowth {
pub v: u32,
pub signer: String,
pub estate: String,
pub main_data_gib_per_member: u64,
pub until_unix_ms: i64,
pub reference: String,
pub issued_unix_ms: i64,
#[serde(default)]
pub signature: String,
}
#[cfg(feature = "metered")]
pub fn growth_message(g: &EstateGrowth) -> Vec<u8> {
let mut v = serde_json::to_value(g).expect("a growth go-ahead serializes");
v.as_object_mut().expect("a growth go-ahead is an object").remove("signature");
let mut s = String::new();
monetize_embed::signing::canonical_json(&v, &mut s);
s.into_bytes()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaidStep {
pub reference: String,
pub main_gib: u64,
}
#[cfg(feature = "metered")]
pub fn verify_step(go_ahead: &str, key: Option<&[u8; 32]>, estate: &str, twin: bool, data_gib: u64, now_unix_ms: i64) -> Result<PaidStep, Refusal> {
use ed25519_dalek::Verifier;
let bad = |m: String| refuse(RefusalCode::BadOrder, m);
let key = key.ok_or_else(|| bad("this box has no monetize key yet, so monetize's paid step cannot be verified: nothing moved".into()))?;
let key = ed25519_dalek::VerifyingKey::from_bytes(key).map_err(|e| bad(format!("this box's monetize key is not an Ed25519 point ({e}): nothing moved")))?;
let not_monetizes = |why: &str| bad(format!("the paid step does not verify under this box's monetize key ({why}): not monetize's, nothing moved"));
let g: EstateGrowth = serde_json::from_str(go_ahead).map_err(|_| not_monetizes("not an EstateGrowth"))?;
if g.v != 1 || g.signer != GROWTH_SIGNER_SHOP || g.estate.trim().is_empty() || g.main_data_gib_per_member == 0 {
return Err(not_monetizes("malformed"));
}
let sig = monetize_embed::signing::base64_decode(&g.signature).ok_or_else(|| not_monetizes("no signature"))?;
let sig = ed25519_dalek::Signature::from_slice(&sig).map_err(|_| not_monetizes("no signature"))?;
key.verify(&growth_message(&g), &sig).map_err(|_| not_monetizes("forged"))?;
if g.estate != estate {
return Err(bad(format!("the paid step is estate {}'s; this box pulls as {estate:?}: nothing moved", g.estate)));
}
if g.until_unix_ms <= now_unix_ms {
return Err(bad(format!("the paid step was good until {} (unix ms), its period is over: nothing moved", g.until_unix_ms)));
}
let main_gib = g.main_data_gib_per_member;
if !twin && data_gib != main_gib {
return Err(bad(format!("the order sizes main's disks to {data_gib} GiB, the paid step to {main_gib} GiB: nothing moved")));
}
if twin && data_gib < main_gib {
return Err(bad(format!("the order sizes the twin's disk to {data_gib} GiB, under main's {main_gib} GiB a disk: it must hold main's whole set, nothing moved")));
}
Ok(PaidStep { reference: g.reference, main_gib })
}
#[cfg(not(feature = "metered"))]
pub fn verify_step(go_ahead: &str, key: Option<&[u8; 32]>, estate: &str, twin: bool, data_gib: u64, now_unix_ms: i64) -> Result<PaidStep, Refusal> {
let _ = (go_ahead, key, estate, twin, now_unix_ms);
Err(refuse(RefusalCode::BadOrder, format!("this community build carries no monetize verifier, so monetize's signed grow step to {data_gib} GiB cannot be checked: refused")))
}
pub fn carried(status: &gunnar_proto::GrowStatusResponse, data_gib: u64) -> bool {
let target = data_gib.saturating_mul(SECTORS_PER_GIB);
status.members_verdict == "complete" && !status.members.is_empty() && status.members.iter().all(|m| crate::grow::target_reached(m.set_bytes / 512, target))
}
pub fn twin_holds(twin: &gunnar_proto::GrowStatusResponse, main_members: usize, main_gib: u64) -> Result<String, Refusal> {
let whole = (main_members as u64).max(1).saturating_mul(main_gib);
let no = |why: String| Err(refuse(RefusalCode::NoRoom, format!("main grows after its twin, and the twin {why}; main's set is {main_members} × {main_gib} GiB: nothing moved")));
if !matches!(twin.phase.as_str(), "idle" | "done" | "failed") {
return no(format!("is still growing ({}: {})", twin.phase, twin.detail));
}
if twin.members.is_empty() {
return no(format!("reports no data disk ({})", twin.members_verdict));
}
if twin.members_verdict != "complete" {
return no(format!("has an {} set", twin.members_verdict));
}
let short = twin.members.iter().map(|m| m.set_bytes >> 30).min().unwrap_or(0);
if !carried(twin, whole) {
return no(format!("holds {short} GiB a disk, under the {whole} GiB main's set needs"));
}
Ok(format!("holds {short} GiB a disk, main's whole set of {whole} GiB"))
}
pub fn state_of(status: &gunnar_proto::GrowStatusResponse) -> GrowState {
let said = if status.detail.is_empty() { status.phase.clone() } else { status.detail.clone() };
match status.phase.as_str() {
"waiting-members" => GrowState::WaitingMembers(said),
"done" => GrowState::Done(said),
"failed" => GrowState::Failed(said),
"idle" => GrowState::Failed(format!("the grow machine is idle: {said}")),
_ => GrowState::Running(said),
}
}
#[derive(Clone)]
pub struct PaidGrowth {
machine: Arc<GrowMachine>,
poll: Duration,
ceiling: Duration,
}
impl PaidGrowth {
pub fn new(machine: Arc<GrowMachine>) -> PaidGrowth {
PaidGrowth {
machine,
poll: Duration::from_secs(2),
ceiling: crate::grow::DEFAULT_DRAIN_TIMEOUT + crate::grow::DEFAULT_WAIT_MEMBERS_TIMEOUT + Duration::from_secs(300),
}
}
pub fn pacing(mut self, poll: Duration, ceiling: Duration) -> PaidGrowth {
self.poll = poll;
self.ceiling = ceiling;
self
}
pub fn check(&self, go_ahead: &str, data_gib: u64, estate: &str, key: Option<&[u8; 32]>, twin: bool, now_unix_ms: i64) -> Result<GrowCheck, Refusal> {
let step = verify_step(go_ahead, key, estate, twin, data_gib, now_unix_ms)?;
Ok(GrowCheck { twin, data_gib, reference: step.reference, carried: carried(&self.machine.status(), data_gib) })
}
pub fn members(&self) -> usize {
self.machine.status().members.len()
}
pub fn start(&self, check: &GrowCheck, nonce: &str, now_unix_ms: i64) -> Result<GrowState, Refusal> {
let target = check.data_gib.saturating_mul(SECTORS_PER_GIB);
let request = gunnar_proto::GrowStartRequest {
target_sectors_per_member: target,
go_ahead: crate::grow::GoAhead::operator(target, nonce, now_unix_ms),
drain_timeout_secs: 0,
wait_members_timeout_secs: 0,
};
let code = if self.machine.phase().is_active() { RefusalCode::AlreadyRunning } else { RefusalCode::BadOrder };
self.machine.start(&request).map(|s| state_of(&s)).map_err(|why| refuse(code, format!("the grow machine refused monetize's signed grow step: {why}")))
}
pub async fn settle(&self, past_waiting: bool) -> GrowState {
let until = tokio::time::Instant::now() + self.ceiling;
loop {
let state = state_of(&self.machine.status());
match &state {
GrowState::Done(_) | GrowState::Failed(_) => return state,
GrowState::WaitingMembers(_) if !past_waiting => return state,
_ if tokio::time::Instant::now() >= until => return state,
_ => tokio::time::sleep(self.poll).await,
}
}
}
pub fn look_again(&self, give_up: bool) -> Result<(), Refusal> {
self.machine.resume(give_up).map(|_| ()).map_err(|why| refuse(RefusalCode::BadOrder, format!("the grow machine would not look again: {why}")))
}
}
#[cfg(test)]
pub(crate) mod tests {
use super::*;
#[cfg(feature = "metered")]
use crate::grow::tests::{carrying, Fake, Script};
#[cfg(feature = "metered")]
use crate::grow::world::{Grown, SurveyFacts};
#[cfg(feature = "metered")]
use std::collections::VecDeque;
const GIB: u64 = 1 << 30;
fn status(phase: &str, verdict: &str, set_gib: &[u64]) -> gunnar_proto::GrowStatusResponse {
gunnar_proto::GrowStatusResponse {
phase: phase.into(),
members_verdict: verdict.into(),
detail: format!("{phase}: as scripted"),
members: set_gib.iter().enumerate().map(|(i, g)| gunnar_proto::GrowMember { index: i as u32, set_bytes: g * GIB, disk_bytes: g * GIB, ..Default::default() }).collect(),
..Default::default()
}
}
#[cfg(feature = "metered")]
pub(crate) mod signed {
use ed25519_dalek::Signer;
use super::super::{growth_message, EstateGrowth, GROWTH_SIGNER_SHOP};
use monetize_embed::signing::base64_encode;
pub const SHOP: [u8; 32] = [11u8; 32];
pub fn shop_pub() -> [u8; 32] {
ed25519_dalek::SigningKey::from_bytes(&SHOP).verifying_key().to_bytes()
}
pub fn step(seed: [u8; 32], estate: &str, gib: u64, until: i64) -> String {
let mut g = EstateGrowth {
v: 1,
signer: GROWTH_SIGNER_SHOP.into(),
estate: estate.into(),
main_data_gib_per_member: gib,
until_unix_ms: until,
reference: "estate/acme/change-2".into(),
issued_unix_ms: 1_000,
signature: String::new(),
};
g.signature = base64_encode(&ed25519_dalek::SigningKey::from_bytes(&seed).sign(&growth_message(&g)).to_bytes());
serde_json::to_string(&g).unwrap()
}
}
#[cfg(feature = "metered")]
#[test]
fn a_paid_step_verifies_under_the_monetize_key_for_this_estate_until_its_period_ends() {
use signed::*;
let key = shop_pub();
let go = step(SHOP, "acme", 120, 50_000);
let s = verify_step(&go, Some(&key), "acme", false, 120, 10_000).expect("main, its step");
assert_eq!(s, PaidStep { reference: "estate/acme/change-2".into(), main_gib: 120 });
verify_step(&go, Some(&key), "acme", true, 480, 10_000).expect("the twin, main's whole set");
let no = |r: Result<PaidStep, Refusal>, says: &str| {
let r = r.expect_err(says);
assert_eq!(r.code, RefusalCode::BadOrder, "{r}");
assert!(r.message.contains(says), "{says:?} in {r}");
};
no(verify_step(&step([12u8; 32], "acme", 120, 50_000), Some(&key), "acme", false, 120, 10_000), "not monetize's");
no(verify_step(&step(SHOP, "globex", 120, 50_000), Some(&key), "acme", false, 120, 10_000), "globex");
no(verify_step(&go, Some(&key), "acme", false, 120, 50_001), "until");
no(verify_step(&go, Some(&key), "acme", false, 240, 10_000), "120");
no(verify_step(&go, Some(&key), "acme", true, 60, 10_000), "whole set");
no(verify_step(&go, None, "acme", false, 120, 10_000), "no monetize key");
no(verify_step("{not json", Some(&key), "acme", false, 120, 10_000), "not monetize's");
}
#[cfg(feature = "metered")]
#[test]
fn the_paid_steps_signed_bytes_are_monetizes_canonical_form() {
let g = EstateGrowth { v: 1, signer: "shop".into(), estate: "acme".into(), main_data_gib_per_member: 120, until_unix_ms: 9_000, reference: "estate/acme/change-2".into(), issued_unix_ms: 7, signature: "x".into() };
assert_eq!(
String::from_utf8(growth_message(&g)).unwrap(),
r#"{"estate":"acme","issued_unix_ms":7,"main_data_gib_per_member":120,"reference":"estate/acme/change-2","signer":"shop","until_unix_ms":9000,"v":1}"#
);
}
#[cfg(not(feature = "metered"))]
#[test]
fn a_community_build_refuses_a_paid_step_by_name() {
let r = verify_step("{}", Some(&[1u8; 32]), "acme", false, 120, 0).unwrap_err();
assert_eq!(r.code, RefusalCode::BadOrder);
assert!(r.message.contains("community build"), "{r}");
}
#[test]
fn a_step_is_carried_only_when_every_member_of_a_complete_set_spans_it() {
assert!(carried(&status("idle", "complete", &[120, 120]), 120));
let mut hair = status("idle", "complete", &[120, 120]);
hair.members[1].set_bytes -= 67 * 512;
assert!(carried(&hair, 120), "a partition start under is carried");
assert!(!carried(&status("idle", "complete", &[120, 60]), 120));
assert!(!carried(&status("idle", "complete", &[120, 120]), 240));
assert!(!carried(&status("idle", "incomplete: member 1 of 2 is missing", &[120, 120]), 120));
assert!(!carried(&status("idle", "none", &[]), 120), "no members, no claim");
}
#[test]
fn the_twin_holds_mains_set_only_once_its_own_disks_span_it() {
let ok = twin_holds(&status("done", "complete", &[480]), 4, 120).expect("480 GiB holds 4 × 120");
assert!(ok.contains("480"), "{ok}");
twin_holds(&status("idle", "complete", &[480]), 4, 120).expect("an idle twin at size");
for (twin, says) in [
(status("done", "complete", &[240]), "240"),
(status("waiting-members", "complete", &[480]), "waiting-members"),
(status("idle", "none", &[]), "no data disk"),
(status("idle", "incomplete: member 1 of 2 is missing", &[480]), "incomplete"),
] {
let r = twin_holds(&twin, 4, 120).expect_err(says);
assert_eq!(r.code, RefusalCode::NoRoom, "{r}");
assert!(r.message.contains(says) && r.message.contains("twin"), "{says:?} in {r}");
}
}
#[test]
fn the_machines_phase_reads_as_the_pulled_growth_needs_it() {
assert_eq!(state_of(&status("draining", "", &[])), GrowState::Running("draining: as scripted".into()));
assert_eq!(state_of(&status("waiting-members", "", &[])), GrowState::WaitingMembers("waiting-members: as scripted".into()));
assert_eq!(state_of(&status("done", "", &[])), GrowState::Done("done: as scripted".into()));
assert_eq!(state_of(&status("failed", "", &[])), GrowState::Failed("failed: as scripted".into()));
assert!(matches!(state_of(&status("idle", "", &[])), GrowState::Failed(d) if d.contains("idle")), "a machine that stopped moving is not running");
}
#[cfg(feature = "metered")]
fn larger_to(sectors: u64) -> SurveyFacts {
let mut s = carrying(sectors);
for m in &mut s.members {
m.larger = true;
}
s.eligible = true;
s.target_sectors = Some(sectors);
s
}
#[cfg(feature = "metered")]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn the_paid_step_drives_this_boxs_own_grow_machine_to_done() {
use signed::*;
let script = Script {
in_flight: VecDeque::from([0]),
surveys: VecDeque::from([carrying(60 * SECTORS_PER_GIB)]),
grown: VecDeque::from([Ok(Grown { grew: true, array_bytes: 480 * GIB, refusal: None, xfs: "grown".into() })]),
..Script::default()
};
let world = Fake::new(script);
let g = PaidGrowth::new(GrowMachine::new(world.clone())).pacing(Duration::from_millis(20), Duration::from_secs(20));
assert_eq!(g.members(), 4);
let now = crate::twin::now_ms();
let go = step(SHOP, "acme", 120, now + 60_000);
let c = g.check(&go, 120, "acme", Some(&shop_pub()), false, now).expect("verified");
assert_eq!(c, GrowCheck { twin: false, data_gib: 120, reference: "estate/acme/change-2".into(), carried: false });
assert!(matches!(g.start(&c, "pull-6-estate/acme/change-2", now).expect("started"), GrowState::Running(_)));
assert!(matches!(g.settle(false).await, GrowState::WaitingMembers(_)), "/data is off, the members are awaited");
assert!(g.start(&c, "pull-7-estate/acme/change-2", now).is_err(), "one growth at a time");
world.script.lock().unwrap().surveys = VecDeque::from([larger_to(120 * SECTORS_PER_GIB)]);
g.look_again(false).expect("resume applies while waiting");
let end = g.settle(true).await;
assert!(matches!(&end, GrowState::Done(d) if d.starts_with("done")), "{end:?}");
let c = g.check(&go, 120, "acme", Some(&shop_pub()), false, now).expect("verified");
assert!(c.carried, "{c:?}");
}
}