1use crate::platform::Platform;
56use choir_hash::ContentHash;
57use choir_identity::{sync, ActorKey, Registry};
58use choir_sequencer::Sequenced;
59use choir_view::{OpKind, ViewOp};
60use std::io::Write as _;
61use std::path::{Path, PathBuf};
62use std::sync::{Arc, Mutex};
63
64pub const INTERVAL_SECS: u64 = 5;
70
71pub const WITNESS_HISTORY: usize = 64;
73
74pub const WITNESS_CHANNEL: &str = "seed/witness";
81
82const MAX_PAGES_PER_ROUND: usize = 64;
88
89#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Home {
92 pub url: String,
95 pub node_id: ContentHash,
98}
99
100#[derive(Clone)]
106pub struct Credential {
107 user: String,
108 token: String,
109}
110
111impl std::fmt::Debug for Credential {
112 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113 write!(f, "Credential({}:<redacted>)", self.user)
114 }
115}
116
117impl Credential {
118 pub fn read(path: &Path) -> Result<Self, String> {
126 let text = std::fs::read_to_string(path)
127 .map_err(|e| format!("reading the seed credential {}: {e}", path.display()))?;
128 let line = text
129 .lines()
130 .map(str::trim)
131 .find(|line| !line.is_empty() && !line.starts_with('#'))
132 .ok_or_else(|| format!("{} holds no `user:token` line", path.display()))?;
133 Self::parse(line).map_err(|e| format!("{}: {e}", path.display()))
134 }
135
136 pub fn parse(line: &str) -> Result<Self, String> {
142 let (user, token) = line
143 .split_once(':')
144 .ok_or("a seed credential is one `user:token` line")?;
145 if user.is_empty() || token.is_empty() {
146 return Err("a seed credential needs both a user and a token".to_string());
147 }
148 if line
149 .chars()
150 .any(|c| c == '"' || c == '\\' || c.is_control())
151 {
152 return Err(
153 "a seed credential may not carry quotes, backslashes or control \
154 characters"
155 .to_string(),
156 );
157 }
158 Ok(Self {
159 user: user.to_string(),
160 token: token.to_string(),
161 })
162 }
163
164 #[must_use]
166 pub fn user(&self) -> &str {
167 &self.user
168 }
169
170 fn basic(&self) -> String {
172 format!(
173 "Basic {}",
174 crate::base64_encode(format!("{}:{}", self.user, self.token).as_bytes())
175 )
176 }
177}
178
179fn get(base: &str, credential: &Credential, path: &str) -> Result<(u16, String), String> {
184 let mut child = std::process::Command::new("curl")
185 .args(["-sS", "-w", "\n%{http_code}", "--config", "-"])
186 .arg(format!("{base}{path}"))
187 .stdin(std::process::Stdio::piped())
188 .stdout(std::process::Stdio::piped())
189 .stderr(std::process::Stdio::piped())
190 .spawn()
191 .map_err(|e| format!("could not start curl: {e}"))?;
192 child
193 .stdin
194 .take()
195 .expect("piped curl stdin")
196 .write_all(format!("user = \"{}:{}\"\n", credential.user, credential.token).as_bytes())
197 .map_err(|e| format!("could not configure curl: {e}"))?;
198 let out = child
199 .wait_with_output()
200 .map_err(|e| format!("could not wait for curl: {e}"))?;
201 if !out.status.success() {
202 return Err(format!(
203 "could not reach the home: {}",
204 String::from_utf8_lossy(&out.stderr).trim()
205 ));
206 }
207 let text = String::from_utf8_lossy(&out.stdout);
208 let (body, status) = text
209 .rsplit_once('\n')
210 .ok_or("curl returned no HTTP status")?;
211 let status = status
212 .trim()
213 .parse()
214 .map_err(|_| "curl returned an invalid HTTP status".to_string())?;
215 Ok((status, body.to_string()))
216}
217
218#[derive(Debug, Clone)]
220pub struct Signers {
221 pub node_id: ContentHash,
223 pub keys: Vec<[u8; 32]>,
225}
226
227impl Signers {
228 fn registry(&self) -> Registry {
230 let mut registry = Registry::new();
231 for key in &self.keys {
232 let _ = registry.register(key);
236 }
237 registry
238 }
239}
240
241pub fn fetch_signers(base: &str, credential: &Credential) -> Result<Signers, String> {
249 let (status, body) = get(base, credential, "/api/signers")?;
250 if status != 200 {
251 return Err(format!(
252 "GET /api/signers answered {status}; a seed needs the home's `@node auditor` \
253 grant for its principal `{}`: {}",
254 credential.user,
255 body.trim()
256 ));
257 }
258 let value: serde_json::Value = serde_json::from_str(&body)
259 .map_err(|e| format!("GET /api/signers served a body that is not JSON: {e}"))?;
260 if value["format_version"].as_u64() != Some(1) {
261 return Err(format!(
262 "GET /api/signers is format_version {}, and this build reads 1",
263 value["format_version"]
264 ));
265 }
266 let key = |row: &serde_json::Value| -> Result<(ContentHash, [u8; 32]), String> {
267 let id = row["actor_id"]
268 .as_str()
269 .and_then(ContentHash::from_hex)
270 .ok_or("a signer row names no actor id")?;
271 let bytes: [u8; 32] = row["public_key_hex"]
272 .as_str()
273 .and_then(crate::platform::hex_decode)
274 .and_then(|bytes| bytes.try_into().ok())
275 .ok_or("a signer row carries no 32-byte public key")?;
276 if ContentHash::blake3(&bytes) != id {
277 return Err(format!(
278 "signer {} is listed beside a key that does not hash to it",
279 id.to_hex()
280 ));
281 }
282 Ok((id, bytes))
283 };
284 let (node_id, node_key) = key(&value["node"])?;
285 let mut keys = vec![node_key];
286 for row in value["signers"].as_array().into_iter().flatten() {
287 keys.push(key(row)?.1);
288 }
289 Ok(Signers { node_id, keys })
290}
291
292fn pin_path(root: &Path) -> PathBuf {
294 root.join(".choir").join("home.fingerprint")
295}
296
297pub fn pin_home(root: &Path, url: &str, node_id: &ContentHash) -> Result<(), String> {
304 let path = pin_path(root);
305 let fingerprint = node_id.to_hex();
306 match std::fs::read_to_string(&path) {
307 Ok(pinned) if pinned.trim() == fingerprint => Ok(()),
308 Ok(pinned) => Err(format!(
309 "home identity changed: {} pins {}, but {url} now signs as {fingerprint}. A seed \
310 does not follow its home to a new key; if the home really was re-keyed, delete \
311 the pin and accept that this copy now claims a different home",
312 path.display(),
313 pinned.trim()
314 )),
315 Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
316 std::fs::create_dir_all(root.join(".choir"))
317 .map_err(|e| format!("creating {}: {e}", root.join(".choir").display()))?;
318 choir_fs::write_atomic(&path, format!("{fingerprint}\n"))
320 .map_err(|e| format!("writing {}: {e}", path.display()))
321 }
322 Err(e) => Err(format!("reading {}: {e}", path.display())),
323 }
324}
325
326pub fn contact(root: &Path, url: &str, credential: &Credential) -> Result<Home, String> {
334 let url = url.trim_end_matches('/').to_string();
335 match fetch_signers(&url, credential) {
336 Ok(signers) => {
337 pin_home(root, &url, &signers.node_id)?;
338 Ok(Home {
339 url,
340 node_id: signers.node_id,
341 })
342 }
343 Err(unreachable) => match pinned(root) {
348 Some(node_id) => Ok(Home { url, node_id }),
349 None => Err(format!(
350 "{unreachable}; a seed's first start needs its home, to pin the key it \
351 signs with"
352 )),
353 },
354 }
355}
356
357#[must_use]
359pub fn pinned(root: &Path) -> Option<ContentHash> {
360 std::fs::read_to_string(pin_path(root))
361 .ok()
362 .and_then(|text| ContentHash::from_hex(text.trim()))
363}
364
365#[derive(Debug, Clone, PartialEq, Eq)]
372pub struct WitnessStatement {
373 pub format_version: u16,
375 pub witness: String,
377 pub home_node_id: String,
379 pub snapshot: String,
381 pub at_seq: u64,
383 pub seen_at_seq: u64,
385}
386
387fn quoted(text: &str) -> String {
389 serde_json::Value::String(text.to_string()).to_string()
390}
391
392impl WitnessStatement {
393 #[must_use]
401 pub fn canonical_bytes(&self) -> Vec<u8> {
402 format!(
403 "{{\"format_version\":{},\"witness\":{},\"home_node_id\":{},\"snapshot\":{},\"at_seq\":{},\"seen_at_seq\":{}}}",
404 self.format_version,
405 quoted(&self.witness),
406 quoted(&self.home_node_id),
407 quoted(&self.snapshot),
408 self.at_seq,
409 self.seen_at_seq
410 )
411 .into_bytes()
412 }
413
414 #[must_use]
416 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
417 let text = |key: &str| value.get(key)?.as_str().map(str::to_string);
418 Some(Self {
419 format_version: u16::try_from(value.get("format_version")?.as_u64()?).ok()?,
420 witness: text("witness")?,
421 home_node_id: text("home_node_id")?,
422 snapshot: text("snapshot")?,
423 at_seq: value.get("at_seq")?.as_u64()?,
424 seen_at_seq: value.get("seen_at_seq")?.as_u64()?,
425 })
426 }
427
428 #[must_use]
430 pub fn to_json(&self) -> serde_json::Value {
431 serde_json::from_slice(&self.canonical_bytes()).expect("canonical bytes are JSON")
432 }
433}
434
435#[derive(Debug, Clone, PartialEq, Eq)]
441pub struct SignedStatement {
442 pub statement: WitnessStatement,
444 pub public_key: [u8; 32],
446 pub signature: Vec<u8>,
449}
450
451impl SignedStatement {
452 #[must_use]
454 pub fn sign(key: &ActorKey, statement: WitnessStatement) -> Self {
455 let witness = key.sign_submission(WITNESS_CHANNEL, &statement.canonical_bytes());
456 Self {
457 statement,
458 public_key: key.public_key_bytes(),
459 signature: witness.signature,
460 }
461 }
462
463 pub fn verify(&self) -> Result<(), String> {
470 if self.statement.format_version != 1 {
471 return Err(format!(
472 "statement is format_version {}, and this build reads 1",
473 self.statement.format_version
474 ));
475 }
476 let id = ContentHash::blake3(&self.public_key).to_hex();
477 if id != self.statement.witness {
478 return Err(format!(
479 "the statement names witness {}, and the key it carries is {id}",
480 self.statement.witness
481 ));
482 }
483 let mut registry = Registry::new();
484 registry
485 .register(&self.public_key)
486 .map_err(|e| format!("the statement's key is not an ed25519 key: {e:?}"))?;
487 registry
488 .verify_submission(
489 WITNESS_CHANNEL,
490 &self.statement.canonical_bytes(),
491 &choir_oplog::Witness::ed25519(id, self.signature.clone()),
492 )
493 .map(|_| ())
494 .map_err(|e| format!("the statement's signature does not verify: {e:?}"))
495 }
496
497 #[must_use]
499 pub fn to_json(&self) -> serde_json::Value {
500 serde_json::json!({
501 "statement": self.statement.to_json(),
502 "public_key_hex": crate::platform::hex_encode(&self.public_key),
503 "signature_hex": crate::platform::hex_encode(&self.signature),
504 })
505 }
506
507 #[must_use]
510 pub fn from_json(value: &serde_json::Value) -> Option<Self> {
511 Some(Self {
512 statement: WitnessStatement::from_json(value.get("statement")?)?,
513 public_key: value
514 .get("public_key_hex")?
515 .as_str()
516 .and_then(crate::platform::hex_decode)?
517 .try_into()
518 .ok()?,
519 signature: value
520 .get("signature_hex")?
521 .as_str()
522 .and_then(crate::platform::hex_decode)?,
523 })
524 }
525}
526
527#[derive(Debug, Default, Clone)]
533pub struct SnapshotChain {
534 prev: std::collections::BTreeMap<String, Option<String>>,
535}
536
537#[derive(Debug, Clone, Copy, PartialEq, Eq)]
539pub enum Relation {
540 Same,
542 Behind(u64),
544 Ahead(u64),
546 Fork,
548}
549
550impl SnapshotChain {
551 pub fn read_page(&mut self, entries: &[serde_json::Value]) -> usize {
554 let mut found = 0;
555 for entry in entries {
556 let Some(payload) = entry["payload_hex"]
557 .as_str()
558 .and_then(crate::platform::hex_decode)
559 else {
560 continue;
561 };
562 if let Ok(ViewOp {
563 kind: OpKind::RecordRefSnapshot { snapshot },
564 ..
565 }) = ViewOp::from_payload(&payload)
566 {
567 self.insert(
568 snapshot.id().to_hex(),
569 snapshot.prev_snapshot.as_ref().map(ContentHash::to_hex),
570 );
571 found += 1;
572 }
573 }
574 found
575 }
576
577 pub fn insert(&mut self, id: String, prev: Option<String>) {
579 self.prev.insert(id, prev);
580 }
581
582 #[must_use]
585 pub fn steps_back(&self, from: &str, to: &str) -> Option<u64> {
586 let mut here = from;
587 let mut steps = 0u64;
588 loop {
589 if here == to {
590 return Some(steps);
591 }
592 here = self.prev.get(here)?.as_deref()?;
593 steps += 1;
594 if steps > self.prev.len() as u64 {
597 return None;
598 }
599 }
600 }
601
602 #[must_use]
611 pub fn relate(&self, a: &str, b: &str) -> Relation {
612 if a == b {
613 return Relation::Same;
614 }
615 if let Some(steps) = self.steps_back(b, a) {
616 return Relation::Behind(steps);
617 }
618 if let Some(steps) = self.steps_back(a, b) {
619 return Relation::Ahead(steps);
620 }
621 Relation::Fork
622 }
623}
624
625#[derive(Debug, Clone, Default, PartialEq, Eq)]
627pub struct Status {
628 pub head_seq: Option<u64>,
630 pub home_head_seq: Option<u64>,
633 pub refs_verified: usize,
636 pub refs_pending: usize,
639 pub unverified_entries: u64,
642 pub gap: bool,
646 pub halted: Option<(u64, String)>,
648 pub last_error: Option<String>,
650}
651
652impl Status {
653 #[must_use]
656 pub fn to_json(&self, home: &Home) -> serde_json::Value {
657 serde_json::json!({
658 "format_version": 1,
659 "home": home.url,
660 "home_node_id": home.node_id.to_hex(),
661 "head_seq": self.head_seq,
662 "home_head_seq": self.home_head_seq,
663 "refs_verified": self.refs_verified,
664 "refs_pending": self.refs_pending,
665 "unverified_entries": self.unverified_entries,
666 "gap": self.gap,
667 "halted": self.halted.as_ref().map(|(seq, reason)| serde_json::json!({
668 "seq": seq,
669 "reason": reason,
670 })),
671 "last_error": self.last_error,
672 })
673 }
674}
675
676#[derive(Debug, Clone, Default, PartialEq, Eq)]
678pub struct Progress {
679 pub appended: u64,
681 pub status: Status,
683}
684
685#[derive(Debug, Clone, PartialEq, Eq)]
687pub enum ReplicaError {
688 Unreachable(String),
691 Halted {
695 seq: u64,
697 reason: String,
699 },
700 Gap {
703 window_base: u64,
705 },
706}
707
708impl ReplicaError {
709 #[must_use]
712 pub fn is_halt(&self) -> bool {
713 !matches!(self, Self::Unreachable(_))
714 }
715}
716
717impl std::fmt::Display for ReplicaError {
718 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
719 match self {
720 Self::Unreachable(why) => write!(f, "replication round failed: {why}"),
721 Self::Halted { seq, reason } => {
722 write!(f, "replication halted at seq {seq}: {reason}")
723 }
724 Self::Gap { window_base } => write!(
725 f,
726 "replication stopped at a gap: the home has evicted everything before seq \
727 {window_base} and keeps no persisted log, so this copy cannot continue its \
728 chain"
729 ),
730 }
731 }
732}
733
734#[derive(Debug, Default)]
736struct Page {
737 served: usize,
739 appended: u64,
741}
742
743#[derive(Debug, Default)]
746pub struct Shared {
747 pub(crate) status: Mutex<Status>,
748 pub(crate) statements: Mutex<std::collections::VecDeque<SignedStatement>>,
751}
752
753impl Shared {
754 pub(crate) fn witness_json(&self, home: &Home, key: &ActorKey) -> serde_json::Value {
756 let statements = self.statements.lock().expect("replica statements lock");
757 let status = self.status.lock().expect("replica status lock");
758 serde_json::json!({
759 "format_version": 1,
760 "witness": key.actor_id().to_hex(),
761 "public_key_hex": crate::platform::hex_encode(&key.public_key_bytes()),
762 "home": home.url,
763 "home_node_id": home.node_id.to_hex(),
764 "gap": status.gap,
765 "halted": status.halted.as_ref().map(|(seq, reason)| serde_json::json!({
768 "seq": seq,
769 "reason": reason,
770 })),
771 "latest": statements.back().map(SignedStatement::to_json),
772 "history": statements.iter().map(SignedStatement::to_json).collect::<Vec<_>>(),
773 })
774 }
775}
776
777fn witness_path(root: &Path) -> PathBuf {
780 root.join(".choir").join("witness.jsonl")
781}
782
783fn load_statements(root: &Path) -> std::collections::VecDeque<SignedStatement> {
787 let text = std::fs::read_to_string(witness_path(root)).unwrap_or_default();
788 let mut out: std::collections::VecDeque<SignedStatement> = text
789 .lines()
790 .filter_map(|line| serde_json::from_str(line).ok())
791 .filter_map(|value: serde_json::Value| SignedStatement::from_json(&value))
792 .collect();
793 while out.len() > WITNESS_HISTORY {
794 out.pop_front();
795 }
796 out
797}
798
799pub struct Replica {
802 home: Home,
803 credential: Credential,
804 root: PathBuf,
805 platform: Arc<Platform>,
806 registry: Mutex<Registry>,
808 shared: Arc<Shared>,
809}
810
811impl Replica {
812 #[must_use]
820 pub fn new(root: PathBuf, home: Home, credential: Credential, platform: Arc<Platform>) -> Self {
821 let shared = Arc::new(Shared {
822 status: Mutex::new(Status {
823 head_seq: platform.view_seq().checked_sub(1),
824 ..Status::default()
825 }),
826 statements: Mutex::new(load_statements(&root)),
827 });
828 platform.attach_replica(home.clone(), shared.clone());
829 Self {
830 home,
831 credential,
832 root,
833 platform,
834 registry: Mutex::new(Registry::new()),
835 shared,
836 }
837 }
838
839 #[must_use]
841 pub fn home(&self) -> &Home {
842 &self.home
843 }
844
845 #[must_use]
847 pub fn status(&self) -> Status {
848 self.shared
849 .status
850 .lock()
851 .expect("replica status lock")
852 .clone()
853 }
854
855 pub fn fetch_signers(&self) -> Result<usize, ReplicaError> {
863 let signers =
864 fetch_signers(&self.home.url, &self.credential).map_err(ReplicaError::Unreachable)?;
865 if signers.node_id != self.home.node_id {
866 return Err(ReplicaError::Halted {
867 seq: self.platform.view_seq(),
868 reason: format!(
869 "the home now signs as {}, and this seed pinned {}",
870 signers.node_id.to_hex(),
871 self.home.node_id.to_hex()
872 ),
873 });
874 }
875 let count = signers.keys.len();
876 *self.registry.lock().expect("replica registry lock") = signers.registry();
877 Ok(count)
878 }
879
880 fn revocations(&self, entries: &[serde_json::Value]) -> sync::Revocations {
888 let mut revoked: sync::Revocations = self.platform.with_view(|view| {
889 view.bindings
890 .iter()
891 .filter_map(|(key, binding)| binding.revoked.as_ref().map(|r| (key.clone(), r.at)))
892 .collect()
893 });
894 for entry in entries.iter().filter_map(sync::rebuild) {
895 if let Ok(ViewOp {
896 kind: OpKind::RevokeKey { key, .. },
897 ..
898 }) = ViewOp::from_payload(&entry.payload)
899 {
900 revoked.entry(key.to_hex()).or_insert(entry.seq);
901 }
902 }
903 revoked
904 }
905
906 fn fetch_page(&self, from: u64) -> Result<Page, ReplicaError> {
909 let (status, body) = get(
910 &self.home.url,
911 &self.credential,
912 &format!("/api/log?from={from}"),
913 )
914 .map_err(ReplicaError::Unreachable)?;
915 let value: serde_json::Value = serde_json::from_str(&body).map_err(|_| {
916 ReplicaError::Unreachable(format!(
917 "GET /api/log answered {status} with a body that is not JSON"
918 ))
919 })?;
920 if status == 409 && value["code"] == "log_evicted" {
921 return Err(ReplicaError::Gap {
922 window_base: value["window_base"].as_u64().unwrap_or_default(),
923 });
924 }
925 if status != 200 {
926 return Err(ReplicaError::Unreachable(format!(
927 "GET /api/log answered {status}: {}",
928 value["error"].as_str().unwrap_or_default()
929 )));
930 }
931 let entries = value["entries"].as_array().cloned().unwrap_or_default();
932 let Some(last) = entries.last() else {
933 return Ok(Page::default());
934 };
935 let last_seq = last["seq"].as_u64();
936 {
940 let mut status = self.shared.status.lock().expect("replica status lock");
941 status.home_head_seq = status.home_head_seq.max(last_seq);
942 }
943
944 let registry = self.registry.lock().expect("replica registry lock");
945 let revoked = self.revocations(&entries);
946 let report = sync::page(&entries, ®istry, &revoked);
947 let keep = match report.first_failure {
949 Some(seq) => entries
950 .iter()
951 .position(|entry| entry["seq"].as_u64() == Some(seq))
952 .unwrap_or(0),
953 None => entries.len(),
954 };
955 drop(registry);
956
957 let mut batch = Vec::with_capacity(keep);
958 let mut attests = Vec::with_capacity(keep);
961 for entry in &entries[..keep] {
962 let (Some(rebuilt), Some(hash)) = (
965 sync::rebuild(entry),
966 entry["hash"].as_str().and_then(ContentHash::from_hex),
967 ) else {
968 return Err(ReplicaError::Halted {
969 seq: entry["seq"].as_u64().unwrap_or(from),
970 reason: "an entry that verified could not be rebuilt".to_string(),
971 });
972 };
973 attests.push(matches!(
974 ViewOp::from_payload(&rebuilt.payload),
975 Ok(ViewOp {
976 kind: OpKind::RecordRefSnapshot { .. },
977 ..
978 })
979 ));
980 batch.push(Sequenced {
981 entry: rebuilt,
982 hash,
983 });
984 }
985 let (appended, stopped) = if batch.is_empty() {
986 (0, None)
987 } else {
988 match self.platform.replicate(batch) {
989 Ok(done) => (done.appended, None),
990 Err(stopped) => (stopped.appended, Some(stopped)),
991 }
992 };
993 let taken = usize::try_from(appended)
996 .unwrap_or(usize::MAX)
997 .min(entries.len());
998 if attests[..taken.min(attests.len())].contains(&true) {
999 self.witness();
1000 }
1001 let unverified = if taken == entries.len() {
1002 report.unverified
1003 } else {
1004 let registry = self.registry.lock().expect("replica registry lock");
1005 sync::page(&entries[..taken], ®istry, &revoked).unverified
1006 };
1007 self.shared
1008 .status
1009 .lock()
1010 .expect("replica status lock")
1011 .unverified_entries += unverified as u64;
1012 if let Some(stopped) = stopped {
1013 return Err(ReplicaError::Halted {
1014 seq: stopped.seq,
1015 reason: stopped.reason,
1016 });
1017 }
1018 if let Some(seq) = report.first_failure {
1019 return Err(ReplicaError::Halted {
1020 seq,
1021 reason: report.failures.first().map_or_else(
1024 || "the page failed verification".to_string(),
1025 |first| {
1026 first
1027 .strip_prefix(&format!("seq {seq}: "))
1028 .unwrap_or(first)
1029 .to_string()
1030 },
1031 ),
1032 });
1033 }
1034 Ok(Page {
1035 served: entries.len(),
1036 appended,
1037 })
1038 }
1039
1040 fn witness(&self) {
1046 if self.shared.status.lock().expect("replica status lock").gap {
1047 return;
1048 }
1049 let Some((snapshot, at_seq)) = self.platform.with_view(|view| {
1050 view.latest_snapshot
1051 .as_ref()
1052 .map(|latest| (latest.id().to_hex(), latest.at_seq))
1053 }) else {
1054 return;
1055 };
1056 let key = self.platform.node_key();
1057 let signed = SignedStatement::sign(
1058 key,
1059 WitnessStatement {
1060 format_version: 1,
1061 witness: key.actor_id().to_hex(),
1062 home_node_id: self.home.node_id.to_hex(),
1063 snapshot,
1064 at_seq,
1065 seen_at_seq: self.platform.view_seq().saturating_sub(1),
1066 },
1067 );
1068 let line = format!("{}\n", signed.to_json());
1069 let written = std::fs::OpenOptions::new()
1070 .create(true)
1071 .append(true)
1072 .open(witness_path(&self.root))
1073 .and_then(|mut file| file.write_all(line.as_bytes()));
1074 if let Err(e) = written {
1075 self.shared
1077 .status
1078 .lock()
1079 .expect("replica status lock")
1080 .last_error = Some(format!("recording a witness statement: {e}"));
1081 }
1082 let mut statements = self
1083 .shared
1084 .statements
1085 .lock()
1086 .expect("replica statements lock");
1087 statements.push_back(signed);
1088 while statements.len() > WITNESS_HISTORY {
1089 statements.pop_front();
1090 }
1091 }
1092
1093 #[must_use]
1095 pub fn statements(&self) -> Vec<SignedStatement> {
1096 self.shared
1097 .statements
1098 .lock()
1099 .expect("replica statements lock")
1100 .iter()
1101 .cloned()
1102 .collect()
1103 }
1104
1105 pub fn fetch_objects(&self, repo: &str) -> Result<crate::portable::RefCheck, String> {
1114 if !repo.ends_with(".git") {
1115 return Err(format!("`{repo}` is not a repository path"));
1116 }
1117 let bare = crate::repo_path_in(&self.root, repo).map_err(|e| format!("{repo}: {e}"))?;
1118 if !bare.join("HEAD").is_file() {
1119 crate::create_repo_in(&self.root, repo).map_err(|e| format!("creating {repo}: {e}"))?;
1120 }
1121 let out = std::process::Command::new("git")
1122 .args([
1123 "-c",
1124 "credential.helper=",
1125 "fetch",
1126 "--quiet",
1127 "--prune",
1128 "--no-tags",
1129 ])
1130 .arg(format!("{}/{repo}", self.home.url))
1131 .arg("+refs/*:refs/*")
1132 .current_dir(&bare)
1133 .env("GIT_TERMINAL_PROMPT", "0")
1134 .env("GIT_CONFIG_COUNT", "1")
1137 .env("GIT_CONFIG_KEY_0", "http.extraHeader")
1138 .env(
1139 "GIT_CONFIG_VALUE_0",
1140 format!("Authorization: {}", self.credential.basic()),
1141 )
1142 .output()
1143 .map_err(|e| format!("spawn git: {e}"))?;
1144 if !out.status.success() {
1145 return Err(format!(
1146 "git fetch {repo}: {}",
1147 String::from_utf8_lossy(&out.stderr).trim()
1148 ));
1149 }
1150 let held = crate::platform::read_git_refs(&bare)
1151 .ok_or_else(|| format!("{repo}: the fetched repository's refs cannot be read"))?;
1152 let wanted = self
1153 .platform
1154 .with_view(crate::portable::view_refs)
1155 .by_repo
1156 .remove(repo)
1157 .unwrap_or_default();
1158 Ok(crate::portable::check_refs(&wanted, &held))
1159 }
1160
1161 pub fn replicate_once(&self) -> Result<Progress, ReplicaError> {
1171 let outcome = self.round();
1172 let mut status = self.shared.status.lock().expect("replica status lock");
1173 status.head_seq = self.platform.view_seq().checked_sub(1);
1174 match &outcome {
1175 Ok(_) => status.last_error = None,
1176 Err(ReplicaError::Unreachable(why)) => status.last_error = Some(why.clone()),
1177 Err(ReplicaError::Halted { seq, reason }) => {
1178 status.halted = Some((*seq, reason.clone()));
1179 }
1180 Err(ReplicaError::Gap { window_base }) => {
1181 status.gap = true;
1182 status.halted = Some((
1183 self.platform.view_seq(),
1184 format!(
1185 "the home's log now starts at seq {window_base}; everything before it \
1186 is evicted and this copy cannot continue the chain"
1187 ),
1188 ));
1189 }
1190 }
1191 outcome.map(|appended| Progress {
1192 appended,
1193 status: status.clone(),
1194 })
1195 }
1196
1197 fn round(&self) -> Result<u64, ReplicaError> {
1198 {
1199 let status = self.shared.status.lock().expect("replica status lock");
1200 if let Some((seq, reason)) = &status.halted {
1201 return Err(ReplicaError::Halted {
1202 seq: *seq,
1203 reason: reason.clone(),
1204 });
1205 }
1206 }
1207 self.fetch_signers()?;
1208 let mut appended = 0;
1209 for _ in 0..MAX_PAGES_PER_ROUND {
1210 let page = self.fetch_page(self.platform.view_seq())?;
1211 appended += page.appended;
1212 if page.served == 0 {
1213 break;
1214 }
1215 }
1216 let refs = self.platform.with_view(crate::portable::view_refs);
1217 let (mut verified, mut pending) = (0, refs.unbacked.len());
1218 let mut failure = None;
1219 for (repo, wanted) in &refs.by_repo {
1220 match self.fetch_objects(repo) {
1221 Ok(check) => {
1222 verified += check.matched;
1223 pending += check.disagreements.len();
1224 }
1225 Err(why) => {
1226 pending += wanted.len();
1227 failure.get_or_insert(why);
1228 }
1229 }
1230 }
1231 {
1232 let mut status = self.shared.status.lock().expect("replica status lock");
1233 status.refs_verified = verified;
1234 status.refs_pending = pending;
1235 }
1236 match failure {
1237 Some(why) => Err(ReplicaError::Unreachable(why)),
1238 None => Ok(appended),
1239 }
1240 }
1241}
1242
1243pub fn run(replica: &Replica, strict: bool) -> ! {
1251 let mut said: Option<String> = None;
1252 loop {
1253 if replica.platform.durability_failed() {
1256 eprintln!(
1257 "choir: the op log is no longer durable, so this seed has stopped \
1258 replicating; exiting so supervision restarts it"
1259 );
1260 std::process::exit(75);
1261 }
1262 match replica.replicate_once() {
1263 Ok(progress) => {
1264 if progress.appended > 0 {
1265 eprintln!(
1266 "choir: seed took {} entries from {} (head {}, refs {} verified, {} \
1267 pending)",
1268 progress.appended,
1269 replica.home.url,
1270 progress
1271 .status
1272 .head_seq
1273 .map_or_else(|| "none".to_string(), |seq| seq.to_string()),
1274 progress.status.refs_verified,
1275 progress.status.refs_pending,
1276 );
1277 }
1278 said = None;
1279 }
1280 Err(error) => {
1281 let text = error.to_string();
1282 if said.as_deref() != Some(text.as_str()) {
1283 eprintln!("choir: {text}");
1284 said = Some(text);
1285 }
1286 if error.is_halt() && strict {
1287 std::process::exit(1);
1288 }
1289 }
1290 }
1291 std::thread::sleep(std::time::Duration::from_secs(INTERVAL_SECS));
1292 }
1293}
1294
1295#[cfg(test)]
1296mod tests {
1297 use super::{Relation, SignedStatement, SnapshotChain, WitnessStatement};
1298
1299 fn statement() -> WitnessStatement {
1300 WitnessStatement {
1301 format_version: 1,
1302 witness: "1e-aa".into(),
1303 home_node_id: "1e-bb".into(),
1304 snapshot: "1e-cc".into(),
1305 at_seq: 4,
1306 seen_at_seq: 9,
1307 }
1308 }
1309
1310 #[test]
1313 fn a_statements_signed_bytes_are_its_fields_in_order() {
1314 assert_eq!(
1315 String::from_utf8(statement().canonical_bytes()).unwrap(),
1316 r#"{"format_version":1,"witness":"1e-aa","home_node_id":"1e-bb","snapshot":"1e-cc","at_seq":4,"seen_at_seq":9}"#
1317 );
1318 let back = WitnessStatement::from_json(&statement().to_json()).unwrap();
1319 assert_eq!(back, statement());
1320 }
1321
1322 #[test]
1325 fn a_statement_verifies_only_as_the_witness_it_names() {
1326 let key = choir_identity::ActorKey::generate();
1327 let own = WitnessStatement {
1328 witness: key.actor_id().to_hex(),
1329 ..statement()
1330 };
1331 let signed = SignedStatement::sign(&key, own);
1332 signed.verify().unwrap();
1333 let round = SignedStatement::from_json(&signed.to_json()).unwrap();
1334 assert_eq!(round, signed);
1335 let claimed = SignedStatement::sign(&key, statement());
1336 assert!(claimed.verify().unwrap_err().contains("names witness"));
1337 }
1338
1339 #[test]
1341 fn two_attestations_agree_by_ancestry_and_fork_otherwise() {
1342 let mut chain = SnapshotChain::default();
1343 chain.insert("a".into(), None);
1344 chain.insert("b".into(), Some("a".into()));
1345 chain.insert("c".into(), Some("b".into()));
1346 chain.insert("x".into(), Some("a".into()));
1347 assert_eq!(chain.relate("c", "c"), Relation::Same);
1348 assert_eq!(chain.relate("a", "c"), Relation::Behind(2));
1349 assert_eq!(chain.relate("c", "b"), Relation::Ahead(1));
1350 assert_eq!(chain.relate("x", "c"), Relation::Fork);
1351 assert_eq!(chain.relate("nowhere", "c"), Relation::Fork);
1352 }
1353}