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 super::super::{growth_message, EstateGrowth, GROWTH_SIGNER_SHOP};
use ed25519_dalek::Signer;
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:?}");
}
}