This repository has no description
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(®istration.repo);
101 self = self.steal_alias(®istration.owner, ®istration.rkey, ®istration.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(®istration.owner, ®istration.rkey)
250 && holder != ®istration.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(®istration.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}