This repository has no description
0

Configure Feed

Select the types of activity you want to include in your feed.

core / knot2 / crates / knot-cobs / src / registry.rs
18 kB 552 lines
1use std::collections::BTreeMap; 2 3use knot_cob::{ 4 ChangeId, ChangePayload, Checkpoint, CobError, CobHome, CobId, CobStore, Evaluate, 5 HistoryModel, SnapshotStride, StateSize, 6}; 7use knot_runtime::Signer; 8use knot_types::{ActorId, OwnerDid, RepoDid, RepoName, RepoRkey, UnixSeconds}; 9use serde::{Deserialize, Serialize}; 10 11#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 12pub struct Registration { 13 pub owner: OwnerDid, 14 pub rkey: RepoRkey, 15 pub name: RepoName, 16 pub repo: RepoDid, 17 pub created_at: UnixSeconds, 18} 19 20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 21pub struct Rename { 22 pub owner: OwnerDid, 23 pub rkey: RepoRkey, 24 pub name: RepoName, 25 pub repo: RepoDid, 26} 27 28#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 29pub struct RepoRef { 30 pub owner: OwnerDid, 31 pub rkey: RepoRkey, 32} 33 34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 35#[serde(tag = "op", content = "data", rename_all = "snake_case")] 36pub enum RegistryChange { 37 Register(Registration), 38 Rename(Rename), 39 Deregister(RepoRef), 40} 41 42impl ChangePayload for RegistryChange { 43 const TYPE: &'static str = "sh.tangled.knot.repoRegistry"; 44} 45 46#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] 47pub struct RepoRecord { 48 pub owner: OwnerDid, 49 pub rkey: RepoRkey, 50 pub name: RepoName, 51 pub created_at: UnixSeconds, 52} 53 54#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] 55pub struct Registry { 56 records: BTreeMap<RepoDid, RepoRecord>, 57 aliases: BTreeMap<OwnerDid, BTreeMap<RepoRkey, RepoDid>>, 58} 59 60impl Registry { 61 pub fn resolve(&self, owner: &OwnerDid, rkey: &RepoRkey) -> Option<&RepoDid> { 62 self.aliases.get(owner)?.get(rkey) 63 } 64 65 pub fn record_of(&self, repo: &RepoDid) -> Option<&RepoRecord> { 66 self.records.get(repo) 67 } 68 69 pub fn owner_of(&self, repo: &RepoDid) -> Option<OwnerDid> { 70 self.records.get(repo).map(|record| record.owner.clone()) 71 } 72 73 pub fn len(&self) -> usize { 74 self.records.len() 75 } 76 77 pub fn is_empty(&self) -> bool { 78 self.records.is_empty() 79 } 80 81 pub fn records(&self) -> impl Iterator<Item = (&RepoDid, &RepoRecord)> { 82 self.records.iter() 83 } 84 85 pub fn aliases(&self) -> impl Iterator<Item = (&OwnerDid, &RepoRkey, &RepoDid)> { 86 self.aliases 87 .iter() 88 .flat_map(|(owner, names)| names.iter().map(move |(rkey, repo)| (owner, rkey, repo))) 89 } 90 91 fn canonical_holder(&self, owner: &OwnerDid, rkey: &RepoRkey) -> Option<&RepoDid> { 92 let holder = self.resolve(owner, rkey)?; 93 self.records 94 .get(holder) 95 .filter(|record| record.rkey == *rkey) 96 .map(|_| holder) 97 } 98 99 fn register(mut self, registration: Registration) -> Self { 100 self = self.drop_repo(&registration.repo); 101 self = self.steal_alias(&registration.owner, &registration.rkey, &registration.repo); 102 self.aliases 103 .entry(registration.owner.clone()) 104 .or_default() 105 .insert(registration.rkey.clone(), registration.repo.clone()); 106 self.records.insert( 107 registration.repo, 108 RepoRecord { 109 owner: registration.owner, 110 rkey: registration.rkey, 111 name: registration.name, 112 created_at: registration.created_at, 113 }, 114 ); 115 self 116 } 117 118 fn rename(mut self, rename: Rename) -> Self { 119 match self.records.get(&rename.repo) { 120 Some(record) if record.owner == rename.owner => {} 121 _ => return self, 122 } 123 self = self.steal_alias(&rename.owner, &rename.rkey, &rename.repo); 124 self.aliases 125 .entry(rename.owner.clone()) 126 .or_default() 127 .insert(rename.rkey.clone(), rename.repo.clone()); 128 if let Some(record) = self.records.get_mut(&rename.repo) { 129 record.rkey = rename.rkey; 130 record.name = rename.name; 131 } 132 self 133 } 134 135 fn deregister(self, target: RepoRef) -> Self { 136 match self.resolve(&target.owner, &target.rkey).cloned() { 137 Some(repo) => self.drop_repo(&repo), 138 None => self, 139 } 140 } 141 142 fn steal_alias(mut self, owner: &OwnerDid, rkey: &RepoRkey, target: &RepoDid) -> Self { 143 match self.resolve(owner, rkey).cloned() { 144 Some(holder) if holder != *target => { 145 let canonical = self 146 .records 147 .get(&holder) 148 .is_some_and(|record| record.rkey == *rkey); 149 if canonical { 150 self.drop_repo(&holder) 151 } else { 152 if let Some(names) = self.aliases.get_mut(owner) { 153 names.remove(rkey); 154 } 155 self.prune_empty_owners() 156 } 157 } 158 _ => self, 159 } 160 } 161 162 fn drop_repo(mut self, repo: &RepoDid) -> Self { 163 self.records.remove(repo); 164 self.aliases 165 .values_mut() 166 .for_each(|names| names.retain(|_, holder| holder != repo)); 167 self.prune_empty_owners() 168 } 169 170 fn prune_empty_owners(mut self) -> Self { 171 self.aliases.retain(|_, names| !names.is_empty()); 172 self 173 } 174} 175 176pub struct RepoRegistryCob; 177 178impl Evaluate for RepoRegistryCob { 179 type State = Registry; 180 type Change = RegistryChange; 181 182 const HISTORY: HistoryModel = HistoryModel::Linear; 183 184 fn initial() -> Self::State { 185 Registry::default() 186 } 187 188 fn apply(state: Self::State, change: Self::Change, _author: &ActorId) -> Self::State { 189 match change { 190 RegistryChange::Register(registration) => state.register(registration), 191 RegistryChange::Rename(rename) => state.rename(rename), 192 RegistryChange::Deregister(target) => state.deregister(target), 193 } 194 } 195} 196 197impl Checkpoint for RepoRegistryCob { 198 const SNAPSHOT_STRIDE: SnapshotStride = SnapshotStride::new(256); 199 fn checkpoint_size(state: &Self::State) -> StateSize { 200 StateSize::new(state.len()) 201 } 202} 203 204#[derive(Debug, thiserror::Error)] 205pub enum RegistryError { 206 #[error(transparent)] 207 Cob(#[from] CobError), 208 #[error("no repo is registered at {owner}/{rkey}")] 209 NotRegistered { owner: OwnerDid, rkey: RepoRkey }, 210 #[error("record key {owner}/{rkey} resolves to {found}, expected {expected}")] 211 RepoMismatch { 212 owner: OwnerDid, 213 rkey: RepoRkey, 214 expected: RepoDid, 215 found: RepoDid, 216 }, 217 #[error("repo {repo} is already registered as {owner}/{rkey}")] 218 AlreadyRegistered { 219 repo: RepoDid, 220 owner: OwnerDid, 221 rkey: RepoRkey, 222 }, 223 #[error("record key {owner}/{rkey} is canonical key of {existing}")] 224 RkeyTaken { 225 owner: OwnerDid, 226 rkey: RepoRkey, 227 existing: RepoDid, 228 }, 229 #[error("repo {repo} isn't hosted on this knot")] 230 NotHosted { repo: RepoDid }, 231 #[error("repo {repo} is no longer registered to {expected}")] 232 OwnerMoved { repo: RepoDid, expected: OwnerDid }, 233} 234 235pub fn register_repo( 236 store: &CobStore, 237 home: &CobHome, 238 object: CobId, 239 registration: Registration, 240 signer: &dyn Signer, 241 timestamp: UnixSeconds, 242) -> Result<Option<ChangeId>, RegistryError> { 243 store.update_maybe_checkpointed::<RepoRegistryCob, RegistryError>( 244 home, 245 object, 246 signer, 247 timestamp, 248 |registry| { 249 if let Some(holder) = registry.canonical_holder(&registration.owner, &registration.rkey) 250 && holder != &registration.repo 251 { 252 return Err(RegistryError::RkeyTaken { 253 owner: registration.owner.clone(), 254 rkey: registration.rkey.clone(), 255 existing: holder.clone(), 256 }); 257 } 258 match registry.record_of(&registration.repo) { 259 Some(record) 260 if record.owner != registration.owner || record.rkey != registration.rkey => 261 { 262 Err(RegistryError::AlreadyRegistered { 263 repo: registration.repo.clone(), 264 owner: record.owner.clone(), 265 rkey: record.rkey.clone(), 266 }) 267 } 268 Some(_) => Ok(None), 269 None => Ok(Some(RegistryChange::Register(registration.clone()))), 270 } 271 }, 272 ) 273} 274 275pub fn rename_repo( 276 store: &CobStore, 277 home: &CobHome, 278 object: CobId, 279 rename: Rename, 280 signer: &dyn Signer, 281 timestamp: UnixSeconds, 282) -> Result<Option<ChangeId>, RegistryError> { 283 store.update_maybe_checkpointed::<RepoRegistryCob, RegistryError>( 284 home, 285 object, 286 signer, 287 timestamp, 288 |registry| { 289 let record = 290 registry 291 .record_of(&rename.repo) 292 .ok_or_else(|| RegistryError::NotHosted { 293 repo: rename.repo.clone(), 294 })?; 295 if record.owner != rename.owner { 296 return Err(RegistryError::OwnerMoved { 297 repo: rename.repo.clone(), 298 expected: rename.owner.clone(), 299 }); 300 } 301 if record.rkey == rename.rkey && record.name == rename.name { 302 return Ok(None); 303 } 304 if let Some(holder) = registry.canonical_holder(&rename.owner, &rename.rkey) 305 && holder != &rename.repo 306 { 307 return Err(RegistryError::RkeyTaken { 308 owner: rename.owner.clone(), 309 rkey: rename.rkey.clone(), 310 existing: holder.clone(), 311 }); 312 } 313 Ok(Some(RegistryChange::Rename(rename.clone()))) 314 }, 315 ) 316} 317 318pub fn deregister_repo( 319 store: &CobStore, 320 home: &CobHome, 321 object: CobId, 322 target: RepoRef, 323 expected: RepoDid, 324 signer: &dyn Signer, 325 timestamp: UnixSeconds, 326) -> Result<ChangeId, RegistryError> { 327 store.update_with_checkpointed::<RepoRegistryCob, RegistryError>( 328 home, 329 object, 330 signer, 331 timestamp, 332 |registry| match registry.resolve(&target.owner, &target.rkey) { 333 None => Err(RegistryError::NotRegistered { 334 owner: target.owner.clone(), 335 rkey: target.rkey.clone(), 336 }), 337 Some(found) if found != &expected => Err(RegistryError::RepoMismatch { 338 owner: target.owner.clone(), 339 rkey: target.rkey.clone(), 340 expected: expected.clone(), 341 found: found.clone(), 342 }), 343 Some(_) => Ok(RegistryChange::Deregister(target.clone())), 344 }, 345 ) 346} 347 348#[cfg(test)] 349mod tests { 350 use super::*; 351 352 fn owner(suffix: &str) -> OwnerDid { 353 OwnerDid::new(format!("did:plc:{suffix}")).unwrap() 354 } 355 356 fn repo(suffix: &str) -> RepoDid { 357 RepoDid::new(format!("did:plc:{suffix}")).unwrap() 358 } 359 360 fn rkey(value: &str) -> RepoRkey { 361 RepoRkey::new(value).unwrap() 362 } 363 364 fn name(value: &str) -> RepoName { 365 RepoName::new(value).unwrap() 366 } 367 368 fn register(owner_id: &str, key: &str, repo_id: &str, at: i64) -> RegistryChange { 369 RegistryChange::Register(Registration { 370 owner: owner(owner_id), 371 rkey: rkey(key), 372 name: name(key), 373 repo: repo(repo_id), 374 created_at: UnixSeconds::new(at), 375 }) 376 } 377 378 fn rename(owner_id: &str, key: &str, repo_id: &str) -> RegistryChange { 379 RegistryChange::Rename(Rename { 380 owner: owner(owner_id), 381 rkey: rkey(key), 382 name: name(key), 383 repo: repo(repo_id), 384 }) 385 } 386 387 fn deregister(owner_id: &str, key: &str) -> RegistryChange { 388 RegistryChange::Deregister(RepoRef { 389 owner: owner(owner_id), 390 rkey: rkey(key), 391 }) 392 } 393 394 fn fold(changes: Vec<RegistryChange>) -> Registry { 395 let author = ActorId::from_secp256k1(&[0x02; 33]); 396 changes 397 .into_iter() 398 .fold(RepoRegistryCob::initial(), |state, change| { 399 RepoRegistryCob::apply(state, change, &author) 400 }) 401 } 402 403 #[test] 404 fn register_maps_owner_and_rkey_to_a_repo() { 405 let state = fold(vec![register("nel", "anemone", "squid", 5)]); 406 assert_eq!( 407 state.resolve(&owner("nel"), &rkey("anemone")), 408 Some(&repo("squid")) 409 ); 410 let record = state.record_of(&repo("squid")).unwrap(); 411 assert_eq!(record.owner, owner("nel")); 412 assert_eq!(record.rkey, rkey("anemone")); 413 assert_eq!(record.name, name("anemone")); 414 assert_eq!(record.created_at, UnixSeconds::new(5)); 415 } 416 417 #[test] 418 fn re_register_replaces_the_repo_under_an_rkey() { 419 let state = fold(vec![ 420 register("nel", "anemone", "squid", 1), 421 register("nel", "anemone", "limpet", 2), 422 ]); 423 assert_eq!( 424 state.resolve(&owner("nel"), &rkey("anemone")), 425 Some(&repo("limpet")) 426 ); 427 assert!( 428 state.record_of(&repo("squid")).is_none(), 429 "repo whose canonical rkey is taken by later register is dropped wholesale" 430 ); 431 assert_eq!(state.len(), 1); 432 } 433 434 #[test] 435 fn rename_retains_the_prior_rkey_as_an_alias() { 436 let state = fold(vec![ 437 register("nel", "anemone", "squid", 1), 438 rename("nel", "barnacle", "squid"), 439 ]); 440 assert_eq!( 441 state.resolve(&owner("nel"), &rkey("barnacle")), 442 Some(&repo("squid")), 443 "new rkey resolves" 444 ); 445 assert_eq!( 446 state.resolve(&owner("nel"), &rkey("anemone")), 447 Some(&repo("squid")), 448 "prior rkey keeps resolving as an alias" 449 ); 450 let record = state.record_of(&repo("squid")).unwrap(); 451 assert_eq!(record.rkey, rkey("barnacle")); 452 assert_eq!(record.name, name("barnacle")); 453 assert_eq!(state.len(), 1); 454 } 455 456 #[test] 457 fn rename_of_an_unregistered_repo_is_a_no_op() { 458 let registered = fold(vec![register("nel", "anemone", "squid", 1)]); 459 let after = fold(vec![ 460 register("nel", "anemone", "squid", 1), 461 rename("nel", "barnacle", "conch"), 462 ]); 463 assert_eq!(after, registered); 464 } 465 466 #[test] 467 fn rename_under_a_mismatched_owner_is_a_no_op() { 468 let registered = fold(vec![register("nel", "anemone", "squid", 1)]); 469 let after = fold(vec![ 470 register("nel", "anemone", "squid", 1), 471 rename("olaren", "barnacle", "squid"), 472 ]); 473 assert_eq!(after, registered); 474 } 475 476 #[test] 477 fn deregister_by_any_alias_removes_the_repo_and_every_alias() { 478 let state = fold(vec![ 479 register("nel", "anemone", "squid", 1), 480 rename("nel", "barnacle", "squid"), 481 deregister("nel", "anemone"), 482 deregister("nel", "anemone"), 483 ]); 484 assert_eq!(state.resolve(&owner("nel"), &rkey("anemone")), None); 485 assert_eq!(state.resolve(&owner("nel"), &rkey("barnacle")), None); 486 assert!(state.is_empty(), "replaying a deregister folds as a no-op"); 487 assert_eq!(state, Registry::default()); 488 } 489 490 #[test] 491 fn a_later_change_steals_a_stale_alias_but_keeps_the_victim_canonical() { 492 let state = fold(vec![ 493 register("nel", "anemone", "squid", 1), 494 rename("nel", "barnacle", "squid"), 495 register("nel", "anemone", "whelk", 2), 496 ]); 497 assert_eq!( 498 state.resolve(&owner("nel"), &rkey("anemone")), 499 Some(&repo("whelk")), 500 "later register wins stale alias" 501 ); 502 assert_eq!( 503 state.resolve(&owner("nel"), &rkey("barnacle")), 504 Some(&repo("squid")), 505 "victim keeps its canonical rkey" 506 ); 507 assert_eq!(state.len(), 2); 508 } 509 510 #[test] 511 fn owner_of_resolves_through_the_record_with_later_register_precedence() { 512 let unique = fold(vec![ 513 register("nel", "anemone", "squid", 1), 514 register("nel", "barnacle", "whelk", 2), 515 ]); 516 assert_eq!(unique.owner_of(&repo("squid")), Some(owner("nel"))); 517 assert_eq!( 518 unique.record_of(&repo("squid")).unwrap().rkey, 519 rkey("anemone") 520 ); 521 assert_eq!(unique.owner_of(&repo("conch")), None); 522 523 let moved = fold(vec![ 524 register("nel", "anemone", "squid", 1), 525 register("olaren", "fork", "squid", 2), 526 ]); 527 assert_eq!( 528 moved.owner_of(&repo("squid")), 529 Some(owner("olaren")), 530 "linear causal order gives later register deterministic precedence" 531 ); 532 assert_eq!( 533 moved.resolve(&owner("nel"), &rkey("anemone")), 534 None, 535 "re-register under a new owner drops old owner's aliases" 536 ); 537 } 538 539 #[test] 540 fn change_payload_roundtrips_through_dag_cbor() { 541 [ 542 register("nel", "anemone", "squid", 5), 543 rename("nel", "barnacle", "squid"), 544 deregister("nel", "anemone"), 545 ] 546 .into_iter() 547 .for_each(|change| { 548 let bytes = change.encode().unwrap(); 549 assert_eq!(RegistryChange::decode(&bytes).unwrap(), change); 550 }); 551 } 552}