oxedyne/ore/cli/tests/relay.rs
72.6 KiB, 42 runs
created by r2848102244:134, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! `ore sync <url>`: a repository and a relay, over a socket. |
| 2 | //! |
| 3 | //! Every test here starts a real `ore-relay` process on a loopback port and |
| 4 | //! drives the real `ore` binary against it, so what is exercised is the wire, the |
| 5 | //! signed request, the access list and the store, and not a shortcut around any |
| 6 | //! of them. |
| 7 | //! |
| 8 | //! # The case the whole rung exists for |
| 9 | //! |
| 10 | //! Two replicas that are never awake at the same time. A pushes on one day and |
| 11 | //! goes away; B, which has never met A and never will, pulls on the next and ends |
| 12 | //! holding A's history, A's files and A's key binding. No filesystem is shared |
| 13 | //! and no session spans the two. That is |
| 14 | //! [`a_relay_converges_two_replicas_that_never_meet`], and it is the thing |
| 15 | //! `ore sync <path>` cannot do. |
| 16 | //! |
| 17 | //! # And the case that keeps the relay honest |
| 18 | //! |
| 19 | //! The relay stores what it is given and verifies none of it, so a relay that |
| 20 | //! has been tampered with serves a forgery quite happily. It gets refused at the |
| 21 | //! client, by signature, by name -- |
| 22 | //! [`a_tampered_relay_cannot_alter_an_operation`]. That is the whole of "never an |
| 23 | //! authority" made testable: the relay can withhold, and it cannot lie. |
| 24 | |
| 25 | mod support; |
| 26 | |
| 27 | use support::{ |
| 28 | counted, |
| 29 | forge_payload, |
| 30 | key_in, |
| 31 | ore, |
| 32 | write, |
| 33 | Ran, |
| 34 | Scratch, |
| 35 | }; |
| 36 | |
| 37 | use oxedyne_fe2o3_core::prelude::*; |
| 38 | |
| 39 | use std::io::{ |
| 40 | BufRead, |
| 41 | BufReader, |
| 42 | Read, |
| 43 | Write as IoWrite, |
| 44 | }; |
| 45 | use std::net::TcpStream; |
| 46 | use std::path::{ |
| 47 | Path, |
| 48 | PathBuf, |
| 49 | }; |
| 50 | use std::process::{ |
| 51 | Child, |
| 52 | Command, |
| 53 | Stdio, |
| 54 | }; |
| 55 | |
| 56 | |
| 57 | /// Returns the `ore-relay` binary, which sits beside the `ore` one cargo built. |
| 58 | fn relay_binary() |
| 59 | -> Outcome<PathBuf> |
| 60 | { |
| 61 | let ore = PathBuf::from(env!("CARGO_BIN_EXE_ore")); |
| 62 | let dir = match ore.parent() { |
| 63 | Some(d) => d, |
| 64 | None => return Err(err!( |
| 65 | "The ore binary {:?} has no directory.", ore; Test, Missing)), |
| 66 | }; |
| 67 | let path = dir.join("ore-relay"); |
| 68 | if !path.is_file() { |
| 69 | return Err(err!( |
| 70 | "{:?} is not there. Run `cargo build --workspace` first: `cargo test` \ |
| 71 | compiles the relay's own test target and does not put its binary here.", path; |
| 72 | Test, Missing)); |
| 73 | } |
| 74 | // AND IT IS THE ONE THE SOURCES SAY IT IS. `cargo test --workspace` does not |
| 75 | // refresh this file -- it builds `ore_relay`'s own test target and leaves the |
| 76 | // binary as whatever last wrote it -- so a change to `relay/src/` is not in |
| 77 | // the relay these tests drive until somebody builds. On 2026-08-22 that ran a |
| 78 | // whole suite against a relay built from a deliberately broken tree half an |
| 79 | // hour earlier, and one test passed for that reason and no other. The comment |
| 80 | // this replaces asserted the opposite, in as many words. |
| 81 | let built = res!(res!(std::fs::metadata(&path)).modified()); |
| 82 | let mut newest: Option<(std::time::SystemTime, PathBuf)> = None; |
| 83 | // From the manifest and not from the binary: a target directory is often |
| 84 | // somewhere else entirely, and walking up from the artefact finds a cache |
| 85 | // rather than the tree, which is a guard that quietly never fires. |
| 86 | let src = match PathBuf::from(env!("CARGO_MANIFEST_DIR")).parent() { |
| 87 | Some(root) => root.join("relay").join("src"), |
| 88 | None => return Err(err!( |
| 89 | "The relay sources could not be found beside {:?}.", |
| 90 | env!("CARGO_MANIFEST_DIR"); Test, Missing)), |
| 91 | }; |
| 92 | if !src.is_dir() { |
| 93 | return Err(err!( |
| 94 | "{:?} is not there, so nothing checks that the relay these tests drive \ |
| 95 | is the one in the tree.", src; Test, Missing)); |
| 96 | } |
| 97 | let mut stack = vec![src]; |
| 98 | while let Some(at) = stack.pop() { |
| 99 | if !at.is_dir() { |
| 100 | continue; |
| 101 | } |
| 102 | for entry in res!(std::fs::read_dir(&at)) { |
| 103 | let entry = res!(entry); |
| 104 | let found = entry.path(); |
| 105 | if found.is_dir() { |
| 106 | stack.push(found); |
| 107 | } else if found.extension().and_then(|e| e.to_str()) == Some("rs") { |
| 108 | let when = res!(res!(entry.metadata()).modified()); |
| 109 | if newest.as_ref().map(|(w, _)| when > *w) != Some(false) { |
| 110 | newest = Some((when, found)); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | } |
| 115 | if let Some((when, which)) = newest { |
| 116 | if when > built { |
| 117 | return Err(err!( |
| 118 | "{:?} was built before {:?} was last written, so these tests would \ |
| 119 | drive a relay that is not the one in the tree. Run `cargo build \ |
| 120 | --workspace` and try again.", path, which; |
| 121 | Test, Mismatch)); |
| 122 | } |
| 123 | } |
| 124 | Ok(path) |
| 125 | } |
| 126 | |
| 127 | /// A relay process listening on a port of its own, stopped however the test |
| 128 | /// ends. |
| 129 | struct Relay { |
| 130 | /// The process. |
| 131 | child: Child, |
| 132 | /// The port it bound, which it chose and then said. |
| 133 | port: u16, |
| 134 | /// Where its repositories live. |
| 135 | data: PathBuf, |
| 136 | } |
| 137 | |
| 138 | impl Relay { |
| 139 | |
| 140 | /// Starts a relay over a data directory, on a port the operating system |
| 141 | /// picks. |
| 142 | /// |
| 143 | /// The port is read back from the relay's own first line rather than guessed |
| 144 | /// at, so two tests running at once cannot land on one port. |
| 145 | fn start(data: &Path) |
| 146 | -> Outcome<Self> |
| 147 | { |
| 148 | Self::start_with(data, &[]) |
| 149 | } |
| 150 | |
| 151 | /// Starts a relay with further arguments of the caller's choosing. |
| 152 | fn start_with(data: &Path, extra: &[&str]) |
| 153 | -> Outcome<Self> |
| 154 | { |
| 155 | let mut child = match Command::new(res!(relay_binary())) |
| 156 | .arg("serve") |
| 157 | .arg(fmt!("{}", data.display())) |
| 158 | .arg("--port").arg("0") |
| 159 | .args(extra) |
| 160 | .stdout(Stdio::piped()) |
| 161 | .stderr(Stdio::piped()) |
| 162 | .spawn() |
| 163 | { |
| 164 | Ok(c) => c, |
| 165 | Err(e) => return Err(err!(e, |
| 166 | "The relay could not be started over {:?}.", data; Test, IO)), |
| 167 | }; |
| 168 | let out = match child.stdout.take() { |
| 169 | Some(o) => o, |
| 170 | None => return Err(err!("The relay was started with no output."; Test, Missing)), |
| 171 | }; |
| 172 | let mut line = String::new(); |
| 173 | if let Err(e) = BufReader::new(out).read_line(&mut line) { |
| 174 | return Err(err!(e, "The relay said nothing about where it is listening."; |
| 175 | Test, IO)); |
| 176 | } |
| 177 | // "relay listening on 127.0.0.1:44321, holding ..." |
| 178 | let port = match line.split(':').nth(1).and_then(|rest| { |
| 179 | rest.split(',').next().and_then(|n| n.trim().parse::<u16>().ok()) |
| 180 | }) { |
| 181 | Some(p) => p, |
| 182 | None => return Err(err!( |
| 183 | "The relay's first line names no port: {:?}", line; Test, Mismatch)), |
| 184 | }; |
| 185 | Ok(Self { child, port, data: data.to_path_buf() }) |
| 186 | } |
| 187 | |
| 188 | /// Starts a relay that will not take more than `bytes` in one request body. |
| 189 | /// |
| 190 | /// What a reverse proxy in front of a relay decides, and what the deployment |
| 191 | /// this exists for sets to eight mebibytes. Small here so an operation over it |
| 192 | /// costs a test kilobytes rather than megabytes; the boundary is the same |
| 193 | /// boundary. |
| 194 | fn start_capped(data: &Path, bytes: usize) |
| 195 | -> Outcome<Self> |
| 196 | { |
| 197 | Self::start_with(data, &["--post-bytes", &fmt!("{}", bytes)]) |
| 198 | } |
| 199 | |
| 200 | /// Posts a body with no credential at all, and returns the whole answer. |
| 201 | fn unsigned_post(&self, path: &str, body: &[u8]) |
| 202 | -> Outcome<String> |
| 203 | { |
| 204 | let mut stream = match TcpStream::connect(("127.0.0.1", self.port)) { |
| 205 | Ok(s) => s, |
| 206 | Err(e) => return Err(err!(e, "The relay would not take a connection."; Test, IO)), |
| 207 | }; |
| 208 | let head = fmt!( |
| 209 | "POST {} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\ |
| 210 | Content-Type: application/octet-stream\r\nContent-Length: {}\r\n\r\n", |
| 211 | path, body.len(), |
| 212 | ); |
| 213 | res!(stream.write_all(head.as_bytes())); |
| 214 | res!(stream.write_all(body)); |
| 215 | let mut said = String::new(); |
| 216 | res!(stream.read_to_string(&mut said)); |
| 217 | Ok(said) |
| 218 | } |
| 219 | |
| 220 | /// Starts a relay that will not put more than `bytes` in one reply. |
| 221 | /// |
| 222 | /// The default is six mebibytes, and pushing that much through a test to reach |
| 223 | /// the boundary would cost minutes; the boundary is the same boundary at any |
| 224 | /// size. |
| 225 | fn start_bounded(data: &Path, bytes: usize) |
| 226 | -> Outcome<Self> |
| 227 | { |
| 228 | Self::start_with(data, &["--reply-bytes", &fmt!("{}", bytes)]) |
| 229 | } |
| 230 | |
| 231 | /// Returns the URL of one hosted repository. |
| 232 | fn url(&self, account: &str, name: &str) -> String { |
| 233 | fmt!("http://127.0.0.1:{}/{}/{}", self.port, account, name) |
| 234 | } |
| 235 | |
| 236 | /// Returns the path of a hosted repository's first segment. |
| 237 | fn segment(&self, account: &str, name: &str) -> PathBuf { |
| 238 | self.data.join(account).join(name).join("log").join("000000.seg") |
| 239 | } |
| 240 | |
| 241 | /// Returns every byte of a hosted repository's log, segments run together. |
| 242 | /// |
| 243 | /// What is on the relay's disk, asked of the disk. The relay's own API would |
| 244 | /// answer the same question and is exactly what a test of what the relay holds |
| 245 | /// must not ask, since it is the thing under test. |
| 246 | fn stored(&self, account: &str, name: &str) |
| 247 | -> Outcome<Vec<u8>> |
| 248 | { |
| 249 | let dir = self.data.join(account).join(name).join("log"); |
| 250 | let mut paths: Vec<PathBuf> = Vec::new(); |
| 251 | for entry in res!(std::fs::read_dir(&dir)) { |
| 252 | paths.push(res!(entry).path()); |
| 253 | } |
| 254 | paths.sort(); |
| 255 | let mut out = Vec::new(); |
| 256 | for path in paths { |
| 257 | out.extend(res!(std::fs::read(&path))); |
| 258 | } |
| 259 | Ok(out) |
| 260 | } |
| 261 | |
| 262 | /// Fetches a path with no credential at all, the way a stranger would. |
| 263 | fn unsigned_get(&self, path: &str) |
| 264 | -> Outcome<String> |
| 265 | { |
| 266 | let mut stream = match TcpStream::connect(("127.0.0.1", self.port)) { |
| 267 | Ok(s) => s, |
| 268 | Err(e) => return Err(err!(e, "The relay would not take a connection."; Test, IO)), |
| 269 | }; |
| 270 | let request = fmt!( |
| 271 | "GET {} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\ |
| 272 | Content-Length: 0\r\n\r\n", path, |
| 273 | ); |
| 274 | res!(stream.write_all(request.as_bytes())); |
| 275 | let mut said = String::new(); |
| 276 | res!(stream.read_to_string(&mut said)); |
| 277 | Ok(said) |
| 278 | } |
| 279 | } |
| 280 | |
| 281 | impl Drop for Relay { |
| 282 | fn drop(&mut self) { |
| 283 | let _ = self.child.kill(); |
| 284 | let _ = self.child.wait(); |
| 285 | } |
| 286 | } |
| 287 | |
| 288 | |
| 289 | /// Runs `ore-relay` once and fails the test if it did not succeed. |
| 290 | fn relay_cmd(args: &[&str]) |
| 291 | -> Outcome<String> |
| 292 | { |
| 293 | let out = match Command::new(res!(relay_binary())).args(args).output() { |
| 294 | Ok(o) => o, |
| 295 | Err(e) => return Err(err!(e, |
| 296 | "`ore-relay {}` could not be run.", args.join(" "); Test, IO)), |
| 297 | }; |
| 298 | if !out.status.success() { |
| 299 | return Err(err!( |
| 300 | "`ore-relay {}` failed: {}{}", args.join(" "), |
| 301 | String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr); |
| 302 | Test, IO)); |
| 303 | } |
| 304 | Ok(fmt!("{}", String::from_utf8_lossy(&out.stdout))) |
| 305 | } |
| 306 | |
| 307 | |
| 308 | /// Makes a signed repository and returns where it is and what its public key is. |
| 309 | fn replica(scratch: &Scratch, name: &str) |
| 310 | -> Outcome<(PathBuf, String)> |
| 311 | { |
| 312 | let dir = res!(scratch.sub(name)); |
| 313 | let said = res!(ore(&dir, &["init"])); |
| 314 | let key = res!(key_in(res!(said.good("init")))); |
| 315 | Ok((dir, key)) |
| 316 | } |
| 317 | |
| 318 | /// Runs `ore sync <url>` and returns what it said. |
| 319 | fn sync(dir: &Path, url: &str) |
| 320 | -> Outcome<Ran> |
| 321 | { |
| 322 | ore(dir, &["sync", url]) |
| 323 | } |
| 324 | |
| 325 | /// Runs `ore sync <url>` and fails the test if it did not succeed. |
| 326 | fn synced(dir: &Path, url: &str) |
| 327 | -> Outcome<String> |
| 328 | { |
| 329 | let out = res!(sync(dir, url)); |
| 330 | Ok(fmt!("{}", res!(out.good("sync <url>")))) |
| 331 | } |
| 332 | |
| 333 | /// Runs `ore sync <url> --pull-only` in a directory and returns what it said. |
| 334 | fn pull_only(dir: &Path, url: &str) |
| 335 | -> Outcome<Ran> |
| 336 | { |
| 337 | ore(dir, &["sync", url, "--pull-only"]) |
| 338 | } |
| 339 | |
| 340 | /// Runs `ore sync <url> --dry-run` in a directory and returns what it said. |
| 341 | fn dry_run(dir: &Path, url: &str) |
| 342 | -> Outcome<Ran> |
| 343 | { |
| 344 | ore(dir, &["sync", url, "--dry-run"]) |
| 345 | } |
| 346 | |
| 347 | /// What `ore log` says a repository holds: how many operations, and the |
| 348 | /// frontier. |
| 349 | /// |
| 350 | /// The frontier is the part that matters when two replicas are compared. A count |
| 351 | /// can match by coincidence; a frontier names the operations themselves, and two |
| 352 | /// repositories printing the same one hold the same history. |
| 353 | fn history(dir: &Path) |
| 354 | -> Outcome<(usize, String)> |
| 355 | { |
| 356 | let out = res!(ore(dir, &["log"])); |
| 357 | let text = fmt!("{}", res!(out.good("log"))); |
| 358 | let mut count: Option<usize> = None; |
| 359 | let mut frontier: Option<String> = None; |
| 360 | for line in text.lines() { |
| 361 | if let Some(rest) = line.strip_prefix("frontier ") { |
| 362 | frontier = Some(fmt!("{}", rest.trim())); |
| 363 | continue; |
| 364 | } |
| 365 | if !line.contains(" in ") || !line.contains(" on replica ") { |
| 366 | continue; |
| 367 | } |
| 368 | let first = match line.split_whitespace().next() { |
| 369 | Some(f) => f, |
| 370 | None => continue, |
| 371 | }; |
| 372 | count = match first.parse::<usize>() { |
| 373 | Ok(n) => Some(n), |
| 374 | Err(_) => continue, |
| 375 | }; |
| 376 | } |
| 377 | match (count, frontier) { |
| 378 | (Some(n), Some(f)) => Ok((n, f)), |
| 379 | _ => Err(err!( |
| 380 | "`ore log` in {:?} said neither how many operations nor which frontier: {}", |
| 381 | dir, text; |
| 382 | Test, Missing)), |
| 383 | } |
| 384 | } |
| 385 | |
| 386 | |
| 387 | /// The two halves of the traffic the summary reports: bytes up, then bytes down. |
| 388 | /// |
| 389 | /// Two numbers and never their sum. The defect they exist for is invisible in |
| 390 | /// one: a loose frontier walk hands a peer a log that peer already holds, which |
| 391 | /// is upload and nothing else, and a clone that took 87 MB down was offering |
| 392 | /// several hundred back up while it did so. |
| 393 | fn traffic(said: &str) |
| 394 | -> Outcome<(usize, usize)> |
| 395 | { |
| 396 | for line in said.lines() { |
| 397 | if !line.contains("this relay publishes") { |
| 398 | continue; |
| 399 | } |
| 400 | // "<up> bytes up, <down> down, largest request body <n> against the <m> ..." |
| 401 | let words: Vec<&str> = line.split_whitespace().collect(); |
| 402 | let read = |at: usize| -> Option<usize> { |
| 403 | words.get(at).and_then(|w| w.parse::<usize>().ok()) |
| 404 | }; |
| 405 | return match (read(0), read(3)) { |
| 406 | (Some(up), Some(down)) => Ok((up, down)), |
| 407 | _ => Err(err!( |
| 408 | "The traffic line names no two numbers: {:?}", line; Test, Mismatch)), |
| 409 | }; |
| 410 | } |
| 411 | Err(err!( |
| 412 | "The output says nothing about what crossed: {}", said; Test, Missing)) |
| 413 | } |
| 414 | |
| 415 | |
| 416 | /// A pushes, goes away, and B -- which has never met A -- pulls the whole |
| 417 | /// history, the files and the key that signed them. |
| 418 | /// |
| 419 | /// This is the service the rung exists to provide, and the one a filesystem |
| 420 | /// transport cannot: no directory is shared between the two working copies, and |
| 421 | /// no session spans them. |
| 422 | #[test] |
| 423 | fn a_relay_converges_two_replicas_that_never_meet() -> Outcome<()> { |
| 424 | let scratch = res!(Scratch::new("relay_apart")); |
| 425 | let data = res!(scratch.sub("data")); |
| 426 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 427 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 428 | let data_arg = fmt!("{}", data.display()); |
| 429 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 430 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 431 | let relay = res!(Relay::start(&data)); |
| 432 | let url = relay.url("oxedyne", "ore"); |
| 433 | |
| 434 | // Monday: A writes and pushes. |
| 435 | res!(write(&a, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 436 | res!(write(&a, "notes.md", b"shared notes\n")); |
| 437 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 438 | let said = res!(synced(&a, &url)); |
| 439 | assert!(said.contains("sent 5 operations the relay did not hold"), |
| 440 | "the whole history goes up: {}", said); |
| 441 | assert!(said.contains("received 0 operations"), "and nothing comes back: {}", said); |
| 442 | // One session, and the requests are counted separately from it. Until |
| 443 | // 2026-08-20 this line said "2 round trips" and meant two turns of the state |
| 444 | // machine, while a push could go out as a dozen HTTP requests. |
| 445 | assert!(said.contains("over 1 session"), "in one session: {}", said); |
| 446 | assert!(said.contains(" request"), "and the requests are counted: {}", said); |
| 447 | |
| 448 | // Tuesday: B, which has never met A, pulls. |
| 449 | let said = res!(synced(&b, &url)); |
| 450 | assert!(said.contains("received 5 operations this repository did not hold"), |
| 451 | "the whole history comes down: {}", said); |
| 452 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), b"alpha\nbeta\ngamma\n".to_vec()); |
| 453 | assert_eq!(res!(std::fs::read(b.join("notes.md"))), b"shared notes\n".to_vec()); |
| 454 | assert_eq!(res!(history(&a)), res!(history(&b)), |
| 455 | "the two replicas hold different histories"); |
| 456 | |
| 457 | // A's key crossed with the operations, so B can say who wrote them rather |
| 458 | // than merely that somebody did. |
| 459 | assert!(said.contains("1 learned now"), "B learned A's binding: {}", said); |
| 460 | let listing = fmt!("{}", res!(res!(ore(&b, &["log"])).good("log"))); |
| 461 | assert!(listing.contains("5 signed and verified, 0 signed by an unknown key"), |
| 462 | "every operation is attributable at B: {}", listing); |
| 463 | |
| 464 | // Neither working copy is left behind its own log, so there is no marker to |
| 465 | // leave: the relay holds no working copy for a marker to protect. |
| 466 | assert!(!a.join(".ore").join("pending").is_file(), "a marker was left at A"); |
| 467 | assert!(!b.join(".ore").join("pending").is_file(), "a marker was left at B"); |
| 468 | |
| 469 | // A second sync either way has nothing to carry. |
| 470 | let said = res!(synced(&b, &url)); |
| 471 | assert!(said.contains("nothing to exchange"), "the second visit is empty: {}", said); |
| 472 | Ok(()) |
| 473 | } |
| 474 | |
| 475 | /// Two replicas that edited the same line converge through the relay, and both |
| 476 | /// arbitrate it the same way and say so in the same words. |
| 477 | /// |
| 478 | /// Two concurrent replacements of one line are one overlap group. The member |
| 479 | /// highest in op order prevails and the other yields, its insertion buried whole, |
| 480 | /// so each working copy holds one writer's line rather than both writers' |
| 481 | /// fragments; and the arbitration is a function of the operation set, so the two |
| 482 | /// replicas reach it independently and render the same bytes. |
| 483 | #[test] |
| 484 | fn a_divergence_converges_through_the_relay() -> Outcome<()> { |
| 485 | let scratch = res!(Scratch::new("relay_diverge")); |
| 486 | let data = res!(scratch.sub("data")); |
| 487 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 488 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 489 | let data_arg = fmt!("{}", data.display()); |
| 490 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 491 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 492 | let relay = res!(Relay::start(&data)); |
| 493 | let url = relay.url("oxedyne", "ore"); |
| 494 | |
| 495 | res!(write(&a, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 496 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 497 | res!(synced(&a, &url)); |
| 498 | res!(synced(&b, &url)); |
| 499 | |
| 500 | // Each writes over the same middle line, knowing nothing of the other. That |
| 501 | // last part is the genuine concurrency. |
| 502 | res!(write(&a, "f.txt", b"alpha\nBETA from A\ngamma\n")); |
| 503 | res!(write(&a, "only-a.txt", b"a line only A wrote\n")); |
| 504 | res!(write(&b, "f.txt", b"alpha\nBETA from B\ngamma\n")); |
| 505 | res!(write(&b, "only-b.txt", b"a line only B wrote\n")); |
| 506 | |
| 507 | // Three visits, none of them at the same time as another. |
| 508 | res!(synced(&a, &url)); |
| 509 | res!(synced(&b, &url)); |
| 510 | res!(synced(&a, &url)); |
| 511 | |
| 512 | let here = res!(history(&a)); |
| 513 | assert_eq!(res!(history(&b)), here, "the two replicas hold different histories"); |
| 514 | |
| 515 | let merged = res!(std::fs::read(a.join("f.txt"))); |
| 516 | let shown = fmt!("{}", String::from_utf8_lossy(&merged)); |
| 517 | assert!(shown == "alpha\nBETA from A\ngamma\n" || shown == "alpha\nBETA from B\ngamma\n", |
| 518 | "the contended line is one writer's, whole: {:?}", shown); |
| 519 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), merged, |
| 520 | "and the other working copy renders the same bytes"); |
| 521 | // Neither writer's uncontended file is touched by any of it. |
| 522 | assert_eq!(res!(std::fs::read(a.join("only-b.txt"))), b"a line only B wrote\n".to_vec()); |
| 523 | assert_eq!(res!(std::fs::read(b.join("only-a.txt"))), b"a line only A wrote\n".to_vec()); |
| 524 | |
| 525 | // The concurrent edit is flagged, and flagged identically, at both ends: the |
| 526 | // raw overlap, and what the renderer then did about it. |
| 527 | let mine = fmt!("{}", res!(res!(ore(&a, &["flags"])).good("flags"))); |
| 528 | let theirs = fmt!("{}", res!(res!(ore(&b, &["flags"])).good("flags"))); |
| 529 | assert_eq!(mine, theirs, "the two replicas flag different things"); |
| 530 | assert!(mine.contains("overlap"), "the raw fact is reported: {}", mine); |
| 531 | assert!(mine.contains("yielded") && mine.contains("prevailed"), |
| 532 | "and so is the arbitration: {}", mine); |
| 533 | assert!(mine.contains("2 flags in all"), "and nothing else is: {}", mine); |
| 534 | assert!(mine.contains("f.txt"), "and which file it was in: {}", mine); |
| 535 | |
| 536 | // The buried version is spilled beside each working copy, identically, so a |
| 537 | // reviewer with no forge at either end has `diff` and needs nothing else. |
| 538 | let one = res!(spilled(&a)); |
| 539 | assert_eq!(one, res!(spilled(&b)), "the two spills differ"); |
| 540 | assert_eq!(one.len(), 1, "one version yielded, so one is spilled"); |
| 541 | // What is spilled is the whole of what the yielding operation wrote, which is |
| 542 | // the hunk the diff found rather than the line it sits in: the newline was |
| 543 | // never contended and is still the base's. |
| 544 | let held = fmt!("{}", String::from_utf8_lossy(&one[0])); |
| 545 | assert!(held == "BETA from A" || held == "BETA from B", |
| 546 | "the spill holds the buried author's whole insertion: {:?}", held); |
| 547 | assert!(!shown.contains(held.trim_end_matches('\n')), |
| 548 | "which is the line the file does not hold: {:?} against {:?}", held, shown); |
| 549 | Ok(()) |
| 550 | } |
| 551 | |
| 552 | /// The buried versions a repository has spilled, in name order. |
| 553 | fn spilled(root: &Path) |
| 554 | -> Outcome<Vec<Vec<u8>>> |
| 555 | { |
| 556 | let dir = root.join(".ore").join("collisions"); |
| 557 | if !dir.is_dir() { |
| 558 | return Ok(Vec::new()); |
| 559 | } |
| 560 | let mut names: Vec<std::path::PathBuf> = Vec::new(); |
| 561 | for entry in res!(std::fs::read_dir(&dir)) { |
| 562 | names.push(res!(entry).path()); |
| 563 | } |
| 564 | names.sort(); |
| 565 | let mut out: Vec<Vec<u8>> = Vec::new(); |
| 566 | for at in names { |
| 567 | out.push(res!(std::fs::read(at))); |
| 568 | } |
| 569 | Ok(out) |
| 570 | } |
| 571 | |
| 572 | /// A relay cannot alter an operation, because the client checks the signature |
| 573 | /// and the relay cannot produce one. |
| 574 | /// |
| 575 | /// The forgery is the competent version: the relay's stored segment is rewritten |
| 576 | /// whole, digests and all, so everything about the file is consistent afterwards |
| 577 | /// except the one thing nobody but the key holder can make. Somebody who owns the |
| 578 | /// relay's disk owns the digests beside the records, which is why the digest is |
| 579 | /// not what this test rests on. |
| 580 | #[test] |
| 581 | fn a_tampered_relay_cannot_alter_an_operation() -> Outcome<()> { |
| 582 | let scratch = res!(Scratch::new("relay_tamper")); |
| 583 | let data = res!(scratch.sub("data")); |
| 584 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 585 | let (c, key_c) = res!(replica(&scratch, "c")); |
| 586 | let data_arg = fmt!("{}", data.display()); |
| 587 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 588 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_c, "pull", "--data", &data_arg])); |
| 589 | |
| 590 | let altered = { |
| 591 | let relay = res!(Relay::start(&data)); |
| 592 | let url = relay.url("oxedyne", "ore"); |
| 593 | res!(write(&a, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 594 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 595 | res!(synced(&a, &url)); |
| 596 | // The relay is stopped while its disk is rewritten, which is the position |
| 597 | // somebody who owns the machine is in. |
| 598 | res!(forge_payload(&relay.segment("oxedyne", "ore"), 0)) |
| 599 | }; |
| 600 | |
| 601 | let relay = res!(Relay::start(&data)); |
| 602 | let url = relay.url("oxedyne", "ore"); |
| 603 | let out = res!(sync(&c, &url)); |
| 604 | assert!(!out.ok, "a client absorbed an operation the relay had altered"); |
| 605 | assert!(out.err.contains("does not verify against the public key it carries"), |
| 606 | "and the reason is the signature: {}", out.err); |
| 607 | assert!(out.err.contains(&altered), |
| 608 | "and it names the operation {}: {}", altered, out.err); |
| 609 | assert!(out.err.contains(&url), |
| 610 | "and where it came from: {}", out.err); |
| 611 | |
| 612 | // Nothing was absorbed: the batch is refused whole, so the client's history |
| 613 | // is exactly what it was. |
| 614 | let (count, _) = res!(history(&c)); |
| 615 | assert_eq!(count, 0, "a refused batch left {} operations behind", count); |
| 616 | assert!(!c.join("f.txt").is_file(), "and wrote no files"); |
| 617 | Ok(()) |
| 618 | } |
| 619 | |
| 620 | /// A key the access list does not name is refused, and so is a repository |
| 621 | /// nobody made. |
| 622 | #[test] |
| 623 | fn a_relay_answers_only_the_keys_its_list_names() -> Outcome<()> { |
| 624 | let scratch = res!(Scratch::new("relay_acl")); |
| 625 | let data = res!(scratch.sub("data")); |
| 626 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 627 | let (stranger, _) = res!(replica(&scratch, "stranger")); |
| 628 | let (reader, key_reader) = res!(replica(&scratch, "reader")); |
| 629 | let data_arg = fmt!("{}", data.display()); |
| 630 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 631 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_reader, "pull", "--data", &data_arg])); |
| 632 | let relay = res!(Relay::start(&data)); |
| 633 | let url = relay.url("oxedyne", "ore"); |
| 634 | |
| 635 | res!(write(&a, "f.txt", b"alpha\n")); |
| 636 | res!(synced(&a, &url)); |
| 637 | |
| 638 | // A key nobody granted anything to is refused at the first thing it asks, |
| 639 | // which is the bindings, and is told it may not even read. |
| 640 | let out = res!(sync(&stranger, &url)); |
| 641 | assert!(!out.ok, "a stranger was served"); |
| 642 | assert!(out.err.contains("holds no pull"), "and told why: {}", out.err); |
| 643 | assert!(out.err.contains("ore-relay grant"), |
| 644 | "and how access is given: {}", out.err); |
| 645 | |
| 646 | // A key granted a pull may take and may not give. |
| 647 | let said = res!(synced(&reader, &url)); |
| 648 | assert!(said.contains("received "), "a pull key pulls: {}", said); |
| 649 | res!(write(&reader, "mine.txt", b"something of my own\n")); |
| 650 | let out = res!(sync(&reader, &url)); |
| 651 | assert!(!out.ok, "a pull key pushed"); |
| 652 | assert!(out.err.contains("holds no push"), "and told why: {}", out.err); |
| 653 | // What it wrote is still its own, in its own history: a refused push costs |
| 654 | // convergence and nothing else. |
| 655 | assert_eq!(res!(std::fs::read(reader.join("mine.txt"))), b"something of my own\n".to_vec()); |
| 656 | |
| 657 | // And a repository nobody made is not made by pushing to it. |
| 658 | let out = res!(sync(&a, &relay.url("oxedyne", "nothing"))); |
| 659 | assert!(!out.ok, "a repository was created by pushing to it"); |
| 660 | assert!(out.err.contains("does not hold oxedyne/nothing"), |
| 661 | "and told why: {}", out.err); |
| 662 | assert!(out.err.contains("ore-relay create"), "and how one is made: {}", out.err); |
| 663 | Ok(()) |
| 664 | } |
| 665 | |
| 666 | /// A relay that is not there costs the capture and nothing else. |
| 667 | #[test] |
| 668 | fn an_unreachable_relay_changes_nothing_but_the_capture() -> Outcome<()> { |
| 669 | let scratch = res!(Scratch::new("relay_absent")); |
| 670 | let data = res!(scratch.sub("data")); |
| 671 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 672 | let data_arg = fmt!("{}", data.display()); |
| 673 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 674 | // Started only to learn a port, and stopped before anything is asked of it. |
| 675 | // |
| 676 | // The port is then CHECKED to be dead rather than assumed to be. The operating |
| 677 | // system hands a freed port straight back out, and this suite starts several |
| 678 | // relays at once, so another test's relay can be listening on it by the time |
| 679 | // this one connects -- which fails here as a 403 from a relay that was never |
| 680 | // supposed to answer, on a test whose name says the relay is absent. Seen once |
| 681 | // in the whole suite and never when this test ran alone, which is what a race |
| 682 | // looks like from the outside. |
| 683 | let mut dead = fmt!(""); |
| 684 | for _ in 0..8 { |
| 685 | let relay = res!(Relay::start(&data)); |
| 686 | let url = relay.url("oxedyne", "ore"); |
| 687 | let at = fmt!("127.0.0.1:{}", relay.port); |
| 688 | drop(relay); |
| 689 | if TcpStream::connect(&at).is_err() { |
| 690 | dead = url; |
| 691 | break; |
| 692 | } |
| 693 | } |
| 694 | if dead.is_empty() { |
| 695 | return Err(err!( |
| 696 | "No freed port stayed free long enough to point at nothing."; Test, IO)); |
| 697 | } |
| 698 | |
| 699 | res!(write(&a, "f.txt", b"alpha\n")); |
| 700 | let out = res!(sync(&a, &dead)); |
| 701 | assert!(!out.ok, "a sync with nothing at the other end succeeded"); |
| 702 | assert!(out.err.contains("could not be reached"), "and said so: {}", out.err); |
| 703 | assert!(out.err.contains(&dead), "naming the relay: {}", out.err); |
| 704 | assert!(out.err.contains("running the command again is the whole of the retry"), |
| 705 | "and what to do about it: {}", out.err); |
| 706 | |
| 707 | // The capture happened, which is wanted regardless, and nothing else did. |
| 708 | assert!(out.out.contains("created f.txt"), "the capture ran first: {}", out.out); |
| 709 | let (count, _) = res!(history(&a)); |
| 710 | assert!(count > 0, "the capture reached the history"); |
| 711 | assert!(!a.join(".ore").join("pending").is_file(), "a marker was left behind"); |
| 712 | assert_eq!(res!(std::fs::read(a.join("f.txt"))), b"alpha\n".to_vec()); |
| 713 | Ok(()) |
| 714 | } |
| 715 | |
| 716 | /// A small divergence over a large shared history goes by sketch over the wire, |
| 717 | /// and the sketch holds. |
| 718 | /// |
| 719 | /// The relay sizes its own answer from the shape the client's sketch message |
| 720 | /// states, so the saving runs in both directions rather than only up. |
| 721 | #[test] |
| 722 | fn a_small_divergence_over_a_large_history_uses_the_sketch() -> Outcome<()> { |
| 723 | let scratch = res!(Scratch::new("relay_sketch")); |
| 724 | let data = res!(scratch.sub("data")); |
| 725 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 726 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 727 | let data_arg = fmt!("{}", data.display()); |
| 728 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 729 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 730 | let relay = res!(Relay::start(&data)); |
| 731 | let url = relay.url("oxedyne", "ore"); |
| 732 | |
| 733 | for i in 0..60 { |
| 734 | res!(write(&a, &fmt!("f{:02}.txt", i), fmt!("file number {}\n", i).as_bytes())); |
| 735 | } |
| 736 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 737 | let said = res!(synced(&a, &url)); |
| 738 | assert!(said.contains("sent 121 operations"), |
| 739 | "the clone carries the whole history: {}", said); |
| 740 | let said = res!(synced(&b, &url)); |
| 741 | assert!(said.contains("received 121 operations"), |
| 742 | "and B takes it whole: {}", said); |
| 743 | |
| 744 | // One line at each end, and the two are two operations apart. |
| 745 | res!(write(&a, "f00.txt", b"file number 0\na tail from A\n")); |
| 746 | let said = res!(synced(&a, &url)); |
| 747 | assert!(said.contains("mode sketch"), |
| 748 | "a small difference over a large history is what a sketch is for: {}", said); |
| 749 | assert!(!said.contains("fell back"), |
| 750 | "and the estimate held, so no round trip was spent on the walk: {}", said); |
| 751 | res!(write(&b, "f01.txt", b"file number 1\na tail from B\n")); |
| 752 | res!(synced(&b, &url)); |
| 753 | res!(synced(&a, &url)); |
| 754 | assert_eq!(res!(history(&a)), res!(history(&b))); |
| 755 | Ok(()) |
| 756 | } |
| 757 | |
| 758 | /// A relay says which versions it speaks, to anybody, before anything is signed. |
| 759 | /// |
| 760 | /// An old client then fails with a sentence naming both sides rather than a |
| 761 | /// decode error part way through an exchange. |
| 762 | #[test] |
| 763 | fn a_relay_says_which_versions_it_speaks() -> Outcome<()> { |
| 764 | let scratch = res!(Scratch::new("relay_versions")); |
| 765 | let data = res!(scratch.sub("data")); |
| 766 | let relay = res!(Relay::start(&data)); |
| 767 | |
| 768 | let said = res!(relay.unsigned_get("/ore")); |
| 769 | assert!(said.contains("200"), "the version endpoint needs no credential: {}", said); |
| 770 | assert!(said.contains("\"transport\""), "it names the transport: {}", said); |
| 771 | assert!(said.contains("\"v1\""), "and its version: {}", said); |
| 772 | assert!(said.contains("\"oresyn\""), "and the message format: {}", said); |
| 773 | assert!(said.contains("\"oreseg\""), "and the segment format: {}", said); |
| 774 | |
| 775 | // A path of a version this relay does not speak is refused by name rather |
| 776 | // than guessed at. |
| 777 | let said = res!(relay.unsigned_get("/ore/v2/oxedyne/ore/sync")); |
| 778 | assert!(said.contains("404"), "an unknown transport version is not served: {}", said); |
| 779 | assert!(said.contains("which versions this relay speaks"), |
| 780 | "and the answer says where to ask: {}", said); |
| 781 | Ok(()) |
| 782 | } |
| 783 | |
| 784 | /// A marker an earlier local sync left is settled before a relay sync runs, so |
| 785 | /// the two transports do not tread on each other. |
| 786 | #[test] |
| 787 | fn a_relay_sync_settles_what_a_local_sync_left() -> Outcome<()> { |
| 788 | let scratch = res!(Scratch::new("relay_pending")); |
| 789 | let data = res!(scratch.sub("data")); |
| 790 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 791 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 792 | let data_arg = fmt!("{}", data.display()); |
| 793 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 794 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 795 | let relay = res!(Relay::start(&data)); |
| 796 | let url = relay.url("oxedyne", "ore"); |
| 797 | |
| 798 | // A local sync writes A's working copy and leaves the marker at B. |
| 799 | res!(write(&a, "f.txt", b"alpha\nbeta\ngamma\n")); |
| 800 | res!(res!(ore(&a, &["sync", &fmt!("{}", b.display())])).good("sync <path>")); |
| 801 | assert!(b.join(".ore").join("pending").is_file(), |
| 802 | "a local sync leaves the far end a marker"); |
| 803 | assert!(!b.join("f.txt").is_file(), "and does not write its files"); |
| 804 | |
| 805 | // The next verb at B is a relay sync, and it settles the marker first, as |
| 806 | // every verb there does. |
| 807 | let said = res!(synced(&b, &url)); |
| 808 | assert!(said.contains("the working copy is now the merged state"), |
| 809 | "the marker was settled before the exchange: {}", said); |
| 810 | assert!(!b.join(".ore").join("pending").is_file(), "and cleared"); |
| 811 | assert_eq!(res!(std::fs::read(b.join("f.txt"))), b"alpha\nbeta\ngamma\n".to_vec()); |
| 812 | |
| 813 | // And the relay took what B had, so a third replica would find it there. |
| 814 | assert!(said.contains("sent "), "B pushed what the local sync gave it: {}", said); |
| 815 | let listed = res!(relay_cmd(&["list", &data_arg])); |
| 816 | assert!(listed.contains("oxedyne/ore"), "the relay lists what it holds: {}", listed); |
| 817 | Ok(()) |
| 818 | } |
| 819 | |
| 820 | |
| 821 | /// A pull grant reads for a replica that holds work of its own, but only when it |
| 822 | /// offers nothing. |
| 823 | /// |
| 824 | /// A relay asks for push over the operations it lacks of what a request offers, |
| 825 | /// and refuses **the whole request** rather than its push half. So one operation |
| 826 | /// of its own locks a `pull` voice out of reading altogether. That is what |
| 827 | /// `--pull-only` is for: the messages go out emptied of what they would hand |
| 828 | /// over, so the request is the read it always was. |
| 829 | #[test] |
| 830 | fn a_pull_grant_reads_for_a_replica_holding_its_own_work() -> Outcome<()> { |
| 831 | let scratch = res!(Scratch::new("relay_pull_only")); |
| 832 | let data = res!(scratch.sub("data")); |
| 833 | let (owner, key_owner) = res!(replica(&scratch, "owner")); |
| 834 | let (reader, key_reader) = res!(replica(&scratch, "reader")); |
| 835 | let data_arg = fmt!("{}", data.display()); |
| 836 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_owner, "--data", &data_arg])); |
| 837 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_reader, "pull", "--data", &data_arg])); |
| 838 | let relay = res!(Relay::start(&data)); |
| 839 | let url = relay.url("oxedyne", "ore"); |
| 840 | |
| 841 | res!(write(&owner, "theirs.txt", b"the owner's work\n")); |
| 842 | res!(synced(&owner, &url)); |
| 843 | |
| 844 | // Work of its own is all it takes: the ordinary sync now carries an operation, |
| 845 | // so the request is a write and the read goes down with it. |
| 846 | res!(write(&reader, "mine.txt", b"work of my own\n")); |
| 847 | let out = res!(sync(&reader, &url)); |
| 848 | assert!(!out.ok, "a pull voice holding its own work was served an ordinary sync"); |
| 849 | assert!(out.err.contains("holds no push"), "and told why: {}", out.err); |
| 850 | |
| 851 | // Offering nothing, the same voice reads. |
| 852 | let said = fmt!("{}", res!(res!(pull_only(&reader, &url)).good("sync --pull-only"))); |
| 853 | assert!(said.contains("offered nothing, by request"), |
| 854 | "the sync says it handed nothing over: {}", said); |
| 855 | assert!(said.contains("kept back"), |
| 856 | "and says what stayed behind, the two ends not being left agreeing: {}", said); |
| 857 | assert_eq!(res!(std::fs::read(reader.join("theirs.txt"))), b"the owner's work\n".to_vec(), |
| 858 | "the owner's work arrived"); |
| 859 | assert_eq!(res!(std::fs::read(reader.join("mine.txt"))), b"work of my own\n".to_vec(), |
| 860 | "and the reader's own work is untouched"); |
| 861 | |
| 862 | // Nothing of the reader's reached the relay, which is the point rather than a |
| 863 | // side effect: the owner, who takes everything the relay holds, never sees it. |
| 864 | res!(synced(&owner, &url)); |
| 865 | assert!(!owner.join("mine.txt").exists(), |
| 866 | "the reader's work reached the relay and came back out of it"); |
| 867 | Ok(()) |
| 868 | } |
| 869 | |
| 870 | |
| 871 | /// A pull grant fetches, and fetches again once the author has moved on, with no |
| 872 | /// option and no ceremony. |
| 873 | /// |
| 874 | /// The walk is loose, so a replica that cannot subtract the author's new tip |
| 875 | /// offers its whole log back -- every operation of which came off this relay in |
| 876 | /// the first place. Read off the offer alone that is a push, and somebody told to |
| 877 | /// fetch a public repository gets exactly one successful command: the next is |
| 878 | /// refused for something it was not doing, which is the first thing an outside |
| 879 | /// reader meets. |
| 880 | #[test] |
| 881 | fn a_pull_grant_fetches_again_after_the_author_moves_on() -> Outcome<()> { |
| 882 | let scratch = res!(Scratch::new("relay_pull_again")); |
| 883 | let data = res!(scratch.sub("data")); |
| 884 | let (owner, key_owner) = res!(replica(&scratch, "owner")); |
| 885 | let (reader, key_reader) = res!(replica(&scratch, "reader")); |
| 886 | let data_arg = fmt!("{}", data.display()); |
| 887 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_owner, "--data", &data_arg])); |
| 888 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_reader, "pull", "--data", &data_arg])); |
| 889 | let relay = res!(Relay::start(&data)); |
| 890 | let url = relay.url("oxedyne", "ore"); |
| 891 | |
| 892 | res!(write(&owner, "readme.txt", b"the first word\n")); |
| 893 | res!(synced(&owner, &url)); |
| 894 | |
| 895 | // The clone. This much always worked: a replica holding nothing offers nothing. |
| 896 | let said = res!(synced(&reader, &url)); |
| 897 | assert!(said.contains("received "), "the clone arrives: {}", said); |
| 898 | assert_eq!(res!(std::fs::read(reader.join("readme.txt"))), b"the first word\n".to_vec()); |
| 899 | |
| 900 | // The author moves on, so the reader's next visit meets a head it has never |
| 901 | // seen and offers back everything it holds -- which is the clone. |
| 902 | res!(write(&owner, "readme.txt", b"the first word\nand a second\n")); |
| 903 | res!(synced(&owner, &url)); |
| 904 | |
| 905 | let said = res!(synced(&reader, &url)); |
| 906 | assert!(said.contains("received "), "and the next visit arrives too: {}", said); |
| 907 | assert_eq!(res!(std::fs::read(reader.join("readme.txt"))), |
| 908 | b"the first word\nand a second\n".to_vec(), |
| 909 | "what the author wrote after the clone did not reach the reader"); |
| 910 | assert_eq!(res!(history(&reader)), res!(history(&owner)), |
| 911 | "the reader is not where the author is"); |
| 912 | |
| 913 | // And the half that must not move: an operation the relay genuinely lacks is a |
| 914 | // push, and a pull grant does not cover one. |
| 915 | res!(write(&reader, "mine.txt", b"work of my own\n")); |
| 916 | let out = res!(sync(&reader, &url)); |
| 917 | assert!(!out.ok, "a pull grant handed over work the relay did not hold"); |
| 918 | assert!(out.err.contains("holds no push"), "and told why: {}", out.err); |
| 919 | Ok(()) |
| 920 | } |
| 921 | |
| 922 | |
| 923 | /// A rehearsal says what a sync would bring and leaves the repository as it was. |
| 924 | /// |
| 925 | /// It has to absorb to know: what would arrive is only knowable by taking it, |
| 926 | /// verifying it under the keys this end knows and placing it. So the test is not |
| 927 | /// that nothing crossed, which would be a weaker claim about a slower command, |
| 928 | /// but that nothing was *kept* -- the log is the length it was, the working copy |
| 929 | /// holds no file that arrived, and `--arrived` afterwards still speaks of the |
| 930 | /// sync before this one rather than of the rehearsal. |
| 931 | #[test] |
| 932 | fn a_rehearsal_says_what_would_arrive_and_keeps_none_of_it() -> Outcome<()> { |
| 933 | let scratch = res!(Scratch::new("relay_dry_run")); |
| 934 | let data = res!(scratch.sub("data")); |
| 935 | let (owner, key_owner) = res!(replica(&scratch, "owner")); |
| 936 | let (other, key_other) = res!(replica(&scratch, "other")); |
| 937 | let data_arg = fmt!("{}", data.display()); |
| 938 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_owner, "--data", &data_arg])); |
| 939 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_other, "push", "--data", &data_arg])); |
| 940 | let relay = res!(Relay::start(&data)); |
| 941 | let url = relay.url("oxedyne", "ore"); |
| 942 | |
| 943 | res!(write(&owner, "shared.txt", b"the beginning\n")); |
| 944 | res!(synced(&owner, &url)); |
| 945 | res!(synced(&other, &url)); |
| 946 | |
| 947 | // The other writes something and puts it on the relay. The owner has not |
| 948 | // taken it and is about to ask what taking it would come to. |
| 949 | res!(write(&other, "theirs.txt", b"work from the other end\n")); |
| 950 | res!(synced(&other, &url)); |
| 951 | |
| 952 | let (held, frontier) = res!(history(&owner)); |
| 953 | let said = fmt!("{}", res!(res!(dry_run(&owner, &url)).good("sync --dry-run"))); |
| 954 | |
| 955 | // It answers the question. How much is read out rather than written down: what |
| 956 | // the relay holds that this end does not depends on how many commands were run |
| 957 | // at the other end, each of which ends by naming the point it reached and |
| 958 | // pushing that name. The claim worth making is that the two ways the rehearsal |
| 959 | // says it agree with each other, and that the sync at the end of this test |
| 960 | // brings that many. |
| 961 | let would = res!(counted(&said, "would take ")); |
| 962 | assert!(would > 0, "the rehearsal says how much would arrive: {}", said); |
| 963 | assert_eq!(res!(counted(&said, "it would bring ")), would, |
| 964 | "the rehearsal counts one way in the summary and another in the description: \ |
| 965 | {}", said); |
| 966 | assert!(said.contains("theirs.txt"), |
| 967 | "naming the file that would appear: {}", said); |
| 968 | assert!(said.contains("nothing was written: this was a rehearsal"), |
| 969 | "and says plainly that it kept none of it: {}", said); |
| 970 | assert!(said.contains("offered nothing"), |
| 971 | "a rehearsal hands nothing over either: {}", said); |
| 972 | |
| 973 | // And keeps none of it. The log is where it was, to the frontier. |
| 974 | assert_eq!(res!(history(&owner)), (held, frontier), |
| 975 | "the rehearsal left operations in the repository"); |
| 976 | assert!(!owner.join("theirs.txt").exists(), |
| 977 | "the rehearsal wrote a file into the working copy"); |
| 978 | |
| 979 | // Nor did it leave a record behind it: `--arrived` still speaks of the sync |
| 980 | // that really happened, which is the first one. |
| 981 | let told = fmt!("{}", res!(res!(ore(&owner, &["log", "--arrived"])).good("log --arrived"))); |
| 982 | assert!(!told.contains("theirs.txt"), |
| 983 | "the rehearsal was recorded as an arrival: {}", told); |
| 984 | |
| 985 | // The real thing afterwards brings exactly what the rehearsal said it would, |
| 986 | // in name and in number. |
| 987 | let ran = res!(synced(&owner, &url)); |
| 988 | assert!(ran.contains(&fmt!("received {} operation", would)), |
| 989 | "the rehearsal said {} operations would arrive and the sync brought \ |
| 990 | something else: {}", would, ran); |
| 991 | assert_eq!(res!(std::fs::read(owner.join("theirs.txt"))), b"work from the other end\n".to_vec(), |
| 992 | "what the rehearsal described did not arrive when the sync ran"); |
| 993 | Ok(()) |
| 994 | } |
| 995 | |
| 996 | /// The claim, end to end: two replicas exchange a private repository through a |
| 997 | /// relay, both read it, and the relay's disk holds none of it. |
| 998 | /// |
| 999 | /// This is the one test the veiled form exists for, and every assertion in it is |
| 1000 | /// aimed at the relay's own bytes rather than at anything the relay says about |
| 1001 | /// itself. A relay that had been quietly reading the history would pass any |
| 1002 | /// question put to its API and fail the search below. |
| 1003 | /// |
| 1004 | /// The search is for the content by name: the file names, the text in the files, |
| 1005 | /// and the name of a mark. Each of those is a string a plaintext segment holds |
| 1006 | /// literally -- `a_relay_converges_two_replicas_that_never_meet` walks the same |
| 1007 | /// path unveiled, and the same search finds all of them there. |
| 1008 | #[test] |
| 1009 | fn a_veiled_repository_crosses_a_relay_that_cannot_read_it() -> Outcome<()> { |
| 1010 | let scratch = res!(Scratch::new("relay_veiled")); |
| 1011 | let data = res!(scratch.sub("data")); |
| 1012 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1013 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1014 | let data_arg = fmt!("{}", data.display()); |
| 1015 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1016 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1017 | let relay = res!(Relay::start(&data)); |
| 1018 | let url = relay.url("oxedyne", "ore"); |
| 1019 | |
| 1020 | // A veils, and is told the key. B is given the same key by hand, which is the |
| 1021 | // whole of how a content key travels: not through the relay. |
| 1022 | let said = fmt!("{}", res!(res!(ore(&a, &["key", "--veil"])).good("key --veil"))); |
| 1023 | assert!(said.contains("this repository is now veiled"), "{}", said); |
| 1024 | let content_key = match said.lines().find(|l| l.starts_with(" ")) { |
| 1025 | Some(l) => fmt!("{}", l.trim()), |
| 1026 | None => return Err(err!( |
| 1027 | "`ore key --veil` printed no key: {}", said; Test, Missing)), |
| 1028 | }; |
| 1029 | let said = fmt!("{}", res!(res!(ore(&b, &["key", "--veil", &content_key])) |
| 1030 | .good("key --veil <key>"))); |
| 1031 | assert!(said.contains("installed a content key"), "{}", said); |
| 1032 | |
| 1033 | // A writes something worth hiding and pushes it. |
| 1034 | let secret: &[u8] = b"the merger closes on Friday at eleven\n"; |
| 1035 | res!(write(&a, "board-minutes.md", secret)); |
| 1036 | res!(write(&a, "figures.csv", b"quarter,revenue\nQ3,4180000\n")); |
| 1037 | res!(res!(ore(&a, &["mark", "before-the-announcement"])).good("mark")); |
| 1038 | let said = res!(synced(&a, &url)); |
| 1039 | assert!(said.contains("sent "), "the history goes up: {}", said); |
| 1040 | |
| 1041 | // What the relay wrote down. Nothing of the content is in it. |
| 1042 | let held = res!(relay.stored("oxedyne", "ore")); |
| 1043 | for hidden in [ |
| 1044 | &secret[..], |
| 1045 | b"board-minutes.md", |
| 1046 | b"figures.csv", |
| 1047 | b"before-the-announcement", |
| 1048 | b"4180000", |
| 1049 | ] { |
| 1050 | assert!( |
| 1051 | !held.windows(hidden.len()).any(|w| w == hidden), |
| 1052 | "the relay's {} bytes on disk contain {:?}", |
| 1053 | held.len(), String::from_utf8_lossy(hidden), |
| 1054 | ); |
| 1055 | } |
| 1056 | // And what is there is veiled rather than merely absent: every record of the |
| 1057 | // log is tagged kind 3. The header is eight bytes, and a record's kind is its |
| 1058 | // first, so the byte after the header says what the first record is. |
| 1059 | assert_eq!(held[6], 4, "the segment declares the current format version"); |
| 1060 | assert!(held.len() > 8, "the relay wrote nothing at all"); |
| 1061 | assert_eq!(held[8], 3, "the first record the relay holds is not a veiled one"); |
| 1062 | |
| 1063 | // B, which has never met A, pulls, and reads every byte of it. |
| 1064 | let said = res!(synced(&b, &url)); |
| 1065 | assert!(said.contains("received "), "the history comes down: {}", said); |
| 1066 | assert_eq!(res!(std::fs::read(b.join("board-minutes.md"))), secret.to_vec(), |
| 1067 | "the replica holding the key did not get the content back"); |
| 1068 | assert_eq!(res!(std::fs::read(b.join("figures.csv"))), b"quarter,revenue\nQ3,4180000\n".to_vec()); |
| 1069 | let told = fmt!("{}", res!(res!(ore(&b, &["log"])).good("log"))); |
| 1070 | assert!(told.contains("before-the-announcement"), |
| 1071 | "the mark A named did not survive the hop: {}", told); |
| 1072 | assert!(told.contains('+'), |
| 1073 | "the operations arrived unverified, so the signature did not survive the \ |
| 1074 | veil: {}", told); |
| 1075 | |
| 1076 | // B writes back, veiled in its turn, and A takes it. |
| 1077 | res!(write(&b, "reply.md", b"agreed, and the lawyers are told\n")); |
| 1078 | res!(res!(ore(&b, &["mark", "answered"])).good("mark")); |
| 1079 | res!(synced(&b, &url)); |
| 1080 | res!(synced(&a, &url)); |
| 1081 | assert_eq!(res!(std::fs::read(a.join("reply.md"))), b"agreed, and the lawyers are told\n".to_vec()); |
| 1082 | assert_eq!(res!(history(&a)), res!(history(&b)), |
| 1083 | "the two replicas hold different histories"); |
| 1084 | |
| 1085 | // The relay still holds none of it, the second replica's work included. |
| 1086 | let held = res!(relay.stored("oxedyne", "ore")); |
| 1087 | for hidden in [ |
| 1088 | &secret[..], |
| 1089 | b"agreed, and the lawyers are told", |
| 1090 | b"answered", |
| 1091 | b"reply.md", |
| 1092 | ] { |
| 1093 | assert!( |
| 1094 | !held.windows(hidden.len()).any(|w| w == hidden), |
| 1095 | "the relay's {} bytes on disk contain {:?} after the second exchange", |
| 1096 | held.len(), String::from_utf8_lossy(hidden), |
| 1097 | ); |
| 1098 | } |
| 1099 | Ok(()) |
| 1100 | } |
| 1101 | |
| 1102 | /// A replica that is not given the content key is refused by name, and absorbs |
| 1103 | /// nothing. |
| 1104 | /// |
| 1105 | /// The relay serves it happily, because a relay cannot tell one client from |
| 1106 | /// another and holds no key to check anything with. Everything that keeps the |
| 1107 | /// repository private is at the reader's end, and this is what that end says when |
| 1108 | /// it is not one of the readers. |
| 1109 | #[test] |
| 1110 | fn a_replica_without_the_content_key_is_refused_by_name() -> Outcome<()> { |
| 1111 | let scratch = res!(Scratch::new("relay_no_key")); |
| 1112 | let data = res!(scratch.sub("data")); |
| 1113 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1114 | let (c, key_c) = res!(replica(&scratch, "c")); |
| 1115 | let data_arg = fmt!("{}", data.display()); |
| 1116 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1117 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_c, "pull", "--data", &data_arg])); |
| 1118 | let relay = res!(Relay::start(&data)); |
| 1119 | let url = relay.url("oxedyne", "ore"); |
| 1120 | |
| 1121 | res!(res!(ore(&a, &["key", "--veil"])).good("key --veil")); |
| 1122 | res!(write(&a, "board-minutes.md", b"the merger closes on Friday\n")); |
| 1123 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 1124 | res!(synced(&a, &url)); |
| 1125 | |
| 1126 | let out = res!(pull_only(&c, &url)); |
| 1127 | assert!(!out.ok, "a replica with no content key read a veiled repository"); |
| 1128 | // The whole phrase, and not the word "veiled" on its own. The engine says |
| 1129 | // that much when anything asks a veiled entry what operation it is, so an |
| 1130 | // assertion on the word alone would hold whether or not this repository ever |
| 1131 | // worked out that the key is what it is missing. |
| 1132 | assert!(out.err.contains("is veiled, and this repository holds no content key"), |
| 1133 | "the refusal does not say what is missing: {}", out.err); |
| 1134 | assert!(out.err.contains("ore key --veil"), |
| 1135 | "nor what would answer it: {}", out.err); |
| 1136 | assert!(out.err.contains("The operation r"), |
| 1137 | "nor which operation it met: {}", out.err); |
| 1138 | // Nothing was taken. The working copy is as it was, and so is the log. |
| 1139 | assert!(!c.join("board-minutes.md").exists(), |
| 1140 | "a repository that cannot read the history wrote a file out of it"); |
| 1141 | Ok(()) |
| 1142 | } |
| 1143 | |
| 1144 | /// A second replica is let into a veiled repository without the content key ever |
| 1145 | /// being read out loud, and the relay carries the wrap that does it. |
| 1146 | /// |
| 1147 | /// The story end to end, in the order a person would live it. B cannot read the |
| 1148 | /// repository, publishes a veil key, and is refused by name. A learns that key, |
| 1149 | /// wraps the content key to it and pushes the wrap. B syncs again and reads |
| 1150 | /// every byte. Nothing crossed but the relay, and the relay still holds nothing. |
| 1151 | /// |
| 1152 | /// The assertion that carries the claim is not that B ends up reading -- it is |
| 1153 | /// that the relay's own bytes hold neither the content nor the content key while |
| 1154 | /// B does. A relay that had quietly kept the key would pass every other check |
| 1155 | /// here. |
| 1156 | #[test] |
| 1157 | fn a_wrap_lets_a_second_replica_in_without_the_key_being_said_aloud() -> Outcome<()> { |
| 1158 | let scratch = res!(Scratch::new("relay_wrap")); |
| 1159 | let data = res!(scratch.sub("data")); |
| 1160 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1161 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1162 | let data_arg = fmt!("{}", data.display()); |
| 1163 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1164 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1165 | let relay = res!(Relay::start(&data)); |
| 1166 | let url = relay.url("oxedyne", "ore"); |
| 1167 | |
| 1168 | // A veils and pushes. The content key is minted here and is never printed to |
| 1169 | // anybody: this test never runs `ore key --veil <key>`, which is the whole |
| 1170 | // point of it. |
| 1171 | let said = fmt!("{}", res!(res!(ore(&a, &["key", "--veil"])).good("key --veil"))); |
| 1172 | assert!(said.contains("this repository is now veiled"), "{}", said); |
| 1173 | let content_key = match said.lines().find(|l| l.starts_with(" ")) { |
| 1174 | Some(l) => fmt!("{}", l.trim()), |
| 1175 | None => return Err(err!( |
| 1176 | "`ore key --veil` printed no key: {}", said; Test, Missing)), |
| 1177 | }; |
| 1178 | let secret: &[u8] = b"the merger closes on Friday at eleven\n"; |
| 1179 | res!(write(&a, "board-minutes.md", secret)); |
| 1180 | res!(res!(ore(&a, &["mark", "before-the-announcement"])).good("mark")); |
| 1181 | res!(synced(&a, &url)); |
| 1182 | |
| 1183 | // B mints a veil key and says which replica it belongs to. |
| 1184 | let said = fmt!("{}", res!(res!(ore(&b, &["key", "--veil-key"])).good("key --veil-key"))); |
| 1185 | assert!(said.contains("minted a veil key with X25519"), "{}", said); |
| 1186 | let replica_b = match said.split("for replica ").nth(1) { |
| 1187 | Some(rest) => fmt!("{}", rest.lines().next().unwrap_or("").trim()), |
| 1188 | None => return Err(err!( |
| 1189 | "`ore key --veil-key` did not say which replica: {}", said; Test, Missing)), |
| 1190 | }; |
| 1191 | assert!(b.join(".ore").join("veilkey").is_file(), "no veil key was written"); |
| 1192 | assert!(!b.join(".ore").join("veil").is_file(), |
| 1193 | "a veil key is not a content key, and minting one gave this replica one"); |
| 1194 | |
| 1195 | // B syncs, and is refused by name: the veil binding is deposited before the |
| 1196 | // operations cross, so publishing it costs nothing even though the sync then |
| 1197 | // fails on the first thing it cannot read. |
| 1198 | let out = res!(sync(&b, &url)); |
| 1199 | assert!(!out.ok, "a replica with no content key read a veiled repository"); |
| 1200 | assert!(out.err.contains("is veiled, and this repository holds no content key"), |
| 1201 | "the refusal does not say what is missing: {}", out.err); |
| 1202 | assert!(!b.join("board-minutes.md").exists(), |
| 1203 | "a replica that cannot read the history wrote a file out of it"); |
| 1204 | |
| 1205 | // A learns the veil key, and cannot wrap to a replica it has not met. |
| 1206 | let out = res!(ore(&a, &["key", "--wrap", &replica_b])); |
| 1207 | assert!(!out.ok, "A wrapped to a replica whose veil key it had never seen"); |
| 1208 | assert!(out.err.contains("knows no veil key for replica"), |
| 1209 | "the refusal does not say what is missing: {}", out.err); |
| 1210 | res!(synced(&a, &url)); |
| 1211 | let said = fmt!("{}", res!(res!(ore(&a, &["key", "--wrap", &replica_b])) |
| 1212 | .good("key --wrap"))); |
| 1213 | assert!(said.contains(&fmt!("content key for replica {}", replica_b.trim_start_matches('r'))), |
| 1214 | "the wrap was not made for the replica that asked: {}", said); |
| 1215 | res!(synced(&a, &url)); |
| 1216 | |
| 1217 | // The wrap is on the relay, and it carries neither the content key nor the |
| 1218 | // content. This is the sentence the design rests on made testable: the relay |
| 1219 | // may hand the wrap to anybody, because the wrap says nothing. |
| 1220 | let carried = res!(std::fs::read(data.join("oxedyne").join("ore").join("wraps"))); |
| 1221 | assert!(!carried.is_empty(), "the relay kept no wrap"); |
| 1222 | for hidden in [content_key.as_bytes(), secret] { |
| 1223 | assert!( |
| 1224 | !carried.windows(hidden.len()).any(|w| w == hidden), |
| 1225 | "the relay\'s wrap file holds {:?} in clear", |
| 1226 | String::from_utf8_lossy(hidden), |
| 1227 | ); |
| 1228 | } |
| 1229 | |
| 1230 | // B syncs again, takes the wrap, and reads the lot. |
| 1231 | let said = res!(synced(&b, &url)); |
| 1232 | assert!(said.contains("a wrap addressed to this replica was opened"), |
| 1233 | "the sync did not say it had been let in: {}", said); |
| 1234 | assert!(b.join(".ore").join("veil").is_file(), |
| 1235 | "the content key was not written where the next command will find it"); |
| 1236 | assert_eq!(res!(std::fs::read(b.join("board-minutes.md"))), secret.to_vec(), |
| 1237 | "the replica that was let in did not get the content"); |
| 1238 | let told = fmt!("{}", res!(res!(ore(&b, &["log"])).good("log"))); |
| 1239 | assert!(told.contains("before-the-announcement"), |
| 1240 | "the mark A named did not survive the hop: {}", told); |
| 1241 | assert!(told.contains('+'), |
| 1242 | "the operations arrived unverified, so the signature did not survive the \ |
| 1243 | veil: {}", told); |
| 1244 | |
| 1245 | // B is a full member from here: it writes back veiled under the same key and |
| 1246 | // A reads it. |
| 1247 | res!(write(&b, "reply.md", b"agreed, and the lawyers are told\n")); |
| 1248 | res!(res!(ore(&b, &["mark", "answered"])).good("mark")); |
| 1249 | res!(synced(&b, &url)); |
| 1250 | res!(synced(&a, &url)); |
| 1251 | assert_eq!(res!(std::fs::read(a.join("reply.md"))), |
| 1252 | b"agreed, and the lawyers are told\n".to_vec()); |
| 1253 | |
| 1254 | // And the relay still holds none of it. |
| 1255 | let held = res!(relay.stored("oxedyne", "ore")); |
| 1256 | for hidden in [ |
| 1257 | &secret[..], |
| 1258 | b"board-minutes.md", |
| 1259 | b"before-the-announcement", |
| 1260 | b"agreed, and the lawyers are told", |
| 1261 | content_key.as_bytes(), |
| 1262 | ] { |
| 1263 | assert!( |
| 1264 | !held.windows(hidden.len()).any(|w| w == hidden), |
| 1265 | "the relay\'s {} bytes on disk contain {:?}", |
| 1266 | held.len(), String::from_utf8_lossy(hidden), |
| 1267 | ); |
| 1268 | } |
| 1269 | Ok(()) |
| 1270 | } |
| 1271 | |
| 1272 | /// A veil binding the relay has been edited to change is dropped, so nobody |
| 1273 | /// wraps a content key to a key the relay put there. |
| 1274 | /// |
| 1275 | /// The relay cannot mint a reading key any more than it can mint a signing key. |
| 1276 | /// What is exercised here is a real relay whose file on disk has been altered |
| 1277 | /// between one sync and the next: the binding stops holding, both ends drop it, |
| 1278 | /// and the owner is told it knows no veil key for that replica rather than |
| 1279 | /// quietly wrapping to the relay\'s choice. The second link on its own -- a |
| 1280 | /// binding perfectly signed by a key that is not the replica\'s -- cannot be |
| 1281 | /// produced through the tool at all, and is proved in `ore_store`. |
| 1282 | #[test] |
| 1283 | fn a_veil_binding_the_relay_has_altered_is_dropped() -> Outcome<()> { |
| 1284 | let scratch = res!(Scratch::new("relay_veil_chain")); |
| 1285 | let data = res!(scratch.sub("data")); |
| 1286 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1287 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1288 | let data_arg = fmt!("{}", data.display()); |
| 1289 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1290 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1291 | let relay = res!(Relay::start(&data)); |
| 1292 | let url = relay.url("oxedyne", "ore"); |
| 1293 | |
| 1294 | res!(res!(ore(&a, &["key", "--veil"])).good("key --veil")); |
| 1295 | res!(write(&a, "board-minutes.md", b"the merger closes on Friday\n")); |
| 1296 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 1297 | res!(synced(&a, &url)); |
| 1298 | |
| 1299 | let said = fmt!("{}", res!(res!(ore(&b, &["key", "--veil-key"])).good("key --veil-key"))); |
| 1300 | let replica_b = match said.split("for replica ").nth(1) { |
| 1301 | Some(rest) => fmt!("{}", rest.lines().next().unwrap_or("").trim()), |
| 1302 | None => return Err(err!( |
| 1303 | "`ore key --veil-key` did not say which replica: {}", said; Test, Missing)), |
| 1304 | }; |
| 1305 | // The sync fails on the first veiled operation it cannot read, and deposits |
| 1306 | // the binding on the way past. |
| 1307 | let out = res!(sync(&b, &url)); |
| 1308 | assert!(!out.ok, "a replica with no content key read a veiled repository"); |
| 1309 | let kept = data.join("oxedyne").join("ore").join("veils"); |
| 1310 | let held = fmt!("{}", String::from_utf8_lossy(&res!(std::fs::read(&kept)))); |
| 1311 | assert!(held.contains(&key_b), |
| 1312 | "the veil binding was not kept beside the signing key that vouched for it: {}", |
| 1313 | held); |
| 1314 | |
| 1315 | // Altered on the relay\'s own disk: the signature stops covering what the |
| 1316 | // binding says, so it stops holding. |
| 1317 | let (_, key_c) = res!(replica(&scratch, "c")); |
| 1318 | res!(std::fs::write(&kept, held.replace(&key_b, &key_c))); |
| 1319 | res!(synced(&a, &url)); |
| 1320 | let out = res!(ore(&a, &["key", "--wrap", &replica_b])); |
| 1321 | assert!(!out.ok, "a content key was wrapped to a veil key the relay had altered"); |
| 1322 | assert!(out.err.contains("knows no veil key for replica"), |
| 1323 | "the refusal does not say why: {}", out.err); |
| 1324 | |
| 1325 | // Put back, and the same command works, so what was refused was the alteration |
| 1326 | // and not the route. |
| 1327 | res!(std::fs::write(&kept, &held)); |
| 1328 | res!(synced(&a, &url)); |
| 1329 | let said = fmt!("{}", res!(res!(ore(&a, &["key", "--wrap", &replica_b])) |
| 1330 | .good("key --wrap"))); |
| 1331 | assert!(said.contains("content key for replica"), |
| 1332 | "the unaltered binding was refused too: {}", said); |
| 1333 | Ok(()) |
| 1334 | } |
| 1335 | |
| 1336 | /// The relay does the whole of its job on a repository it cannot read: the |
| 1337 | /// frontier walk, the sketch, and a genuine divergence converging. |
| 1338 | /// |
| 1339 | /// Blindness is not much of a claim if it costs the service. So this is the |
| 1340 | /// sketch test run veiled, with a divergence in the middle: two replicas write |
| 1341 | /// concurrently over a large shared history, each visits the relay once, and both |
| 1342 | /// end holding the same frontier -- while the relay's disk holds none of the |
| 1343 | /// sixty file names it reconciled. |
| 1344 | #[test] |
| 1345 | fn a_veiled_repository_still_walks_sketches_and_converges() -> Outcome<()> { |
| 1346 | let scratch = res!(Scratch::new("relay_veiled_sketch")); |
| 1347 | let data = res!(scratch.sub("data")); |
| 1348 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1349 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1350 | let data_arg = fmt!("{}", data.display()); |
| 1351 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1352 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1353 | let relay = res!(Relay::start(&data)); |
| 1354 | let url = relay.url("oxedyne", "ore"); |
| 1355 | |
| 1356 | let said = fmt!("{}", res!(res!(ore(&a, &["key", "--veil"])).good("key --veil"))); |
| 1357 | let content_key = match said.lines().find(|l| l.starts_with(" ")) { |
| 1358 | Some(l) => fmt!("{}", l.trim()), |
| 1359 | None => return Err(err!("`ore key --veil` printed no key: {}", said; Test, Missing)), |
| 1360 | }; |
| 1361 | res!(res!(ore(&b, &["key", "--veil", &content_key])).good("key --veil <key>")); |
| 1362 | |
| 1363 | for i in 0..60 { |
| 1364 | res!(write(&a, &fmt!("f{:02}.txt", i), fmt!("file number {}\n", i).as_bytes())); |
| 1365 | } |
| 1366 | res!(res!(ore(&a, &["mark", "base"])).good("mark")); |
| 1367 | // The walk carries the whole history up and down, veiled. |
| 1368 | let said = res!(synced(&a, &url)); |
| 1369 | assert!(said.contains("mode frontier walk"), |
| 1370 | "a clone is what the walk is for: {}", said); |
| 1371 | assert!(said.contains("sent 121 operations"), |
| 1372 | "the whole history goes up veiled: {}", said); |
| 1373 | let said = res!(synced(&b, &url)); |
| 1374 | assert!(said.contains("received 121 operations"), |
| 1375 | "and comes down again: {}", said); |
| 1376 | |
| 1377 | // Now a real divergence: both write before either visits. |
| 1378 | res!(write(&a, "f00.txt", b"file number 0\na tail from A\n")); |
| 1379 | res!(write(&b, "f01.txt", b"file number 1\na tail from B\n")); |
| 1380 | let said = res!(synced(&a, &url)); |
| 1381 | assert!(said.contains("mode sketch"), |
| 1382 | "a small difference over a large history is what the sketch is for, and \ |
| 1383 | veiling does not change the shape it is sized from: {}", said); |
| 1384 | assert!(!said.contains("fell back"), |
| 1385 | "and the estimate held, so the sketch reconciled entries the relay could \ |
| 1386 | not read: {}", said); |
| 1387 | let said = res!(synced(&b, &url)); |
| 1388 | assert!(said.contains("received "), "B takes A's branch: {}", said); |
| 1389 | res!(synced(&a, &url)); |
| 1390 | assert_eq!(res!(history(&a)), res!(history(&b)), |
| 1391 | "the divergence did not converge through a relay that could not read it"); |
| 1392 | |
| 1393 | // Both working copies hold both branches. |
| 1394 | assert_eq!(res!(std::fs::read(a.join("f01.txt"))), b"file number 1\na tail from B\n".to_vec()); |
| 1395 | assert_eq!(res!(std::fs::read(b.join("f00.txt"))), b"file number 0\na tail from A\n".to_vec()); |
| 1396 | |
| 1397 | // And the relay reconciled all of it without ever holding a file name. |
| 1398 | let held = res!(relay.stored("oxedyne", "ore")); |
| 1399 | for i in 0..60 { |
| 1400 | let name = fmt!("f{:02}.txt", i); |
| 1401 | assert!( |
| 1402 | !held.windows(name.len()).any(|w| w == name.as_bytes()), |
| 1403 | "the relay's disk holds the file name {:?}", name, |
| 1404 | ); |
| 1405 | } |
| 1406 | for hidden in [&b"a tail from A"[..], b"a tail from B", b"base"] { |
| 1407 | assert!( |
| 1408 | !held.windows(hidden.len()).any(|w| w == hidden), |
| 1409 | "the relay's disk holds {:?}", String::from_utf8_lossy(hidden), |
| 1410 | ); |
| 1411 | } |
| 1412 | Ok(()) |
| 1413 | } |
| 1414 | |
| 1415 | /// A clone larger than one reply arrives whole, across several sessions, and the |
| 1416 | /// summary never calls a partial exchange a finished one. |
| 1417 | /// |
| 1418 | /// The failure this guards is not a refusal. Before 2026-08-20 `serve.rs` framed |
| 1419 | /// the entire reply as one body, so a 58 MB clone had to be materialised whole in |
| 1420 | /// the receiving process. Proxies cap requests and not responses, so nothing in |
| 1421 | /// front of the relay would ever have refused it; it dies on the receiving end, |
| 1422 | /// where a phone or a small VPS has nothing to raise. |
| 1423 | /// |
| 1424 | /// This used to go on to say the clone was held "at about six times its size -- |
| 1425 | /// measured at 347,532 kB for 44,541 operations", as though the framing were what |
| 1426 | /// cost that. It is not: the same clone across fourteen bounded replies peaked at |
| 1427 | /// 346,612 kB, under one percent lower. The multiple is the engine's per-operation |
| 1428 | /// cost of holding a history, and `ore log` pays it over no network at all. The |
| 1429 | /// bound is still worth testing, for the whole-body materialisation above. |
| 1430 | /// |
| 1431 | /// The relay here is bounded to a few hundred bytes so the boundary is reached in |
| 1432 | /// a test rather than in a megabyte-scale fixture. The boundary is the same one. |
| 1433 | #[test] |
| 1434 | fn a_clone_larger_than_one_reply_still_arrives_whole() -> Outcome<()> { |
| 1435 | let scratch = res!(Scratch::new("relay_bounded")); |
| 1436 | let data = res!(scratch.sub("data")); |
| 1437 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1438 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1439 | let data_arg = fmt!("{}", data.display()); |
| 1440 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1441 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1442 | |
| 1443 | // A pushes a history of some size against a relay with ordinary bounds, so |
| 1444 | // that what is being tested is the pull and not the push. |
| 1445 | let plain = res!(Relay::start(&data)); |
| 1446 | let url = plain.url("oxedyne", "ore"); |
| 1447 | for i in 0..12 { |
| 1448 | res!(write(&a, &fmt!("f{}.txt", i), fmt!("{}\n", "content line\n".repeat(40)).as_bytes())); |
| 1449 | res!(res!(ore(&a, &["mark", &fmt!("m{}", i)])).good("mark")); |
| 1450 | } |
| 1451 | res!(synced(&a, &url)); |
| 1452 | let (there, frontier_a) = res!(history(&a)); |
| 1453 | drop(plain); |
| 1454 | |
| 1455 | // The same data, served by a relay that will not answer with more than a few |
| 1456 | // hundred bytes at a time. |
| 1457 | let relay = res!(Relay::start_bounded(&data, 400)); |
| 1458 | let url = relay.url("oxedyne", "ore"); |
| 1459 | let said = res!(synced(&b, &url)); |
| 1460 | |
| 1461 | // It took more than one session, or the bound was not reached and this test |
| 1462 | // proves nothing about the thing it is named for. |
| 1463 | assert!(!said.contains("over 1 session,"), |
| 1464 | "the reply bound was never reached, so nothing here was exercised: {}", said); |
| 1465 | // And it finished anyway. |
| 1466 | assert!(!said.contains("UNFINISHED"), |
| 1467 | "the clone stopped short of the history: {}", said); |
| 1468 | // A fresh replica keeps nothing back, however many sessions it took to fill. |
| 1469 | // The count is a fact about the state the exchange began in, and this one began |
| 1470 | // empty; summed across sessions instead it reads as hundreds of thousands of |
| 1471 | // operations withheld by a repository that had none. |
| 1472 | let (c, key_c) = res!(replica(&scratch, "c")); |
| 1473 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_c, "push", "--data", &data_arg])); |
| 1474 | let told = res!(pull_only(&c, &url)); |
| 1475 | let told = fmt!("{}", res!(told.good("sync --pull-only"))); |
| 1476 | assert!(!told.contains("kept back"), |
| 1477 | "a replica that began empty reported withholding its own work: {}", told); |
| 1478 | |
| 1479 | let (here, frontier_b) = res!(history(&b)); |
| 1480 | assert_eq!(here, there, "the clone holds a different number of operations"); |
| 1481 | assert_eq!(frontier_b, frontier_a, "the clone holds a different history"); |
| 1482 | for i in 0..12 { |
| 1483 | assert!(b.join(fmt!("f{}.txt", i)).is_file(), "f{}.txt did not arrive", i); |
| 1484 | } |
| 1485 | Ok(()) |
| 1486 | } |
| 1487 | |
| 1488 | |
| 1489 | /// An operation larger than one request body crosses, and comes back byte for |
| 1490 | /// byte. |
| 1491 | /// |
| 1492 | /// **The defect this exists for, at the size a test can afford.** fe2o3's git |
| 1493 | /// history holds `fe2o3_steel/steel`, a 22,153,680 byte compiled binary added in |
| 1494 | /// `7b6d603` and deleted in `d842b87`. The import reads git history, so it is one |
| 1495 | /// operation in the Ore log, and one operation was the smallest thing the |
| 1496 | /// transport had: `split` bounded a `Send` at four mebibytes of entries and |
| 1497 | /// always kept at least one entry, `groups` always kept at least one message, so |
| 1498 | /// it became a 22,153,955 byte request body. Steel's `http_max_body_bytes` |
| 1499 | /// defaults to 8,388,608 and the deployed forge sets no override, so the proxy |
| 1500 | /// closed the connection part way through the body and the client saw `Broken |
| 1501 | /// pipe`. The push only ever succeeded through an `ssh -L` tunnel to the relay's |
| 1502 | /// loopback port, which is not a deployment. |
| 1503 | /// |
| 1504 | /// Raising the limit was refused, and rightly: it carries this operation and not |
| 1505 | /// the next one. The operation crosses in pieces instead. |
| 1506 | /// |
| 1507 | /// The relay here is capped at 64 kB rather than eight mebibytes so the boundary |
| 1508 | /// is reached in a test rather than in a megabyte-scale fixture, and it is |
| 1509 | /// enforced at the relay rather than by a proxy, which is a better refusal than |
| 1510 | /// the one the deployment gives. |
| 1511 | #[test] |
| 1512 | fn an_operation_larger_than_one_request_body_crosses_and_comes_back() -> Outcome<()> { |
| 1513 | let scratch = res!(Scratch::new("relay_oversize")); |
| 1514 | let data = res!(scratch.sub("data")); |
| 1515 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1516 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1517 | let data_arg = fmt!("{}", data.display()); |
| 1518 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1519 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1520 | |
| 1521 | // One file whose contents are one splice, several times what the relay will |
| 1522 | // take in a request body. The bytes vary, so a compressing carrier could not |
| 1523 | // make this fit and pass by accident. |
| 1524 | let mut big = Vec::with_capacity(400_000); |
| 1525 | let mut x: u32 = 0x1234_5678; |
| 1526 | while big.len() < 400_000 { |
| 1527 | x = x.wrapping_mul(1_664_525).wrapping_add(1_013_904_223); |
| 1528 | big.extend_from_slice(&x.to_le_bytes()); |
| 1529 | } |
| 1530 | res!(write(&a, "steel", &big)); |
| 1531 | |
| 1532 | let relay = res!(Relay::start_capped(&data, 64 << 10)); |
| 1533 | let url = relay.url("oxedyne", "ore"); |
| 1534 | let said = res!(synced(&a, &url)); |
| 1535 | assert!(said.contains("pieces "), |
| 1536 | "the operation did not cross in pieces, so this test proves nothing: {}", said); |
| 1537 | assert!(!said.contains("UNFINISHED"), "the push stopped short: {}", said); |
| 1538 | let (there, frontier_a) = res!(history(&a)); |
| 1539 | |
| 1540 | // And a replica that has never met the first one clones it back. |
| 1541 | let said = res!(synced(&b, &url)); |
| 1542 | assert!(!said.contains("UNFINISHED"), "the clone stopped short: {}", said); |
| 1543 | let got = match std::fs::read(b.join("steel")) { |
| 1544 | Ok(g) => g, |
| 1545 | Err(e) => return Err(err!(e, |
| 1546 | "The large file did not arrive at the second replica."; Test, IO)), |
| 1547 | }; |
| 1548 | assert_eq!(got.len(), big.len(), "the file that came back is a different length"); |
| 1549 | assert_eq!(got, big, "the file that came back is not the file that went"); |
| 1550 | |
| 1551 | let (here, frontier_b) = res!(history(&b)); |
| 1552 | assert_eq!(here, there, "the clone holds a different number of operations"); |
| 1553 | assert_eq!(frontier_b, frontier_a, "the clone holds a different history"); |
| 1554 | Ok(()) |
| 1555 | } |
| 1556 | |
| 1557 | /// A relay refuses a body over what it published, and says both numbers. |
| 1558 | /// |
| 1559 | /// The half that was missing until 2026-08-22. A relay published a request limit |
| 1560 | /// compiled into it and enforced nothing, so a client that ignored the number met |
| 1561 | /// the proxy instead -- and a proxy closes the connection part way through the |
| 1562 | /// body, which reads as the relay being down rather than as a limit being |
| 1563 | /// reached. |
| 1564 | #[test] |
| 1565 | fn a_relay_refuses_a_body_over_what_it_published() -> Outcome<()> { |
| 1566 | let scratch = res!(Scratch::new("relay_postcap")); |
| 1567 | let data = res!(scratch.sub("data")); |
| 1568 | let (_a, key_a) = res!(replica(&scratch, "a")); |
| 1569 | let data_arg = fmt!("{}", data.display()); |
| 1570 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1571 | |
| 1572 | let relay = res!(Relay::start_capped(&data, 4_096)); |
| 1573 | // What it says it takes. |
| 1574 | let said = res!(relay.unsigned_get("/ore")); |
| 1575 | assert!(said.contains("4096"), |
| 1576 | "the relay does not publish the limit it was given: {}", said); |
| 1577 | // And what it does about a body over it. |
| 1578 | let answer = res!(relay.unsigned_post("/ore/v1/oxedyne/ore/sync", &vec![0u8; 8_192])); |
| 1579 | assert!(answer.contains("413"), "an oversized body was not refused: {}", answer); |
| 1580 | assert!(answer.contains("8192") && answer.contains("4096"), |
| 1581 | "the refusal does not name both numbers: {}", answer); |
| 1582 | Ok(()) |
| 1583 | } |
| 1584 | |
| 1585 | |
| 1586 | /// A clone offers back nothing it was given, however many sessions it takes. |
| 1587 | /// |
| 1588 | /// **The defect this exists for.** A relay bounds its reply, so a clone larger |
| 1589 | /// than the bound is a run of sessions, and a session worked out what it owed |
| 1590 | /// from the relay's frontier alone. Nothing in a frontier says "you handed me |
| 1591 | /// this": the relay's heads are operations the puller has not reached yet, so |
| 1592 | /// they subtract nothing, and every session offered back the whole prefix the |
| 1593 | /// sessions before it had just delivered. |
| 1594 | /// |
| 1595 | /// Measured on the clone of fe2o3 of 12026-08-22 -- 35,314 operations, sixteen |
| 1596 | /// sessions -- 166,224 operations were offered, every one of them already at the |
| 1597 | /// relay: **717,611,365 bytes up to take 87,505,248 down**, eight to one, on a |
| 1598 | /// replica that had authored nothing at all. `--pull-only` cost 1,197 bytes up |
| 1599 | /// over the same clone, because it suppresses the offer rather than computing a |
| 1600 | /// smaller one. |
| 1601 | /// |
| 1602 | /// The assertion is the property and not a threshold: a clone that uploads less |
| 1603 | /// than it downloads did not offer its own log back even once. |
| 1604 | #[test] |
| 1605 | fn a_bounded_clone_does_not_offer_back_what_it_was_given() -> Outcome<()> { |
| 1606 | let scratch = res!(Scratch::new("relay_reoffer")); |
| 1607 | let data = res!(scratch.sub("data")); |
| 1608 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1609 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1610 | let data_arg = fmt!("{}", data.display()); |
| 1611 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1612 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1613 | |
| 1614 | // A history of some size, pushed against ordinary bounds so that what is under |
| 1615 | // test is the pull. |
| 1616 | let plain = res!(Relay::start(&data)); |
| 1617 | let url = plain.url("oxedyne", "ore"); |
| 1618 | for i in 0..12 { |
| 1619 | res!(write(&a, &fmt!("f{}.txt", i), |
| 1620 | fmt!("{}", "content line\n".repeat(40)).as_bytes())); |
| 1621 | res!(res!(ore(&a, &["mark", &fmt!("m{}", i)])).good("mark")); |
| 1622 | } |
| 1623 | res!(synced(&a, &url)); |
| 1624 | let (there, frontier_a) = res!(history(&a)); |
| 1625 | drop(plain); |
| 1626 | |
| 1627 | // The same history, from a relay that will not answer with more than a few |
| 1628 | // hundred bytes at a time, so the clone is a run of sessions. |
| 1629 | let relay = res!(Relay::start_bounded(&data, 400)); |
| 1630 | let url = relay.url("oxedyne", "ore"); |
| 1631 | let said = res!(synced(&b, &url)); |
| 1632 | assert!(!said.contains("over 1 session,"), |
| 1633 | "the reply bound was never reached, so nothing here was exercised: {}", said); |
| 1634 | assert!(!said.contains("UNFINISHED"), |
| 1635 | "the clone stopped short of the history: {}", said); |
| 1636 | |
| 1637 | let (up, down) = res!(traffic(&said)); |
| 1638 | assert!(up < down, |
| 1639 | "a clone that authored nothing sent {} bytes to take {} -- it is offering \ |
| 1640 | back what it was given: {}", up, down, said); |
| 1641 | |
| 1642 | // And it is a clone: the history arrived whole. |
| 1643 | let (here, frontier_b) = res!(history(&b)); |
| 1644 | assert_eq!(here, there, "the clone holds a different number of operations"); |
| 1645 | assert_eq!(frontier_b, frontier_a, "the clone holds a different history"); |
| 1646 | Ok(()) |
| 1647 | } |
| 1648 | |
| 1649 | /// And a replica that has written of its own still hands over every one of them, |
| 1650 | /// however the exchange is cut up. |
| 1651 | /// |
| 1652 | /// The other side of the same change, and the one that matters more: what a |
| 1653 | /// session remembers is subtracted from what it offers, so a belief that is too |
| 1654 | /// generous does not cost bytes -- it silently fails to deliver somebody's work. |
| 1655 | /// The relay is bounded here so the exchange takes several sessions, which is |
| 1656 | /// exactly when the remembering happens. |
| 1657 | #[test] |
| 1658 | fn a_bounded_exchange_hands_over_every_operation() -> Outcome<()> { |
| 1659 | let scratch = res!(Scratch::new("relay_reoffer_both")); |
| 1660 | let data = res!(scratch.sub("data")); |
| 1661 | let (a, key_a) = res!(replica(&scratch, "a")); |
| 1662 | let (b, key_b) = res!(replica(&scratch, "b")); |
| 1663 | let data_arg = fmt!("{}", data.display()); |
| 1664 | res!(relay_cmd(&["create", "oxedyne/ore", "--owner", &key_a, "--data", &data_arg])); |
| 1665 | res!(relay_cmd(&["grant", "oxedyne/ore", &key_b, "push", "--data", &data_arg])); |
| 1666 | |
| 1667 | let plain = res!(Relay::start(&data)); |
| 1668 | let url = plain.url("oxedyne", "ore"); |
| 1669 | for i in 0..12 { |
| 1670 | res!(write(&a, &fmt!("f{}.txt", i), |
| 1671 | fmt!("{}", "content line\n".repeat(40)).as_bytes())); |
| 1672 | res!(res!(ore(&a, &["mark", &fmt!("m{}", i)])).good("mark")); |
| 1673 | } |
| 1674 | res!(synced(&a, &url)); |
| 1675 | drop(plain); |
| 1676 | |
| 1677 | // B has never met the relay and has written four files of its own, so it is |
| 1678 | // behind on everything and ahead on something at the same time. |
| 1679 | for i in 0..4 { |
| 1680 | res!(write(&b, &fmt!("mine{}.txt", i), |
| 1681 | fmt!("{}", "b wrote this\n".repeat(40)).as_bytes())); |
| 1682 | res!(res!(ore(&b, &["mark", &fmt!("b{}", i)])).good("mark")); |
| 1683 | } |
| 1684 | let (mine, _) = res!(history(&b)); |
| 1685 | |
| 1686 | let relay = res!(Relay::start_bounded(&data, 400)); |
| 1687 | let url = relay.url("oxedyne", "ore"); |
| 1688 | let said = res!(synced(&b, &url)); |
| 1689 | assert!(!said.contains("over 1 session,"), |
| 1690 | "the reply bound was never reached, so nothing here was exercised: {}", said); |
| 1691 | assert!(!said.contains("UNFINISHED"), |
| 1692 | "the exchange stopped short: {}", said); |
| 1693 | |
| 1694 | // A visits again and finds B's work waiting, whole. The relay is asked nothing |
| 1695 | // about itself: this is read off a third replica's rendered files. |
| 1696 | res!(synced(&a, &url)); |
| 1697 | for i in 0..4 { |
| 1698 | let at = a.join(fmt!("mine{}.txt", i)); |
| 1699 | assert!(at.is_file(), "mine{}.txt never reached the relay: {}", i, said); |
| 1700 | assert_eq!(res!(std::fs::read(&at)), fmt!("{}", "b wrote this\n".repeat(40)).into_bytes(), |
| 1701 | "mine{}.txt arrived changed", i); |
| 1702 | } |
| 1703 | let (theirs, frontier_a) = res!(history(&a)); |
| 1704 | let (here, frontier_b) = res!(history(&b)); |
| 1705 | assert!(theirs >= mine, "the relay lost operations B had already written"); |
| 1706 | assert_eq!(frontier_a, frontier_b, "the two replicas hold different histories"); |
| 1707 | assert_eq!(theirs, here, "the two replicas hold different numbers of operations"); |
| 1708 | Ok(()) |
| 1709 | } |