//! `spectra admin`: the ledger's own checks (spec §9.5, §19). use super::{ui, Ctx}; use anyhow::{Context, Result}; use spectra_core::ledger::Ledger; use spectra_core::principals::Principals; /// `spectra admin verify`: recompute the chain, check every checkpoint and /// proof; exit 1 when anything is wrong. pub async fn verify(ctx: &Ctx, json: bool) -> Result { let spin = if json { None } else { Some(ui::spinner("verifying the ledger")) }; // On a client the server verifies its own ledger (spec §8.6). let v: serde_json::Value = match ctx.remote()? { Some(_) => { let home = super::identity::owner_home(ctx, None); let c = ctx.owner_client(&home).await?.context("no server")?; super::call(&c, "GET", "/api/v1/admin/verify", None).await? } None => serde_json::to_value(Ledger::open_read_only(&ctx.db()?).await?.verify(&mut Principals::default()).await?)?, }; if let Some(s) = spin { s.clear(); } let findings = v["findings"].as_array().cloned().unwrap_or_default(); let clean = findings.is_empty(); if json { println!("{v}"); return Ok(if clean { 0 } else { 1 }); } if clean { println!("{} {}", ui::done("Verified"), ui::subject("the ledger")); } else { println!( "{} {} {}", ui::err(ui::glyph("✘", "x")), ui::err("The ledger does not verify:"), ui::err(format!("{} finding(s)", findings.len())) ); } println!("{}", ui::kv("events", v["events"].as_u64().unwrap_or(0))); let last = v["last_checkpoint"].as_u64().map(|s| format!("last at #{s}")).unwrap_or_else(|| "none yet".into()); println!("{}", ui::kv("signed", format!("{} checkpoint(s){}{last}", v["checkpoints"].as_u64().unwrap_or(0), ui::sep()))); let uncovered = v["uncovered"].as_u64().unwrap_or(0); if uncovered > 0 { println!("{}", ui::kv("pending", ui::dim(format!("{uncovered} event(s) since, chained and awaiting the next checkpoint")))); } println!("{}", ui::kv("head", ui::hash(v["head"].as_str().unwrap_or("")))); for f in &findings { // As the problem is named in code: hash_mismatch is HashMismatch. let problem = match &f["problem"] { serde_json::Value::String(p) => p .split('_') .map(|w| { let mut c = w.chars(); c.next().map(|f| f.to_uppercase().chain(c).collect::()).unwrap_or_default() }) .collect(), other => other.to_string(), }; println!( " {} {} {}", ui::id(format!("#{}", f["seq"].as_u64().unwrap_or(0))), ui::err(problem), f["detail"].as_str().unwrap_or("") ); } Ok(if clean { 0 } else { 1 }) } /// `spectra admin check`: do the projections equal a rebuild? pub async fn check(ctx: &Ctx) -> Result { ctx.local_only("spectra admin check")?; let ledger = ctx.writer().await?; let differ = ledger.check().await?; if differ.is_empty() { println!("{} {}", ui::done("Checked"), ui::subject("every projection matches the ledger")); Ok(0) } else { println!("{} projections differ from the ledger: {}", ui::caution(), ui::err(differ.join(", "))); println!(" {} {}", ui::note(), ui::dim("rebuild them with spectra admin rebuild")); Ok(1) } } /// `spectra admin rebuild`: replay the ledger into every projection. pub async fn rebuild(ctx: &Ctx) -> Result { ctx.local_only("spectra admin rebuild")?; let ledger = ctx.writer().await?; let spin = ui::spinner("replaying the ledger"); ledger.rebuild().await?; spin.done(&format!("{} {}", ui::done("Rebuilt"), ui::subject(format!("projections from {} events", ledger.head().await.0)))); Ok(0) }