| 1 | |
|
| 2 | |
|
| 3 | |
|
| 4 | |
|
| 5 | |
|
| 6 | |
|
| 7 | |
|
| 8 | |
|
| 9 | |
|
| 10 | |
|
| 11 | |
|
| 12 | |
|
| 13 | |
|
| 14 | |
|
| 15 | |
|
| 16 | |
|
| 17 | |
|
| 18 | |
|
| 19 | | use crate::{db, Shared}; |
| 20 | | use axum::extract::{Path as UrlPath, State}; |
| 21 | | use axum::http::{header, StatusCode}; |
| 22 | | use axum::response::{IntoResponse, Response}; |
| 23 | | use oxy_core::hash::{KeyId, ObjectRef}; |
| 24 | | use oxy_core::identity::Identity; |
| 25 | | use oxy_core::repo::{PushReport, Repo}; |
| 26 | | use oxy_core::tlog::{self, Checkpoint, Entry, Hash, HashReader, Tile, TlogError, TILE_WIDTH}; |
| 27 | | use std::fs::{File, OpenOptions}; |
| 28 | | use std::io::Write; |
| 29 | | use std::os::unix::fs::FileExt; |
| 30 | | use std::path::{Path, PathBuf}; |
| 31 | | use std::sync::Mutex; |
| 32 | |
|
| 33 | |
|
| 34 | | pub const CHECKPOINT_EVERY: std::time::Duration = std::time::Duration::from_secs(5); |
| 35 | |
|
| 36 | | struct Files { |
| 37 | | entries: File, |
| 38 | | offsets: File, |
| 39 | | hashes: File, |
| 40 | | |
| 41 | | size: u64, |
| 42 | | |
| 43 | | entries_len: u64, |
| 44 | | } |
| 45 | |
|
| 46 | |
|
| 47 | | struct FileHashes<'a>(&'a File); |
| 48 | |
|
| 49 | | impl HashReader for FileHashes<'_> { |
| 50 | 893× | fn read_hashes(&self, indexes: &[u64]) -> Result<Vec<Hash>, TlogError> { |
| 51 | 893× | indexes |
| 52 | 893× | .iter() |
| 53 | 1206× | .map(|&i| { |
| 54 | 1206× | let mut h = [0u8; 32]; |
| 55 | 1206× | self.0.read_exact_at(&mut h, i * 32).map_err(|e| TlogError::Read(format!("stored hash {i}: {e}")))?; |
| 56 | 1206× | Ok(h) |
| 57 | 1206× | }) |
| 58 | 893× | .collect() |
| 59 | 893× | } |
| 60 | | } |
| 61 | |
|
| 62 | |
|
| 63 | | pub struct Log { |
| 64 | | dir: PathBuf, |
| 65 | | |
| 66 | | pub origin: String, |
| 67 | | files: Mutex<Files>, |
| 68 | | |
| 69 | | |
| 70 | | lock: File, |
| 71 | | |
| 72 | | checkpoint: Mutex<Option<(u64, String)>>, |
| 73 | | |
| 74 | | witnesses: std::sync::RwLock<Vec<Peer>>, |
| 75 | | |
| 76 | | witness_for: std::sync::RwLock<Vec<(String, Identity)>>, |
| 77 | | |
| 78 | | |
| 79 | | writer: bool, |
| 80 | | } |
| 81 | |
|
| 82 | |
|
| 83 | | #[derive(Clone, Debug)] |
| 84 | | pub struct Peer { |
| 85 | | pub name: String, |
| 86 | | |
| 87 | | pub url: String, |
| 88 | | pub identity: Identity, |
| 89 | | } |
| 90 | |
|
| 91 | 496× | fn read_u64_at(f: &File, at: u64) -> std::io::Result<u64> { |
| 92 | 496× | let mut b = [0u8; 8]; |
| 93 | 496× | f.read_exact_at(&mut b, at)?; |
| 94 | 496× | Ok(u64::from_le_bytes(b)) |
| 95 | 496× | } |
| 96 | |
|
| 97 | |
|
| 98 | 211× | pub fn origin_of(base_url: &str) -> String { |
| 99 | 211× | let host = base_url.split_once("://").map(|(_, h)| h).unwrap_or(base_url); |
| 100 | 211× | format!("{}/log", host.trim_end_matches('/')) |
| 101 | 211× | } |
| 102 | |
|
| 103 | | impl Log { |
| 104 | | |
| 105 | | |
| 106 | 214× | pub fn open(dir: &Path, origin: &str) -> anyhow::Result<Log> { |
| 107 | 214× | Self::open_as(dir, origin, true) |
| 108 | 214× | } |
| 109 | |
|
| 110 | | |
| 111 | | |
| 112 | | |
| 113 | | |
| 114 | 3× | pub fn open_reader(dir: &Path, origin: &str) -> anyhow::Result<Log> { |
| 115 | 3× | Self::open_as(dir, origin, false) |
| 116 | 3× | } |
| 117 | |
|
| 118 | 217× | fn open_as(dir: &Path, origin: &str, writer: bool) -> anyhow::Result<Log> { |
| 119 | 217× | std::fs::create_dir_all(dir)?; |
| 120 | 651× | let open = |name: &str| OpenOptions::new().read(true).append(true).create(true).open(dir.join(name)); |
| 121 | 217× | let (entries, offsets, hashes) = (open("entries")?, open("offsets")?, open("hashes")?); |
| 122 | 217× | let lock = OpenOptions::new().read(true).write(true).create(true).truncate(false).open(dir.join("append.lock"))?; |
| 123 | 217× | let mut files = Files { entries, offsets, hashes, size: 0, entries_len: 0 }; |
| 124 | 217× | if writer { |
| 125 | 214× | lock.lock()?; |
| 126 | 214× | let done = Self::recover(&mut files, true); |
| 127 | 214× | lock.unlock()?; |
| 128 | 214× | done?; |
| 129 | | } else { |
| 130 | 3× | Self::recover(&mut files, false)?; |
| 131 | | } |
| 132 | 217× | let size = files.size; |
| 133 | 217× | Ok(Log { |
| 134 | 217× | dir: dir.to_path_buf(), |
| 135 | 217× | origin: origin.to_string(), |
| 136 | 217× | files: Mutex::new(files), |
| 137 | 217× | lock, |
| 138 | 217× | checkpoint: Mutex::new(Self::read_checkpoint(dir, size)), |
| 139 | 217× | witnesses: Default::default(), |
| 140 | 217× | witness_for: Default::default(), |
| 141 | 217× | writer, |
| 142 | 217× | }) |
| 143 | 217× | } |
| 144 | |
|
| 145 | | |
| 146 | | |
| 147 | | |
| 148 | | |
| 149 | | |
| 150 | | |
| 151 | 893× | fn recover(files: &mut Files, writer: bool) -> anyhow::Result<()> { |
| 152 | 893× | let entries_on_disk = files.entries.metadata()?.len(); |
| 153 | 893× | let mut size = files.offsets.metadata()?.len() / 8; |
| 154 | 893× | let mut entries_len = 0; |
| 155 | 895× | while size > 0 { |
| 156 | 413× | let off = read_u64_at(&files.offsets, (size - 1) * 8)?; |
| 157 | 413× | let mut len = [0u8; 2]; |
| 158 | 413× | if off + 2 <= entries_on_disk && files.entries.read_exact_at(&mut len, off).is_ok() { |
| 159 | 411× | let end = off + 2 + u16::from_be_bytes(len) as u64; |
| 160 | 411× | if end <= entries_on_disk { |
| 161 | 411× | entries_len = end; |
| 162 | 411× | break; |
| 163 | 0× | } |
| 164 | 2× | } |
| 165 | 2× | size -= 1; |
| 166 | | } |
| 167 | | |
| 168 | | |
| 169 | 893× | let have = files.hashes.metadata()?.len() / 32; |
| 170 | 893× | let mut whole = size; |
| 171 | 894× | while tlog::stored_hash_count(whole) > have { |
| 172 | 1× | whole -= 1; |
| 173 | 1× | } |
| 174 | 893× | if !writer { |
| 175 | 255× | files.size = whole; |
| 176 | 255× | files.entries_len = entries_len; |
| 177 | 255× | return Ok(()); |
| 178 | 638× | } |
| 179 | 638× | files.offsets.set_len(size * 8)?; |
| 180 | 638× | files.entries.set_len(entries_len)?; |
| 181 | 638× | files.hashes.set_len(tlog::stored_hash_count(whole) * 32)?; |
| 182 | 638× | files.entries_len = entries_len; |
| 183 | 638× | for n in whole..size { |
| 184 | 1× | let bytes = Self::entry_bytes(files, n)?; |
| 185 | 1× | let add = tlog::stored_hashes_for_leaf(n, tlog::leaf_hash(&bytes), &FileHashes(&files.hashes))?; |
| 186 | 3× | for h in add { |
| 187 | 3× | files.hashes.write_all(&h)?; |
| 188 | | } |
| 189 | 1× | files.size = n + 1; |
| 190 | | } |
| 191 | 638× | files.size = size; |
| 192 | 638× | files.hashes.sync_data()?; |
| 193 | 638× | Ok(()) |
| 194 | 893× | } |
| 195 | |
|
| 196 | | |
| 197 | 252× | fn catch_up(&self, f: &mut Files) { |
| 198 | 252× | if let Err(e) = Self::recover(f, false) { |
| 199 | 0× | tracing::warn!("tlog: reading the log's extent: {e:#}"); |
| 200 | 252× | } |
| 201 | 252× | } |
| 202 | |
|
| 203 | | |
| 204 | 1050× | fn read_checkpoint(dir: &Path, size: u64) -> Option<(u64, String)> { |
| 205 | 1050× | std::fs::read_to_string(dir.join("checkpoint")).ok().and_then(|note| { |
| 206 | 599× | let n = tlog::Note::parse(¬e).ok()?; |
| 207 | 599× | let c = Checkpoint::parse(&n.text).ok()?; |
| 208 | 599× | (c.size <= size).then_some((c.size, note)) |
| 209 | 599× | }) |
| 210 | 1050× | } |
| 211 | |
|
| 212 | 83× | fn entry_bytes(f: &Files, n: u64) -> anyhow::Result<Vec<u8>> { |
| 213 | 83× | let off = read_u64_at(&f.offsets, n * 8)?; |
| 214 | 83× | let mut len = [0u8; 2]; |
| 215 | 83× | f.entries.read_exact_at(&mut len, off)?; |
| 216 | 83× | let mut b = vec![0u8; u16::from_be_bytes(len) as usize]; |
| 217 | 83× | f.entries.read_exact_at(&mut b, off + 2)?; |
| 218 | 83× | Ok(b) |
| 219 | 83× | } |
| 220 | |
|
| 221 | | |
| 222 | 31× | pub fn entry(&self, n: u64) -> anyhow::Result<Vec<u8>> { |
| 223 | 31× | let mut f = self.files.lock().unwrap(); |
| 224 | 31× | if n >= f.size { |
| 225 | 0× | self.catch_up(&mut f); |
| 226 | 31× | } |
| 227 | 31× | anyhow::ensure!(n < f.size, "entry {n} is not in the log"); |
| 228 | 31× | Self::entry_bytes(&f, n) |
| 229 | 31× | } |
| 230 | |
|
| 231 | | |
| 232 | 252× | pub fn size(&self) -> u64 { |
| 233 | 252× | let mut f = self.files.lock().unwrap(); |
| 234 | 252× | self.catch_up(&mut f); |
| 235 | 252× | f.size |
| 236 | 252× | } |
| 237 | |
|
| 238 | | |
| 239 | 426× | pub fn append(&self, entries: &[Entry]) -> anyhow::Result<Vec<u64>> { |
| 240 | 426× | if !self.writer { |
| 241 | 2× | anyhow::bail!("this process does not write the log: it has no web role"); |
| 242 | 424× | } |
| 243 | 424× | let mut f = self.files.lock().unwrap(); |
| 244 | | |
| 245 | | |
| 246 | 424× | self.lock.lock()?; |
| 247 | 424× | let appended = Self::recover(&mut f, true).and_then(|()| Self::append_locked(&mut f, entries)); |
| 248 | 424× | self.lock.unlock()?; |
| 249 | 424× | appended |
| 250 | 426× | } |
| 251 | |
|
| 252 | 424× | fn append_locked(f: &mut Files, entries: &[Entry]) -> anyhow::Result<Vec<u64>> { |
| 253 | 424× | let mut out = Vec::with_capacity(entries.len()); |
| 254 | 828× | for e in entries { |
| 255 | 828× | let bytes = e.encode(); |
| 256 | 828× | let n = f.size; |
| 257 | 828× | let add = tlog::stored_hashes_for_leaf(n, tlog::leaf_hash(&bytes), &FileHashes(&f.hashes))?; |
| 258 | 828× | let off = f.entries_len; |
| 259 | 828× | let mut rec = (bytes.len() as u16).to_be_bytes().to_vec(); |
| 260 | 828× | rec.extend_from_slice(&bytes); |
| 261 | 828× | f.entries.write_all(&rec)?; |
| 262 | 828× | f.entries_len += rec.len() as u64; |
| 263 | 828× | f.offsets.write_all(&off.to_le_bytes())?; |
| 264 | 828× | let mut hs = Vec::with_capacity(add.len() * 32); |
| 265 | 1575× | for h in add { |
| 266 | 1575× | hs.extend_from_slice(&h); |
| 267 | 1575× | } |
| 268 | 828× | f.hashes.write_all(&hs)?; |
| 269 | 828× | f.size = n + 1; |
| 270 | 828× | out.push(n); |
| 271 | | } |
| 272 | 424× | f.entries.sync_data()?; |
| 273 | 424× | f.offsets.sync_data()?; |
| 274 | 424× | f.hashes.sync_data()?; |
| 275 | 424× | Ok(out) |
| 276 | 424× | } |
| 277 | |
|
| 278 | | |
| 279 | 234× | pub fn tree_hash(&self, n: u64) -> anyhow::Result<Hash> { |
| 280 | 234× | let f = self.files.lock().unwrap(); |
| 281 | 234× | Ok(tlog::tree_hash(n, &FileHashes(&f.hashes))?) |
| 282 | 234× | } |
| 283 | |
|
| 284 | | |
| 285 | 30× | pub fn prove_inclusion(&self, t: u64, n: u64) -> anyhow::Result<Vec<Hash>> { |
| 286 | 30× | let f = self.files.lock().unwrap(); |
| 287 | 30× | Ok(tlog::prove_inclusion(t, n, &FileHashes(&f.hashes))?) |
| 288 | 30× | } |
| 289 | |
|
| 290 | | |
| 291 | 2× | pub fn prove_consistency(&self, t: u64, n: u64) -> anyhow::Result<Vec<Hash>> { |
| 292 | 2× | let f = self.files.lock().unwrap(); |
| 293 | 2× | Ok(tlog::prove_consistency(t, n, &FileHashes(&f.hashes))?) |
| 294 | 2× | } |
| 295 | |
|
| 296 | | |
| 297 | 7× | pub fn tile(&self, t: &Tile) -> anyhow::Result<Option<Vec<u8>>> { |
| 298 | 7× | let f = self.files.lock().unwrap(); |
| 299 | 7× | let at_level = f.size >> (tlog::TILE_HEIGHT * t.level); |
| 300 | 7× | if at_level < (t.n << tlog::TILE_HEIGHT) + t.width as u64 { |
| 301 | 2× | return Ok(None); |
| 302 | 5× | } |
| 303 | 5× | Ok(Some(t.read_data(&FileHashes(&f.hashes))?)) |
| 304 | 7× | } |
| 305 | |
|
| 306 | | |
| 307 | 3× | pub fn bundle(&self, t: &Tile) -> anyhow::Result<Option<Vec<u8>>> { |
| 308 | 3× | let f = self.files.lock().unwrap(); |
| 309 | 3× | let first = t.n << tlog::TILE_HEIGHT; |
| 310 | 3× | if f.size < first + t.width as u64 { |
| 311 | 0× | return Ok(None); |
| 312 | 3× | } |
| 313 | 51× | let entries: Vec<Vec<u8>> = (first..first + t.width as u64).map(|n| Self::entry_bytes(&f, n)).collect::<anyhow::Result<_>>()?; |
| 314 | 3× | Ok(Some(tlog::encode_bundle(&entries)?)) |
| 315 | 3× | } |
| 316 | |
|
| 317 | | |
| 318 | 207× | pub fn configure(&self, c: &crate::config::Tlog) -> anyhow::Result<()> { |
| 319 | 207× | let key = |b64: &str, what: &str| -> anyhow::Result<Identity> { |
| 320 | 0× | let bytes = oxy_core::dsse::base64_decode(b64.trim()).ok_or_else(|| anyhow::anyhow!("[tlog] {what}: the key is not base64"))?; |
| 321 | 0× | Identity::decode(&bytes).map_err(|e| anyhow::anyhow!("[tlog] {what}: {e}")) |
| 322 | 0× | }; |
| 323 | 207× | let mut ws = Vec::new(); |
| 324 | 207× | for w in &c.witnesses { |
| 325 | 0× | ws.push(Peer { name: w.name.clone(), url: w.url.clone(), identity: key(&w.key, &w.name)? }); |
| 326 | | } |
| 327 | 207× | let mut wf = Vec::new(); |
| 328 | 207× | for l in &c.witness_for { |
| 329 | 0× | wf.push((l.origin.clone(), key(&l.key, &l.origin)?)); |
| 330 | | } |
| 331 | 207× | self.set_witnesses(ws); |
| 332 | 207× | for (o, k) in wf { |
| 333 | 0× | self.add_witness_for(&o, k); |
| 334 | 0× | } |
| 335 | 207× | Ok(()) |
| 336 | 207× | } |
| 337 | |
|
| 338 | 208× | pub fn set_witnesses(&self, peers: Vec<Peer>) { |
| 339 | 208× | *self.witnesses.write().unwrap() = peers; |
| 340 | 208× | } |
| 341 | |
|
| 342 | 556× | pub fn witnesses(&self) -> Vec<Peer> { |
| 343 | 556× | self.witnesses.read().unwrap().clone() |
| 344 | 556× | } |
| 345 | |
|
| 346 | | |
| 347 | 1× | pub fn add_witness_for(&self, origin: &str, key: Identity) { |
| 348 | 1× | let mut w = self.witness_for.write().unwrap(); |
| 349 | 1× | w.retain(|(o, _)| o != origin); |
| 350 | 1× | w.push((origin.to_string(), key)); |
| 351 | 1× | } |
| 352 | |
|
| 353 | 5× | fn witnessed_key(&self, origin: &str) -> Option<Identity> { |
| 354 | 5× | self.witness_for.read().unwrap().iter().find(|(o, _)| o == origin).map(|(_, k)| k.clone()) |
| 355 | 5× | } |
| 356 | |
|
| 357 | | |
| 358 | 5× | pub fn witness_name(&self) -> &str { |
| 359 | 5× | self.origin.trim_end_matches("/log") |
| 360 | 5× | } |
| 361 | |
|
| 362 | | |
| 363 | | |
| 364 | 2× | fn set_note(&self, size: u64, note: String) -> anyhow::Result<()> { |
| 365 | 2× | let mut cur = self.checkpoint.lock().unwrap(); |
| 366 | 2× | if cur.as_ref().map(|(s, _)| *s) != Some(size) { |
| 367 | 0× | return Ok(()); |
| 368 | 2× | } |
| 369 | 2× | let tmp = self.dir.join("checkpoint.new"); |
| 370 | 2× | std::fs::write(&tmp, ¬e)?; |
| 371 | 2× | File::open(&tmp)?.sync_all()?; |
| 372 | 2× | std::fs::rename(&tmp, self.dir.join("checkpoint"))?; |
| 373 | 2× | *cur = Some((size, note)); |
| 374 | 2× | Ok(()) |
| 375 | 2× | } |
| 376 | |
|
| 377 | | |
| 378 | | |
| 379 | | |
| 380 | 833× | pub fn checkpoint(&self) -> Option<String> { |
| 381 | 833× | let mut held = self.checkpoint.lock().unwrap(); |
| 382 | 833× | if let Some(kept) = Self::read_checkpoint(&self.dir, u64::MAX) { |
| 383 | 598× | if held.as_ref().is_none_or(|(s, _)| kept.0 > *s) { |
| 384 | 1× | *held = Some(kept); |
| 385 | 597× | } |
| 386 | 235× | } |
| 387 | 833× | held.as_ref().map(|(_, n)| n.clone()) |
| 388 | 833× | } |
| 389 | |
|
| 390 | | |
| 391 | | |
| 392 | 240× | pub fn refresh_checkpoint(&self, identity: &oxy_core::identity::Identity, sk: &oxy_core::identity::SecretKey) -> anyhow::Result<bool> { |
| 393 | 240× | if !self.writer { |
| 394 | 0× | return Ok(false); |
| 395 | 240× | } |
| 396 | 240× | let size = self.size(); |
| 397 | | |
| 398 | 240× | let _ = self.checkpoint(); |
| 399 | 240× | if self.checkpoint.lock().unwrap().as_ref().is_some_and(|(s, _)| *s >= size) { |
| 400 | 16× | return Ok(false); |
| 401 | 224× | } |
| 402 | 224× | let c = Checkpoint { origin: self.origin.clone(), size, root: self.tree_hash(size)?, extensions: vec![] }; |
| 403 | 224× | let note = c.sign(identity, sk)?.encode(); |
| 404 | 224× | let tmp = self.dir.join("checkpoint.new"); |
| 405 | 224× | std::fs::write(&tmp, ¬e)?; |
| 406 | 219× | File::open(&tmp)?.sync_all()?; |
| 407 | 219× | std::fs::rename(&tmp, self.dir.join("checkpoint"))?; |
| 408 | 219× | *self.checkpoint.lock().unwrap() = Some((size, note)); |
| 409 | 219× | Ok(true) |
| 410 | 240× | } |
| 411 | | } |
| 412 | |
|
| 413 | |
|
| 414 | | #[derive(Clone, Debug)] |
| 415 | | pub struct Pending { |
| 416 | | pub kind: &'static str, |
| 417 | | |
| 418 | | pub repo: [u8; 16], |
| 419 | | pub repo_row: Option<i64>, |
| 420 | | pub object: ObjectRef, |
| 421 | | pub signers: Vec<KeyId>, |
| 422 | | } |
| 423 | |
|
| 424 | |
|
| 425 | |
|
| 426 | |
|
| 427 | 120× | pub async fn log(app: &Shared, items: Vec<Pending>) -> Vec<u64> { |
| 428 | 120× | let mut fresh = Vec::new(); |
| 429 | 243× | for p in items { |
| 430 | 243× | let object = p.object.to_hex(); |
| 431 | 243× | match db::tlog::entry_of(&app.db, &object, p.kind).await { |
| 432 | 17× | Ok(Some(_)) => continue, |
| 433 | 226× | Ok(None) => {} |
| 434 | 0× | Err(e) => { |
| 435 | 0× | eprintln!("tlog: looking up {} {}: {e}", p.kind, &object[..12]); |
| 436 | 0× | continue; |
| 437 | | } |
| 438 | | } |
| 439 | 226× | if !fresh.iter().any(|q: &Pending| q.kind == p.kind && q.object == p.object) { |
| 440 | 223× | fresh.push(p); |
| 441 | 223× | } |
| 442 | | } |
| 443 | 120× | if fresh.is_empty() { |
| 444 | 0× | return vec![]; |
| 445 | 120× | } |
| 446 | 120× | let now = db::now_ms(); |
| 447 | 120× | let entries: Vec<Entry> = fresh |
| 448 | 120× | .iter() |
| 449 | 223× | .map(|p| Entry { kind: p.kind.to_string(), repo: p.repo, object: p.object, signers: p.signers.clone(), received: now as u64 }) |
| 450 | 120× | .collect(); |
| 451 | 120× | let tl = app.tlog.clone(); |
| 452 | 120× | let numbers = match tokio::task::spawn_blocking(move || tl.append(&entries)).await { |
| 453 | 120× | Ok(Ok(n)) => n, |
| 454 | 0× | Ok(Err(e)) => { |
| 455 | 0× | eprintln!("tlog: appending {} entries: {e}", fresh.len()); |
| 456 | 0× | return vec![]; |
| 457 | | } |
| 458 | 0× | Err(e) => { |
| 459 | 0× | eprintln!("tlog: appending: {e}"); |
| 460 | 0× | return vec![]; |
| 461 | | } |
| 462 | | }; |
| 463 | 223× | for (p, n) in fresh.iter().zip(&numbers) { |
| 464 | 223× | if let Err(e) = db::tlog::record(&app.db, *n as i64, &p.object.to_hex(), p.kind, p.repo_row, now).await { |
| 465 | 0× | eprintln!("tlog: indexing entry {n}: {e}"); |
| 466 | 223× | } |
| 467 | | } |
| 468 | 120× | numbers |
| 469 | 120× | } |
| 470 | |
|
| 471 | 210× | fn sig_keys(sigs: &[oxy_core::identity::Signature]) -> Vec<KeyId> { |
| 472 | 210× | let mut keys: Vec<KeyId> = sigs.iter().map(|s| s.key).collect(); |
| 473 | 210× | keys.sort(); |
| 474 | 210× | keys.dedup(); |
| 475 | 210× | keys |
| 476 | 210× | } |
| 477 | |
|
| 478 | |
|
| 479 | |
|
| 480 | 194× | fn certificates(repo: &Repo, rid: [u8; 16], row: Option<i64>, signers: &[KeyId], out: &mut Vec<Pending>) { |
| 481 | 198× | for k in signers { |
| 482 | 198× | if let Ok(Some(c)) = repo.store().certificate_of(k) { |
| 483 | 14× | let issuer = repo.store().get_certificate(&c).map(|sc| vec![sc.cert.issuer]).unwrap_or_default(); |
| 484 | 14× | out.push(Pending { kind: "certificate", repo: rid, repo_row: row, object: c, signers: issuer }); |
| 485 | 184× | } |
| 486 | | } |
| 487 | 194× | } |
| 488 | |
|
| 489 | |
|
| 490 | |
|
| 491 | |
|
| 492 | 257× | pub fn push_entries(repo: &Repo, row: Option<i64>, report: &PushReport) -> Vec<Pending> { |
| 493 | 257× | let Ok(rid) = repo.id() else { return vec![] }; |
| 494 | 257× | let mut out = Vec::new(); |
| 495 | 257× | let add = |out: &mut Vec<Pending>, kind: &'static str, object: ObjectRef, signers: Vec<KeyId>| { |
| 496 | 194× | certificates(repo, rid, row, &signers, out); |
| 497 | 194× | out.push(Pending { kind, repo: rid, repo_row: row, object, signers }); |
| 498 | 194× | }; |
| 499 | 265× | for u in &report.updated { |
| 500 | 288× | if !report.results.iter().any(|r| r.ok && r.name == u.name) { |
| 501 | 0× | continue; |
| 502 | 265× | } |
| 503 | | |
| 504 | 265× | if let (Some(tag), true) = (u.name.strip_prefix("tags/"), u.new_state.is_zero()) { |
| 505 | 2× | if let Ok(Some((r, sa))) = repo.tag_withdrawal(tag) { |
| 506 | 2× | add(&mut out, "untag", r, sig_keys(&sa.sigs)); |
| 507 | 2× | if let Ok(Some(acc)) = repo.acceptance_of(&r) { |
| 508 | 0× | add(&mut out, "accepted", acc.attest.hash(), sig_keys(&acc.sigs)); |
| 509 | 2× | } |
| 510 | 0× | } |
| 511 | 2× | continue; |
| 512 | 263× | } |
| 513 | 263× | if u.new_state.is_zero() { |
| 514 | 2× | continue; |
| 515 | 261× | } |
| 516 | 261× | if let Some(tag) = u.name.strip_prefix("tags/") { |
| 517 | 13× | if let Ok(Some((r, sa))) = repo.tag_attest(tag) { |
| 518 | 12× | add(&mut out, "tag", r, sig_keys(&sa.sigs)); |
| 519 | | |
| 520 | | |
| 521 | 12× | if let Ok(Some(acc)) = repo.acceptance_of(&r) { |
| 522 | 1× | add(&mut out, "accepted", acc.attest.hash(), sig_keys(&acc.sigs)); |
| 523 | 11× | } |
| 524 | 1× | } |
| 525 | 248× | } else if let Ok(Some((r, sa))) = repo.latest_ref_attest(&u.name) { |
| 526 | 92× | add(&mut out, "ref", r, sig_keys(&sa.sigs)); |
| 527 | 92× | if let Ok(Some(acc)) = repo.acceptance_of(&r) { |
| 528 | 7× | add(&mut out, "accepted", acc.attest.hash(), sig_keys(&acc.sigs)); |
| 529 | 85× | } |
| 530 | 156× | } |
| 531 | | } |
| 532 | 257× | let mut seals = std::collections::BTreeSet::new(); |
| 533 | 257× | for h in &report.imported { |
| 534 | 247× | for s in repo.store().seals_for(h).unwrap_or_default() { |
| 535 | 63× | seals.insert(s); |
| 536 | 63× | } |
| 537 | | } |
| 538 | 257× | for s in seals { |
| 539 | 63× | let signers = sig_keys(&repo.store().signatures(&s).unwrap_or_default()); |
| 540 | 63× | add(&mut out, "seal", s, signers); |
| 541 | 63× | } |
| 542 | 257× | if let Ok(Some((r, sp))) = repo.current_policy() { |
| 543 | 17× | add(&mut out, "policy", r, sig_keys(&sp.sigs)); |
| 544 | 240× | } |
| 545 | 257× | out |
| 546 | 257× | } |
| 547 | |
|
| 548 | |
|
| 549 | |
|
| 550 | |
|
| 551 | |
|
| 552 | |
|
| 553 | 358× | pub fn add_log(app: &Shared, row: Option<i64>, refs: &mut oxy_core::protocol::RefsResponse) -> Vec<Pending> { |
| 554 | 358× | let mut backfill = Vec::new(); |
| 555 | 358× | let Some(note) = app.tlog.checkpoint() else { return backfill }; |
| 556 | 331× | let Some(size) = tlog::Note::parse(¬e).ok().and_then(|n| Checkpoint::parse(&n.text).ok()).map(|c| c.size) else { |
| 557 | 0× | return backfill; |
| 558 | | }; |
| 559 | 331× | refs.log = Some(note); |
| 560 | | |
| 561 | 331× | refs.witnesses = app.tlog.witnesses().iter().map(|p| (p.name.clone(), p.identity.encode())).collect(); |
| 562 | 369× | for r in refs.refs.iter_mut() { |
| 563 | 369× | let Some(a) = &r.attestation else { continue }; |
| 564 | 145× | if let Some((logged, integration)) = place(app, size, row, "ref", a, &mut backfill) { |
| 565 | 19× | r.logged = Some(logged); |
| 566 | 19× | r.integration = integration; |
| 567 | 126× | } |
| 568 | | } |
| 569 | | |
| 570 | | |
| 571 | 331× | for t in refs.tags.iter_mut() { |
| 572 | 33× | let Some(a) = &t.attestation else { continue }; |
| 573 | 32× | if let Some((logged, integration)) = place(app, size, row, "tag", a, &mut backfill) { |
| 574 | 8× | t.logged = Some(logged); |
| 575 | 8× | t.integration = integration; |
| 576 | 24× | } |
| 577 | | } |
| 578 | 331× | backfill |
| 579 | 358× | } |
| 580 | |
|
| 581 | |
|
| 582 | |
|
| 583 | |
|
| 584 | |
|
| 585 | 177× | fn place( |
| 586 | 177× | app: &Shared, |
| 587 | 177× | size: u64, |
| 588 | 177× | row: Option<i64>, |
| 589 | 177× | kind: &'static str, |
| 590 | 177× | a: &oxy_core::protocol::SignedAttest, |
| 591 | 177× | backfill: &mut Vec<Pending>, |
| 592 | 177× | ) -> Option<(oxy_core::protocol::Logged, Vec<oxy_core::protocol::Integration>)> { |
| 593 | 177× | let object = a.attest.hash(); |
| 594 | 177× | let idx = |
| 595 | 177× | tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(db::tlog::entry_of(&app.db, &object.to_hex(), kind))); |
| 596 | 161× | match idx { |
| 597 | 161× | Ok(Some(i)) if (i as u64) < size => { |
| 598 | 27× | let (Ok(entry), Ok(proof)) = (app.tlog.entry(i as u64), app.tlog.prove_inclusion(size, i as u64)) else { return None }; |
| 599 | | |
| 600 | | |
| 601 | 27× | let notes = |
| 602 | 27× | tokio::task::block_in_place(|| tokio::runtime::Handle::current().block_on(db::tlog::cosigned_notes_after(&app.db, i, 2))) |
| 603 | 27× | .unwrap_or_default(); |
| 604 | 27× | let mut integration = Vec::new(); |
| 605 | 27× | for (at, text) in notes { |
| 606 | 2× | if let Ok(proof) = app.tlog.prove_inclusion(at as u64, i as u64) { |
| 607 | 2× | integration.push(oxy_core::protocol::Integration { note: text, proof }); |
| 608 | 2× | } |
| 609 | | } |
| 610 | 27× | Some((oxy_core::protocol::Logged { index: i as u64, entry, proof }, integration)) |
| 611 | | } |
| 612 | 134× | Ok(Some(_)) => None, |
| 613 | | Ok(None) => { |
| 614 | 16× | backfill.push(Pending { kind, repo: a.attest.repo, repo_row: row, object, signers: sig_keys(&a.sigs) }); |
| 615 | 16× | None |
| 616 | | } |
| 617 | 0× | Err(e) => { |
| 618 | 0× | eprintln!("tlog: looking up {}: {e}", &object.to_hex()[..12]); |
| 619 | 0× | None |
| 620 | | } |
| 621 | | } |
| 622 | 177× | } |
| 623 | |
|
| 624 | |
|
| 625 | |
|
| 626 | |
|
| 627 | 224× | pub async fn collect(app: &Shared) { |
| 628 | 224× | let peers = app.tlog.witnesses(); |
| 629 | 224× | let Some(text) = app.tlog.checkpoint() else { return }; |
| 630 | 224× | if peers.is_empty() { |
| 631 | 222× | return; |
| 632 | 2× | } |
| 633 | 2× | let Ok(mut note) = tlog::Note::parse(&text) else { return }; |
| 634 | 2× | let Ok(cp) = Checkpoint::parse(¬e.text) else { return }; |
| 635 | | |
| 636 | 4× | let own = tlog::Note { text: note.text.clone(), sigs: note.sigs.iter().filter(|s| s.name == cp.origin).cloned().collect() }; |
| 637 | 2× | let mut changed = false; |
| 638 | 2× | for p in peers { |
| 639 | 2× | if note.cosigned_by(&p.name, &p.identity).is_ok() { |
| 640 | 0× | continue; |
| 641 | 2× | } |
| 642 | 2× | let mut old = db::tlog::cosigned_size(&app.db, &p.name).await.unwrap_or(0) as u64; |
| 643 | 2× | for _ in 0..2 { |
| 644 | 2× | let proof = if old == 0 || old >= cp.size { Vec::new() } else { app.tlog.prove_consistency(cp.size, old).unwrap_or_default() }; |
| 645 | 2× | let body = tlog::AddCheckpoint { old_size: old, proof, note: own.encode() }.encode(); |
| 646 | 2× | let client = match crate::net::client(&p.url, std::slice::from_ref(&p.url), std::time::Duration::from_secs(20)).await { |
| 647 | 2× | Ok(c) => c, |
| 648 | 0× | Err(e) => { |
| 649 | 0× | eprintln!("tlog: witness {}: {e}", p.name); |
| 650 | 0× | break; |
| 651 | | } |
| 652 | | }; |
| 653 | 2× | let res = match client.post(&p.url).header(header::CONTENT_TYPE, "text/plain").body(body).send().await { |
| 654 | 2× | Ok(r) => r, |
| 655 | 0× | Err(e) => { |
| 656 | 0× | eprintln!("tlog: witness {}: {e}", p.name); |
| 657 | 0× | break; |
| 658 | | } |
| 659 | | }; |
| 660 | 2× | let status = res.status(); |
| 661 | 2× | let answer = res.text().await.unwrap_or_default(); |
| 662 | 2× | if status == StatusCode::CONFLICT { |
| 663 | 0× | match answer.trim().parse() { |
| 664 | 0× | Ok(n) if n != old => { |
| 665 | 0× | old = n; |
| 666 | 0× | continue; |
| 667 | | } |
| 668 | | _ => { |
| 669 | 0× | eprintln!("tlog: witness {}: a conflict without a size: {answer}", p.name); |
| 670 | 0× | break; |
| 671 | | } |
| 672 | | } |
| 673 | 2× | } |
| 674 | 2× | if !status.is_success() { |
| 675 | 0× | eprintln!("tlog: witness {} refused the checkpoint of {} entries ({status}): {}", p.name, cp.size, answer.trim()); |
| 676 | 0× | break; |
| 677 | 2× | } |
| 678 | 2× | let lines = tlog::Note::parse(&format!("{}\n{answer}", note.text)).map(|n| n.sigs).unwrap_or_default(); |
| 679 | 4× | let theirs: Vec<_> = lines.into_iter().filter(|s| s.name == p.name).collect(); |
| 680 | 2× | let check = tlog::Note { text: note.text.clone(), sigs: theirs.clone() }; |
| 681 | 2× | match check.cosigned_by(&p.name, &p.identity) { |
| 682 | | Ok(_) => { |
| 683 | 2× | note.sigs.extend(theirs); |
| 684 | 2× | db::tlog::set_cosigned(&app.db, &p.name, cp.size as i64).await.ok(); |
| 685 | 2× | changed = true; |
| 686 | | } |
| 687 | 0× | Err(e) => eprintln!("tlog: witness {} answered a cosignature that does not verify: {e}", p.name), |
| 688 | | } |
| 689 | 2× | break; |
| 690 | | } |
| 691 | | } |
| 692 | 2× | if changed { |
| 693 | | |
| 694 | 2× | db::tlog::keep_cosigned_note(&app.db, cp.size as i64, ¬e.encode()).await.ok(); |
| 695 | 2× | if let Err(e) = app.tlog.set_note(cp.size, note.encode()) { |
| 696 | 0× | eprintln!("tlog: keeping the cosigned checkpoint: {e}"); |
| 697 | 2× | } |
| 698 | 0× | } |
| 699 | 224× | } |
| 700 | |
|
| 701 | |
|
| 702 | |
|
| 703 | |
|
| 704 | 5× | pub async fn add_checkpoint(State(app): State<Shared>, body: String) -> Response { |
| 705 | 5× | let Ok(req) = tlog::AddCheckpoint::parse(&body) else { |
| 706 | 0× | return plain(StatusCode::BAD_REQUEST, "not an add-checkpoint request"); |
| 707 | | }; |
| 708 | 5× | let Some(origin) = tlog::Note::parse(&req.note).ok().and_then(|n| Checkpoint::parse(&n.text).ok()).map(|c| c.origin) else { |
| 709 | 0× | return plain(StatusCode::BAD_REQUEST, "the checkpoint does not parse"); |
| 710 | | }; |
| 711 | 5× | let Some(key) = app.tlog.witnessed_key(&origin) else { |
| 712 | 1× | return plain(StatusCode::NOT_FOUND, &format!("this hub does not witness {origin}")); |
| 713 | | }; |
| 714 | 4× | let latest = match db::tlog::witnessed(&app.db, &origin).await { |
| 715 | 4× | Ok(l) => l.and_then(|(size, root)| Some((size as u64, ObjectRef::from_hex(&root)?.0))), |
| 716 | 0× | Err(e) => return plain(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), |
| 717 | | }; |
| 718 | 4× | let sk = match crate::keys::load_secret(&app.config.keys_dir()) { |
| 719 | 4× | Ok((_, sk)) => sk, |
| 720 | 0× | Err(e) => return plain(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), |
| 721 | | }; |
| 722 | 4× | let me = tlog::WitnessKey { name: app.tlog.witness_name(), identity: &app.hub_key.identity, sk: &sk }; |
| 723 | 4× | let now = (db::now_ms() / 1000) as u64; |
| 724 | 4× | let conflict = |n: u64| ([(header::CONTENT_TYPE, "text/x.tlog.size")], format!("{n}\n")); |
| 725 | 4× | match tlog::witness(&req, &origin, &key, latest, &me, now) { |
| 726 | 2× | Ok(tlog::Witnessed::Cosigned { checkpoint, lines }) => { |
| 727 | 2× | let root = ObjectRef(checkpoint.root).to_hex(); |
| 728 | 2× | match db::tlog::witness_advance(&app.db, &origin, req.old_size as i64, checkpoint.size as i64, &root).await { |
| 729 | | Ok(true) => { |
| 730 | 2× | let body: String = tlog::Note { text: String::new(), sigs: lines }.encode().trim_start_matches('\n').to_string(); |
| 731 | 2× | ([(header::CONTENT_TYPE, "text/plain; charset=utf-8")], body).into_response() |
| 732 | | } |
| 733 | | Ok(false) => { |
| 734 | 0× | let now = db::tlog::witnessed(&app.db, &origin).await.ok().flatten().map(|(s, _)| s as u64).unwrap_or(0); |
| 735 | 0× | (StatusCode::CONFLICT, conflict(now)).into_response() |
| 736 | | } |
| 737 | 0× | Err(e) => plain(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), |
| 738 | | } |
| 739 | | } |
| 740 | 1× | Ok(tlog::Witnessed::Conflict(n)) => (StatusCode::CONFLICT, conflict(n)).into_response(), |
| 741 | 0× | Err(tlog::WitnessError::Unsigned(e)) => plain(StatusCode::FORBIDDEN, &e), |
| 742 | 1× | Err(tlog::WitnessError::Inconsistent(e)) => plain(StatusCode::UNPROCESSABLE_ENTITY, &e), |
| 743 | 0× | Err(tlog::WitnessError::Bad(e)) => plain(StatusCode::BAD_REQUEST, &e), |
| 744 | | } |
| 745 | 5× | } |
| 746 | |
|
| 747 | |
|
| 748 | 206× | pub async fn checkpointer(app: Shared) { |
| 749 | 206× | let mut tick = tokio::time::interval(CHECKPOINT_EVERY); |
| 750 | | loop { |
| 751 | 428× | tick.tick().await; |
| 752 | 228× | let (identity, keys) = (app.hub_key.identity.clone(), app.config.keys_dir()); |
| 753 | 228× | let tl = app.tlog.clone(); |
| 754 | 228× | let done = tokio::task::spawn_blocking(move || -> anyhow::Result<bool> { |
| 755 | 228× | let (_, sk) = crate::keys::load_secret(&keys)?; |
| 756 | 228× | tl.refresh_checkpoint(&identity, &sk) |
| 757 | 228× | }) |
| 758 | 228× | .await; |
| 759 | 222× | match done { |
| 760 | 0× | Ok(Err(e)) => eprintln!("tlog: signing a checkpoint: {e}"), |
| 761 | | |
| 762 | 222× | _ => collect(&app).await, |
| 763 | | } |
| 764 | | } |
| 765 | | } |
| 766 | |
|
| 767 | 3× | fn plain(status: StatusCode, why: &str) -> Response { |
| 768 | 3× | (status, [(header::CONTENT_TYPE, "text/plain; charset=utf-8")], format!("{why}\n")).into_response() |
| 769 | 3× | } |
| 770 | |
|
| 771 | |
|
| 772 | |
|
| 773 | 3× | pub async fn checkpoint(State(app): State<Shared>) -> Response { |
| 774 | 3× | match app.tlog.checkpoint() { |
| 775 | 3× | Some(note) => ([(header::CONTENT_TYPE, "text/plain; charset=utf-8"), (header::CACHE_CONTROL, "no-cache")], note).into_response(), |
| 776 | 0× | None => plain(StatusCode::SERVICE_UNAVAILABLE, "no checkpoint yet: the log signs one within seconds"), |
| 777 | | } |
| 778 | 3× | } |
| 779 | |
|
| 780 | |
|
| 781 | |
|
| 782 | 1× | pub async fn witnesses(State(app): State<Shared>) -> Response { |
| 783 | 1× | let body: String = |
| 784 | 1× | app.tlog.witnesses().iter().map(|p| format!("{} {}\n", p.name, oxy_core::dsse::base64(&p.identity.encode()))).collect(); |
| 785 | 1× | ([(header::CONTENT_TYPE, "text/plain; charset=utf-8"), (header::CACHE_CONTROL, "no-cache")], body).into_response() |
| 786 | 1× | } |
| 787 | |
|
| 788 | |
|
| 789 | |
|
| 790 | |
|
| 791 | 6× | pub async fn tile(State(app): State<Shared>, UrlPath(path): UrlPath<String>) -> Response { |
| 792 | 6× | let (entries, rest) = match path.strip_prefix("entries/") { |
| 793 | 2× | Some(r) => (true, format!("0/{r}")), |
| 794 | 4× | None => (false, path.clone()), |
| 795 | | }; |
| 796 | 6× | let Some(t) = Tile::parse(&rest) else { |
| 797 | 0× | return plain(StatusCode::NOT_FOUND, "no such tile"); |
| 798 | | }; |
| 799 | 6× | let tl = app.tlog.clone(); |
| 800 | 6× | let got = tokio::task::spawn_blocking(move || if entries { tl.bundle(&t) } else { tl.tile(&t) }).await; |
| 801 | 6× | match got { |
| 802 | 5× | Ok(Ok(Some(data))) => { |
| 803 | 5× | let cache = if t.width as u64 == TILE_WIDTH { "public, max-age=31536000, immutable" } else { "public, max-age=5" }; |
| 804 | 5× | ([(header::CONTENT_TYPE, "application/octet-stream"), (header::CACHE_CONTROL, cache)], data).into_response() |
| 805 | | } |
| 806 | 1× | Ok(Ok(None)) => plain(StatusCode::NOT_FOUND, "the log does not hold that tile yet"), |
| 807 | 0× | Ok(Err(e)) => plain(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), |
| 808 | 0× | Err(e) => plain(StatusCode::INTERNAL_SERVER_ERROR, &e.to_string()), |
| 809 | | } |
| 810 | 6× | } |
| 811 | |
|
| 812 | | #[cfg(test)] |
| 813 | | mod tests { |
| 814 | | use super::*; |
| 815 | |
|
| 816 | | #[test] |
| 817 | 1× | fn a_reader_leaves_the_files_alone_and_cannot_append() { |
| 818 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 819 | 1× | let log = Log::open(dir.path(), "hub.example/log").unwrap(); |
| 820 | 1× | log.append(&[entry(0), entry(1)]).unwrap(); |
| 821 | | |
| 822 | 1× | std::fs::OpenOptions::new().append(true).open(dir.path().join("offsets")).unwrap().write_all(&9999u64.to_le_bytes()).unwrap(); |
| 823 | 1× | let before = std::fs::metadata(dir.path().join("offsets")).unwrap().len(); |
| 824 | 1× | let reader = Log::open_reader(dir.path(), "hub.example/log").unwrap(); |
| 825 | 1× | assert_eq!(reader.size(), 2); |
| 826 | 1× | assert_eq!(std::fs::metadata(dir.path().join("offsets")).unwrap().len(), before, "the writer's files are untouched"); |
| 827 | 1× | assert!(reader.append(&[entry(2)]).is_err()); |
| 828 | 1× | assert_eq!(reader.tree_hash(2).unwrap(), log.tree_hash(2).unwrap()); |
| 829 | 1× | } |
| 830 | |
|
| 831 | 606× | fn entry(i: u64) -> Entry { |
| 832 | 606× | Entry { kind: "ref".into(), repo: [1; 16], object: ObjectRef([i as u8; 32]), signers: vec![], received: i } |
| 833 | 606× | } |
| 834 | |
|
| 835 | | #[test] |
| 836 | 1× | fn the_log_survives_a_torn_append_and_keeps_its_root() { |
| 837 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 838 | 1× | let log = Log::open(dir.path(), "hub.example/log").unwrap(); |
| 839 | 1× | let first: Vec<Entry> = (0..300).map(entry).collect(); |
| 840 | 1× | assert_eq!(log.append(&first).unwrap(), (0..300).collect::<Vec<_>>()); |
| 841 | 1× | let root = log.tree_hash(300).unwrap(); |
| 842 | | |
| 843 | 1× | let mut stored: Vec<Hash> = Vec::new(); |
| 844 | 300× | for (n, e) in first.iter().enumerate() { |
| 845 | 300× | stored.extend(tlog::stored_hashes_for_leaf(n as u64, e.leaf_hash(), &stored).unwrap()); |
| 846 | 300× | } |
| 847 | 1× | assert_eq!(root, tlog::tree_hash(300, &stored).unwrap()); |
| 848 | 1× | drop(log); |
| 849 | | |
| 850 | 1× | let torn = dir.path().join("hashes"); |
| 851 | 1× | let len = std::fs::metadata(&torn).unwrap().len(); |
| 852 | 1× | OpenOptions::new().write(true).open(&torn).unwrap().set_len(len - 40).unwrap(); |
| 853 | 1× | let mut f = OpenOptions::new().append(true).open(dir.path().join("entries")).unwrap(); |
| 854 | 1× | f.write_all(&[0, 200, 1, 2]).unwrap(); |
| 855 | 1× | let log = Log::open(dir.path(), "hub.example/log").unwrap(); |
| 856 | 1× | assert_eq!(log.size(), 300); |
| 857 | 1× | assert_eq!(log.tree_hash(300).unwrap(), root); |
| 858 | 1× | log.append(&[entry(300)]).unwrap(); |
| 859 | 1× | assert_eq!(log.size(), 301); |
| 860 | 1× | let p = log.prove_inclusion(301, 7).unwrap(); |
| 861 | 1× | tlog::check_inclusion(&p, 301, &log.tree_hash(301).unwrap(), 7, &first[7].leaf_hash()).unwrap(); |
| 862 | 1× | let c = log.prove_consistency(301, 300).unwrap(); |
| 863 | 1× | tlog::check_consistency(&c, 301, &log.tree_hash(301).unwrap(), 300, &root).unwrap(); |
| 864 | | |
| 865 | 1× | let full = log.tile(&Tile { level: 0, n: 0, width: 256 }).unwrap().unwrap(); |
| 866 | 1× | assert_eq!(full.len(), 256 * 32); |
| 867 | 1× | assert!(log.tile(&Tile { level: 0, n: 1, width: 45 }).unwrap().is_some()); |
| 868 | 1× | assert!(log.tile(&Tile { level: 0, n: 1, width: 46 }).unwrap().is_none(), "not yet"); |
| 869 | 1× | let bundle = log.bundle(&Tile { level: 0, n: 1, width: 45 }).unwrap().unwrap(); |
| 870 | 1× | assert_eq!(tlog::decode_bundle(&bundle).unwrap()[0], first[256].encode()); |
| 871 | 1× | } |
| 872 | |
|
| 873 | | |
| 874 | | |
| 875 | | |
| 876 | | #[test] |
| 877 | 1× | fn writers_sharing_the_directory_append_one_at_a_time() { |
| 878 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 879 | 1× | let a = std::sync::Arc::new(Log::open(dir.path(), "hub.example/log").unwrap()); |
| 880 | 1× | let b = std::sync::Arc::new(Log::open(dir.path(), "hub.example/log").unwrap()); |
| 881 | 1× | let reader = Log::open_reader(dir.path(), "hub.example/log").unwrap(); |
| 882 | 2× | let spawn = |log: std::sync::Arc<Log>, from: u64| { |
| 883 | 2× | std::thread::spawn(move || { |
| 884 | 2× | let mut got = Vec::new(); |
| 885 | 300× | for i in 0..150 { |
| 886 | 300× | got.extend(log.append(&[entry(from + i)]).unwrap()); |
| 887 | 300× | } |
| 888 | 2× | got |
| 889 | 2× | }) |
| 890 | 2× | }; |
| 891 | 1× | let (ta, tb) = (spawn(a.clone(), 0), spawn(b.clone(), 1000)); |
| 892 | 1× | let mut numbers: Vec<u64> = ta.join().unwrap().into_iter().chain(tb.join().unwrap()).collect(); |
| 893 | 1× | numbers.sort(); |
| 894 | 1× | assert_eq!(numbers, (0..300).collect::<Vec<_>>(), "every entry its own place"); |
| 895 | 1× | assert_eq!((a.size(), b.size(), reader.size()), (300, 300, 300)); |
| 896 | 1× | let root = a.tree_hash(300).unwrap(); |
| 897 | 1× | assert_eq!(b.tree_hash(300).unwrap(), root); |
| 898 | 1× | let fresh = Log::open(dir.path(), "hub.example/log").unwrap(); |
| 899 | 1× | assert_eq!(fresh.tree_hash(300).unwrap(), root, "the files are one log"); |
| 900 | 1× | assert_eq!(b.entry(299).unwrap(), fresh.entry(299).unwrap()); |
| 901 | 1× | assert_eq!(reader.entry(299).unwrap(), fresh.entry(299).unwrap()); |
| 902 | | |
| 903 | 1× | let sk = oxy_core::identity::SecretKey::generate(); |
| 904 | 1× | let hub = sk.identity("hub", "[email protected]", 1); |
| 905 | 1× | assert!(a.refresh_checkpoint(&hub, &sk).unwrap()); |
| 906 | 1× | assert_eq!(b.checkpoint(), a.checkpoint()); |
| 907 | 1× | assert!(!b.refresh_checkpoint(&hub, &sk).unwrap(), "already covered by the other's"); |
| 908 | 1× | } |
| 909 | |
|
| 910 | | #[test] |
| 911 | 1× | fn checkpoints_are_signed_when_the_log_grows() { |
| 912 | 1× | let dir = tempfile::tempdir().unwrap(); |
| 913 | 1× | let sk = oxy_core::identity::SecretKey::generate(); |
| 914 | 1× | let hub = sk.identity("hub", "[email protected]", 1); |
| 915 | 1× | let log = Log::open(dir.path(), &origin_of("https://hub.example:7343/")).unwrap(); |
| 916 | 1× | assert_eq!(log.origin, "hub.example:7343/log"); |
| 917 | 1× | assert!(log.refresh_checkpoint(&hub, &sk).unwrap(), "the empty log's"); |
| 918 | 1× | assert!(!log.refresh_checkpoint(&hub, &sk).unwrap(), "nothing new"); |
| 919 | 1× | log.append(&[entry(1), entry(2)]).unwrap(); |
| 920 | 1× | assert!(log.refresh_checkpoint(&hub, &sk).unwrap()); |
| 921 | 1× | let note = tlog::Note::parse(&log.checkpoint().unwrap()).unwrap(); |
| 922 | 1× | let c = Checkpoint::verify(¬e, "hub.example:7343/log", &hub).unwrap(); |
| 923 | 1× | assert_eq!((c.size, c.root), (2, log.tree_hash(2).unwrap())); |
| 924 | | |
| 925 | 1× | drop(log); |
| 926 | 1× | let log = Log::open(dir.path(), "hub.example:7343/log").unwrap(); |
| 927 | 1× | assert_eq!(log.checkpoint().map(|n| n.contains("\n2\n")), Some(true)); |
| 928 | 1× | } |
| 929 | | } |