Skip to main content

choir_demo/
main.rs

1//! Guided walkthrough of the whole stack: run `cargo run -p choir-demo`.
2//!
3//! Three "agents" get keys, race signed ops through the single-writer
4//! sequencer (one gets rejected, one gets a first-class conflict and
5//! keeps working), the op log is time-traveled, and a real `git` client
6//! pushes through the choir-node daemon at the end.
7//!
8//! # Where this sits
9//!
10//! `docs/architecture.md` is the map of the whole workspace.
11//! This crate is a narrated walkthrough of the whole stack, and the fastest answer to "what is this?".
12//!
13//! It builds on [`choir_hash`], [`choir_identity`], [`choir_merge`], [`choir_node`], [`choir_oplog`], [`choir_sequencer`], [`choir_store`] and [`choir_view`].
14
15use std::collections::BTreeMap;
16
17use choir_identity::{ActorKey, Registry};
18use choir_merge::{MergeOutcome, Pipeline};
19use choir_oplog::{MemLog, OpEntry};
20use choir_sequencer::{Sequencer, Submission, SubmitPolicy};
21use choir_store::{get_blob, put_blob, ChunkerParams, MemStore};
22use choir_view::{Commit, OpKind, TreeEntry, View, ViewOp};
23
24/// The production admission shape: verify the author's signature, then
25/// CAS the op against a view cached on the writer thread.
26struct ChoirPolicy {
27    registry: Registry,
28    view: View,
29}
30
31impl SubmitPolicy for ChoirPolicy {
32    fn check(&mut self, sub: &Submission) -> Result<(), String> {
33        let sig = sub.author_sig.as_ref().ok_or("unsigned submission")?;
34        self.registry
35            .verify_submission(&sub.channel, &sub.payload, sig)
36            .map_err(|e| format!("bad signature: {e:?}"))?;
37        let op = ViewOp::from_payload(&sub.payload).map_err(|e| format!("bad op: {e:?}"))?;
38        let mut trial = self.view.clone();
39        trial.apply(&op).map_err(|e| format!("stale head: {e:?}"))
40    }
41
42    fn accepted(&mut self, entry: &OpEntry, _hash: &choir_hash::ContentHash) {
43        let op = ViewOp::from_payload(&entry.payload).expect("checked");
44        self.view.apply(&op).expect("checked");
45    }
46}
47
48fn short(h: &choir_hash::ContentHash) -> String {
49    h.to_hex()[..11].to_string()
50}
51
52fn section(title: &str) {
53    println!("\n=== {title} ===");
54}
55
56fn main() {
57    let mut store = MemStore::new();
58
59    section("1. Identity (L8): one ed25519 key per agent");
60    let alice = ActorKey::generate();
61    let bob = ActorKey::generate();
62    let mallory = ActorKey::generate();
63    let mut registry = Registry::new();
64    registry.register(&alice.public_key_bytes()).unwrap();
65    registry.register(&bob.public_key_bytes()).unwrap();
66    println!(
67        "  alice   -> actor id {}  (registered)",
68        short(&alice.actor_id())
69    );
70    println!(
71        "  bob     -> actor id {}  (registered)",
72        short(&bob.actor_id())
73    );
74    println!(
75        "  mallory -> actor id {}  (NOT registered)",
76        short(&mallory.actor_id())
77    );
78
79    section("2. Content-addressed commits (L0/L1): BLAKE3 + FastCDC");
80    let base_src = "fn main() {\n    println!(\"v1\");\n}\n";
81    let blob = put_blob(&mut store, base_src.as_bytes(), ChunkerParams::default()).unwrap();
82    let mut tree = BTreeMap::new();
83    tree.insert("main.rs".to_string(), TreeEntry::File { blob });
84    let c_base = Commit {
85        format_version: choir_view::FORMAT_VERSION,
86        parents: vec![],
87        tree,
88        author: short(&alice.actor_id()),
89        message: "base".into(),
90        resolves: None,
91    }
92    .put(&mut store)
93    .unwrap();
94    println!("  base commit {} (tree: main.rs)", short(&c_base));
95
96    section("3. Signed ops through the single-writer sequencer (L2)");
97    let sequencer = Sequencer::spawn_with_policy(
98        Box::new(MemLog::new()),
99        Box::new(ChoirPolicy {
100            registry,
101            view: View::default(),
102        }),
103    );
104    let handle = sequencer.handle();
105
106    let op = ViewOp::new(OpKind::SetRef {
107        name: "main".into(),
108        commit: c_base.clone(),
109        prev: None,
110    })
111    .to_payload();
112    let sig = alice.sign_submission("alice", &op);
113    let acc = handle.try_submit("alice", op, Some(sig)).unwrap();
114    println!(
115        "  alice sets main -> {}   ACCEPTED seq={} ({:?})",
116        short(&c_base),
117        acc.seq,
118        acc.decision_latency
119    );
120
121    let op = ViewOp::new(OpKind::SetWorkspaceHead {
122        workspace: "bob".into(),
123        commit: c_base.clone(),
124        prev: None,
125    })
126    .to_payload();
127    let sig = bob.sign_submission("bob", &op);
128    let acc = handle.try_submit("bob", op, Some(sig)).unwrap();
129    println!("  bob checks out base          ACCEPTED seq={}", acc.seq);
130
131    let op = ViewOp::new(OpKind::SetRef {
132        name: "main".into(),
133        commit: c_base.clone(),
134        prev: None,
135    })
136    .to_payload();
137    let sig = mallory.sign_submission("mallory", &op);
138    let err = handle.try_submit("mallory", op, Some(sig)).unwrap_err();
139    println!("  mallory (unregistered key)   REJECTED: {err}");
140
141    let op = ViewOp::new(OpKind::SetRef {
142        name: "main".into(),
143        commit: c_base.clone(),
144        prev: None, // stale: claims main doesn't exist yet
145    })
146    .to_payload();
147    let sig = bob.sign_submission("bob", &op);
148    let err = handle.try_submit("bob", op, Some(sig)).unwrap_err();
149    println!(
150        "  bob with a stale CAS         REJECTED: {}",
151        &err[..err.len().min(80)]
152    );
153
154    section("4. Merge pipeline (L4): disjoint edits fold, real conflicts stay first-class");
155    let left = "fn main() {\n    println!(\"v1\");\n}\n// alice: added docs\n";
156    let right = "// bob: added header\nfn main() {\n    println!(\"v1\");\n}\n";
157    let r = Pipeline::default_v1().merge(base_src, left, right);
158    match &r.outcome {
159        MergeOutcome::Resolved(text) => {
160            println!("  disjoint edits -> RESOLVED by `{}` strategy:", r.strategy);
161            for l in text.lines() {
162                println!("    | {l}");
163            }
164        }
165        _ => unreachable!(),
166    }
167
168    let l2 = "fn main() {\n    println!(\"alice wins\");\n}\n";
169    let r2 = "fn main() {\n    println!(\"bob wins\");\n}\n";
170    let r = Pipeline::default_v1().merge(base_src, l2, r2);
171    let annotated = match &r.outcome {
172        MergeOutcome::Conflict { annotated } => {
173            println!("  same-line edits -> CONFLICT (no silent pick). Stored as a commit:");
174            annotated.clone()
175        }
176        _ => unreachable!(),
177    };
178
179    // The conflict becomes a *valid* commit; bob keeps working on top.
180    let b = put_blob(&mut store, base_src.as_bytes(), ChunkerParams::default()).unwrap();
181    let l = put_blob(&mut store, l2.as_bytes(), ChunkerParams::default()).unwrap();
182    let rr = put_blob(&mut store, r2.as_bytes(), ChunkerParams::default()).unwrap();
183    let mut tree = BTreeMap::new();
184    tree.insert(
185        "main.rs".to_string(),
186        TreeEntry::Conflict {
187            base: Some(b),
188            left: l,
189            right: rr,
190        },
191    );
192    let c_conflict = Commit {
193        format_version: choir_view::FORMAT_VERSION,
194        parents: vec![c_base.clone()],
195        tree,
196        author: "merge".into(),
197        message: "conflicted merge (first-class)".into(),
198        resolves: None,
199    }
200    .put(&mut store)
201    .unwrap();
202    let op = ViewOp::new(OpKind::SetWorkspaceHead {
203        workspace: "bob".into(),
204        commit: c_conflict.clone(),
205        prev: Some(c_base),
206    })
207    .to_payload();
208    let sig = bob.sign_submission("bob", &op);
209    let acc = handle.try_submit("bob", op, Some(sig)).unwrap();
210    println!(
211        "  bob's head -> conflicted commit {} ACCEPTED seq={} (work continues!)",
212        short(&c_conflict),
213        acc.seq
214    );
215    for l in annotated.lines().take(7) {
216        println!("    | {l}");
217    }
218
219    section("5. Time travel (L1): the op log replays to any point");
220    let log = sequencer.shutdown();
221    let now = View::materialize(log.as_ref()).unwrap();
222    let before = View::at(log.as_ref(), 2).unwrap();
223    println!(
224        "  view NOW     : bob @ {} (conflicted)",
225        short(now.workspaces.get("bob").unwrap())
226    );
227    println!(
228        "  view at op 2 : bob @ {} (before the merge -- undo is just prefix replay)",
229        short(before.workspaces.get("bob").unwrap())
230    );
231    println!(
232        "  log entries  : {} total, every one signed + hash-chained",
233        log.len()
234    );
235
236    section("6. Round-trip a blob out of the store (verified on read)");
237    let head = Commit::get(&store, now.workspaces.get("bob").unwrap()).unwrap();
238    if let TreeEntry::Conflict { left, .. } = &head.tree["main.rs"] {
239        let bytes = get_blob(&store, left).unwrap();
240        println!("  left side of bob's conflict, straight from the chunk store:");
241        for l in String::from_utf8_lossy(&bytes).lines() {
242            println!("    | {l}");
243        }
244    }
245
246    section("7. The daemon (L3): a real `git` push through choir-node");
247    let work = std::env::temp_dir().join(format!("choir-demo-{}", std::process::id()));
248    std::fs::create_dir_all(&work).unwrap();
249    let node = choir_node::Node::bind(&work.join("repos"), 0).unwrap();
250    let port = node.port();
251    node.create_repo("demo/hello.git").unwrap();
252    let node = std::sync::Arc::new(node);
253    {
254        let node = node.clone();
255        std::thread::spawn(move || node.serve_forever());
256    }
257    let url = format!("http://127.0.0.1:{port}/demo/hello.git");
258    println!("  daemon up at {url}");
259    let clone_dir = work.join("clone");
260    let git = |dir: &std::path::Path, args: &[&str]| {
261        let out = std::process::Command::new("git")
262            .args([
263                "-c",
264                "commit.gpgsign=false",
265                "-c",
266                "init.defaultBranch=main",
267            ])
268            .args(args)
269            .current_dir(dir)
270            .env("GIT_TERMINAL_PROMPT", "0")
271            .env("GIT_AUTHOR_NAME", "demo")
272            .env("GIT_AUTHOR_EMAIL", "demo@choir")
273            .env("GIT_COMMITTER_NAME", "demo")
274            .env("GIT_COMMITTER_EMAIL", "demo@choir")
275            .output()
276            .expect("git runs");
277        assert!(
278            out.status.success(),
279            "git {args:?}: {}",
280            String::from_utf8_lossy(&out.stderr)
281        );
282        String::from_utf8_lossy(&out.stdout).into_owned()
283    };
284    git(&work, &["clone", "-q", &url, clone_dir.to_str().unwrap()]);
285    std::fs::write(clone_dir.join("README.md"), "# pushed through choir-node\n").unwrap();
286    git(&clone_dir, &["add", "."]);
287    git(&clone_dir, &["commit", "-q", "-m", "hello from the demo"]);
288    git(&clone_dir, &["push", "-q", "origin", "HEAD:main"]);
289    println!("  cloned, committed, pushed. Fresh clone sees:");
290    let verify_dir = work.join("verify");
291    git(&work, &["clone", "-q", &url, verify_dir.to_str().unwrap()]);
292    let log_out = git(&verify_dir, &["log", "--oneline"]);
293    for l in log_out.lines() {
294        println!("    | {l}");
295    }
296    node.unblock();
297    std::fs::remove_dir_all(&work).ok();
298
299    println!("\nAll seven layers exercised. This is the stack the plan calls L0-L8.");
300}