1use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD};
2use kcode_k1_invite_projection::{ApplyOutcome, InviteAction, InviteProjection};
3use kcode_k1_peering::K1Peering;
4use kcode_k1_transaction::SubsystemId;
5use kcode_k1_transaction_id::TxId;
6use kcode_k1_txn_ordering::{K1TxnOrdering, Subsystem};
7use std::collections::{HashMap, HashSet};
8use std::path::Path;
9use std::str::FromStr;
10use std::sync::{Arc, Condvar, Mutex, MutexGuard};
11
12const SUBSYSTEM_NAME: &str = "k1-invites-subsystem";
13const ISSUE_LENGTH: usize = 8;
14const CONSUME_HEADER_LENGTH: usize = 46;
15const REOPEN_REQUIRED: &str = "k1-invites is unavailable; reopen required";
16
17#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
18pub struct InviteCode {
19 bytes: [u8; 6],
20}
21
22impl InviteCode {
23 pub fn expose(&self) -> String {
24 URL_SAFE_NO_PAD.encode(self.bytes.as_slice())
25 }
26}
27
28impl FromStr for InviteCode {
29 type Err = String;
30
31 fn from_str(value: &str) -> Result<Self, Self::Err> {
32 let mut bytes = [0_u8; 6];
33 if value.len() != 8 {
34 return Err("invalid invite code".to_owned());
35 }
36 let written = match URL_SAFE_NO_PAD.decode_slice(value, &mut bytes) {
37 Ok(written) => written,
38 Err(_) => return Err("invalid invite code".to_owned()),
39 };
40 if written != bytes.len() {
41 return Err("invalid invite code".to_owned());
42 }
43 let mut canonical = [0_u8; 8];
44 let encoded = match URL_SAFE_NO_PAD.encode_slice(bytes, &mut canonical) {
45 Ok(encoded) => encoded,
46 Err(_) => return Err("invalid invite code".to_owned()),
47 };
48 if encoded != canonical.len() || canonical.as_slice() != value.as_bytes() {
49 return Err("invalid invite code".to_owned());
50 }
51 Ok(Self { bytes })
52 }
53}
54
55#[derive(Clone, Copy, Debug, Eq, PartialEq)]
56pub enum InviteStatus {
57 Unknown,
58 Unused,
59 Consumed,
60}
61
62#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
63pub struct UserId(TxId);
64
65impl UserId {
66 pub const fn from_tx_id(txid: TxId) -> Self {
67 Self(txid)
68 }
69
70 pub const fn as_tx_id(self) -> TxId {
71 self.0
72 }
73}
74
75#[derive(Clone, Copy, Eq, Hash, PartialEq)]
76pub struct RegistrationKey([u8; 32]);
77
78impl RegistrationKey {
79 pub fn from_bytes(bytes: [u8; 32]) -> Self {
80 Self(bytes)
81 }
82
83 pub fn as_bytes(&self) -> &[u8; 32] {
84 &self.0
85 }
86}
87
88#[derive(Clone, Eq, PartialEq)]
89pub struct Registration {
90 user_id: UserId,
91 registration_key: RegistrationKey,
92 data: Arc<Vec<u8>>,
93}
94
95impl Registration {
96 pub fn user_id(&self) -> UserId {
97 self.user_id
98 }
99
100 pub fn registration_key(&self) -> RegistrationKey {
101 self.registration_key
102 }
103
104 pub fn data(&self) -> &[u8] {
105 self.data.as_slice()
106 }
107}
108
109pub struct K1Invites {
110 subsystem: Arc<InviteSubsystem>,
111 _ordering: Arc<K1TxnOrdering>,
112 peering: Arc<K1Peering>,
113}
114
115impl K1Invites {
116 pub fn open(
117 root: &Path,
118 ordering: Arc<K1TxnOrdering>,
119 peering: Arc<K1Peering>,
120 ) -> Result<Self, String> {
121 let projection = InviteProjection::open(root)?;
122 let snapshot = projection.snapshot()?;
123 let checkpoint = snapshot.checkpoint;
124 let mut state = FacadeState::new();
125 state
126 .issues
127 .try_reserve(snapshot.issues.len())
128 .map_err(|_| allocation_error())?;
129 state
130 .issue_codes
131 .try_reserve(snapshot.issues.len())
132 .map_err(|_| allocation_error())?;
133 state
134 .registrations
135 .try_reserve(snapshot.registrations.len())
136 .map_err(|_| allocation_error())?;
137 state
138 .registration_keys
139 .try_reserve(snapshot.registrations.len())
140 .map_err(|_| allocation_error())?;
141 for issue in snapshot.issues {
142 if state.issues.contains_key(&issue.code)
143 || state.issue_codes.contains_key(&issue.issue_id)
144 {
145 return Err("invite projection snapshot is inconsistent".to_owned());
146 }
147 state.issues.insert(issue.code, issue.issue_id);
148 state.issue_codes.insert(issue.issue_id, issue.code);
149 }
150 for accepted in snapshot.registrations {
151 let code = accepted.code;
152 if state.issue_codes.get(&accepted.issue_id) != Some(&code) {
153 return Err("invite projection snapshot is inconsistent".to_owned());
154 }
155 let registration_key = RegistrationKey::from_bytes(accepted.registration_key);
156 if state.registrations.contains_key(&code)
157 || state.registration_keys.contains_key(®istration_key)
158 {
159 return Err("invite projection snapshot is inconsistent".to_owned());
160 }
161 state.registrations.insert(
162 code,
163 Registration {
164 user_id: UserId(accepted.user_id),
165 registration_key,
166 data: Arc::new(accepted.data),
167 },
168 );
169 state.registration_keys.insert(registration_key, code);
170 }
171 let subsystem = Arc::new(InviteSubsystem {
172 projection: Mutex::new(projection),
173 state: Mutex::new(state),
174 });
175 let callback: Arc<dyn Subsystem> = subsystem.clone();
176 ordering.register_subsystem(subsystem_id()?, checkpoint, callback)?;
177 subsystem.ensure_available()?;
178 Ok(Self {
179 subsystem,
180 _ordering: ordering,
181 peering,
182 })
183 }
184
185 pub fn status(&self, code: &InviteCode) -> Result<InviteStatus, String> {
186 let state = lock_unpoison(&self.subsystem.state);
187 if !state.available {
188 return Err(REOPEN_REQUIRED.to_owned());
189 }
190 Ok(status_from_state(&state, code.bytes))
191 }
192
193 pub fn create(&self) -> Result<(TxId, InviteCode), String> {
194 enum Resolution {
195 Return(Result<TxId, String>),
196 Retry,
197 }
198
199 loop {
200 self.subsystem.ensure_available()?;
201 let subsystem_id = subsystem_id()?;
202 let mut code = InviteCode { bytes: [0_u8; 6] };
203 getrandom::fill(&mut code.bytes).map_err(|error| error.to_string())?;
204 let reserved = {
205 let mut state = lock_unpoison(&self.subsystem.state);
206 if !state.available {
207 return Err(REOPEN_REQUIRED.to_owned());
208 }
209 if state.issues.contains_key(&code.bytes)
210 || state.pending_issues.contains(&code.bytes)
211 {
212 false
213 } else {
214 state
215 .pending_issues
216 .try_reserve(1)
217 .map_err(|_| allocation_error())?;
218 state.pending_issues.insert(code.bytes);
219 true
220 }
221 };
222 if !reserved {
223 continue;
224 }
225 let payload = issue_payload(code.bytes);
226 let submission = self.peering.submit_txn(subsystem_id, &payload);
227 let resolution = {
228 let mut state = lock_unpoison(&self.subsystem.state);
229 let pending_was_present = state.pending_issues.remove(&code.bytes);
230 if !pending_was_present {
231 state.available = false;
232 Resolution::Return(Err(REOPEN_REQUIRED.to_owned()))
233 } else if !state.available {
234 Resolution::Return(Err(REOPEN_REQUIRED.to_owned()))
235 } else {
236 match submission {
237 Ok(returned_id) => match state.issues.get(&code.bytes).copied() {
238 Some(accepted_id) if accepted_id == returned_id => {
239 Resolution::Return(Ok(returned_id))
240 }
241 Some(_) => Resolution::Retry,
242 None => Resolution::Return(Err(
243 "issue transaction was not the winning Issue".to_owned(),
244 )),
245 },
246 Err(error) => match state.issues.get(&code.bytes).copied() {
247 Some(accepted_id) => Resolution::Return(Ok(accepted_id)),
248 None => Resolution::Return(Err(error)),
249 },
250 }
251 }
252 };
253 match resolution {
254 Resolution::Return(result) => return result.map(|id| (id, code)),
255 Resolution::Retry => continue,
256 }
257 }
258 }
259
260 pub fn consume_with_data(
261 &self,
262 code: &InviteCode,
263 registration_key: RegistrationKey,
264 data: &[u8],
265 ) -> Result<UserId, String> {
266 self.subsystem.ensure_available()?;
267 let code = code.bytes;
268 let shared_data = Arc::new(fallible_copy(data)?);
269 let candidate_cell = Arc::new(PendingResult::new());
270 let role = {
271 let mut state = lock_unpoison(&self.subsystem.state);
272 if !state.available {
273 return Err(REOPEN_REQUIRED.to_owned());
274 }
275 if let Some(accepted) = state.registrations.get(&code) {
276 ConsumeRole::Accepted(accepted.clone())
277 } else if let Some(pending) = state.pending_consumes.get(&code) {
278 ConsumeRole::Pending {
279 registration_key: pending.registration_key,
280 data: pending.data.clone(),
281 cell: pending.cell.clone(),
282 }
283 } else {
284 let issue_id = state
285 .issues
286 .get(&code)
287 .copied()
288 .ok_or_else(|| "unknown invite code".to_owned())?;
289 if state.registration_keys.contains_key(®istration_key)
290 || state
291 .pending_registration_keys
292 .contains_key(®istration_key)
293 {
294 return Err("registration key is already used by another invite".to_owned());
295 }
296 state
297 .pending_consumes
298 .try_reserve(1)
299 .map_err(|_| allocation_error())?;
300 state
301 .pending_registration_keys
302 .try_reserve(1)
303 .map_err(|_| allocation_error())?;
304 state.pending_consumes.insert(
305 code,
306 PendingConsume {
307 registration_key,
308 data: shared_data.clone(),
309 cell: candidate_cell.clone(),
310 },
311 );
312 state
313 .pending_registration_keys
314 .insert(registration_key, code);
315 ConsumeRole::Leader {
316 issue_id,
317 data: shared_data.clone(),
318 cell: candidate_cell.clone(),
319 }
320 }
321 };
322 match role {
323 ConsumeRole::Accepted(accepted) => {
324 if accepted.registration_key == registration_key && accepted.data.as_slice() == data
325 {
326 Ok(accepted.user_id)
327 } else {
328 Err("invite was already consumed with different registration data".to_owned())
329 }
330 }
331 ConsumeRole::Pending {
332 registration_key: pending_key,
333 data: pending_data,
334 cell,
335 } => {
336 if pending_key != registration_key || pending_data.as_slice() != data {
337 return Err("invite has a conflicting consume request in progress".to_owned());
338 }
339 wait_for_pending(&cell)
340 }
341 ConsumeRole::Leader {
342 issue_id,
343 data,
344 cell,
345 } => {
346 let payload = match consume_payload(issue_id, registration_key, data.as_slice()) {
347 Ok(payload) => payload,
348 Err(error) => {
349 return self.abandon_pending(code, registration_key, &cell, error);
350 }
351 };
352 let subsystem_id = match subsystem_id() {
353 Ok(subsystem_id) => subsystem_id,
354 Err(error) => {
355 return self.abandon_pending(code, registration_key, &cell, error);
356 }
357 };
358 if let Err(error) = self.subsystem.ensure_available() {
359 return self.abandon_pending(code, registration_key, &cell, error);
360 }
361 let submission = self.peering.submit_txn(subsystem_id, &payload);
362 self.complete_pending(code, registration_key, data.as_slice(), &cell, submission)
363 }
364 }
365 }
366
367 pub fn registrations(&self) -> Result<Vec<Registration>, String> {
368 self.subsystem.ensure_available()?;
369 let snapshot = {
370 let projection = lock_unpoison(&self.subsystem.projection);
371 projection.snapshot()
372 };
373 let snapshot = match snapshot {
374 Ok(snapshot) => snapshot,
375 Err(error) => {
376 self.subsystem.mark_unavailable();
377 return Err(error);
378 }
379 };
380 self.subsystem.ensure_available()?;
381 let mut registrations = Vec::new();
382 registrations
383 .try_reserve_exact(snapshot.registrations.len())
384 .map_err(|_| allocation_error())?;
385 for accepted in snapshot.registrations {
386 registrations.push(Registration {
387 user_id: UserId(accepted.user_id),
388 registration_key: RegistrationKey::from_bytes(accepted.registration_key),
389 data: Arc::new(accepted.data),
390 });
391 }
392 Ok(registrations)
393 }
394
395 fn abandon_pending(
396 &self,
397 code: [u8; 6],
398 registration_key: RegistrationKey,
399 cell: &Arc<PendingResult>,
400 error: String,
401 ) -> Result<UserId, String> {
402 let consistent = self.remove_pending(code, registration_key, cell);
403 let result = if consistent {
404 Err(error)
405 } else {
406 Err(REOPEN_REQUIRED.to_owned())
407 };
408 publish_pending(cell, &result);
409 result
410 }
411
412 fn complete_pending(
413 &self,
414 code: [u8; 6],
415 registration_key: RegistrationKey,
416 data: &[u8],
417 cell: &Arc<PendingResult>,
418 submission: Result<TxId, String>,
419 ) -> Result<UserId, String> {
420 let (available, accepted, consistent) = {
421 let mut state = lock_unpoison(&self.subsystem.state);
422 let available = state.available;
423 let accepted = state.registrations.get(&code).cloned();
424 let consistent = pending_matches(&state, code, registration_key, cell);
425 state.pending_consumes.remove(&code);
426 if state.pending_registration_keys.get(®istration_key) == Some(&code) {
427 state.pending_registration_keys.remove(®istration_key);
428 }
429 if !consistent {
430 state.available = false;
431 }
432 (available, accepted, consistent)
433 };
434 let result = if !available || !consistent {
435 Err(REOPEN_REQUIRED.to_owned())
436 } else {
437 match submission {
438 Err(error) => match accepted {
439 Some(accepted)
440 if accepted.registration_key == registration_key
441 && accepted.data.as_slice() == data =>
442 {
443 Ok(accepted.user_id)
444 }
445 _ => Err(error),
446 },
447 Ok(returned_id) => {
448 if let Some(accepted) = accepted {
449 if accepted.user_id.as_tx_id() == returned_id
450 && accepted.registration_key == registration_key
451 && accepted.data.as_slice() == data
452 {
453 Ok(accepted.user_id)
454 } else {
455 Err("consume transaction was not the accepted registration".to_owned())
456 }
457 } else {
458 Err("consume transaction was a semantic loser".to_owned())
459 }
460 }
461 }
462 };
463 publish_pending(cell, &result);
464 result
465 }
466
467 fn remove_pending(
468 &self,
469 code: [u8; 6],
470 registration_key: RegistrationKey,
471 cell: &Arc<PendingResult>,
472 ) -> bool {
473 let mut state = lock_unpoison(&self.subsystem.state);
474 let consistent = pending_matches(&state, code, registration_key, cell);
475 state.pending_consumes.remove(&code);
476 if state.pending_registration_keys.get(®istration_key) == Some(&code) {
477 state.pending_registration_keys.remove(®istration_key);
478 }
479 if !consistent {
480 state.available = false;
481 }
482 consistent
483 }
484}
485
486struct InviteSubsystem {
487 projection: Mutex<InviteProjection>,
488 state: Mutex<FacadeState>,
489}
490
491impl InviteSubsystem {
492 fn ensure_available(&self) -> Result<(), String> {
493 if lock_unpoison(&self.state).available {
494 Ok(())
495 } else {
496 Err(REOPEN_REQUIRED.to_owned())
497 }
498 }
499
500 fn apply_if_available(&self, action: InviteAction) -> Result<ApplyOutcome, String> {
501 let projection = lock_unpoison(&self.projection);
502 self.ensure_available()?;
503 projection.apply(action)
504 }
505
506 fn mark_unavailable(&self) {
507 lock_unpoison(&self.state).available = false;
508 }
509
510 fn fault(&self, error: String) -> Result<(), String> {
511 self.mark_unavailable();
512 Err(error)
513 }
514
515 fn accept_issue(&self, id: TxId, code: [u8; 6]) -> Result<(), String> {
516 let mut state = lock_unpoison(&self.state);
517 if !state.available {
518 return Err(REOPEN_REQUIRED.to_owned());
519 }
520 if state.issues.contains_key(&code) || state.issue_codes.contains_key(&id) {
521 state.available = false;
522 return Err("accepted Issue contradicted invite indexes".to_owned());
523 }
524 if state.issues.try_reserve(1).is_err() || state.issue_codes.try_reserve(1).is_err() {
525 state.available = false;
526 return Err(allocation_error());
527 }
528 state.issues.insert(code, id);
529 state.issue_codes.insert(id, code);
530 Ok(())
531 }
532
533 fn accept_registration(
534 &self,
535 id: TxId,
536 issue_id: TxId,
537 code: [u8; 6],
538 registration_key: [u8; 32],
539 data: &[u8],
540 ) -> Result<(), String> {
541 let copied_data = match fallible_copy(data) {
542 Ok(data) => Arc::new(data),
543 Err(error) => return self.fault(error),
544 };
545 let registration_key = RegistrationKey::from_bytes(registration_key);
546 let mut state = lock_unpoison(&self.state);
547 if !state.available {
548 return Err(REOPEN_REQUIRED.to_owned());
549 }
550 if state.issues.get(&code) != Some(&issue_id)
551 || state.issue_codes.get(&issue_id) != Some(&code)
552 || state.registrations.contains_key(&code)
553 || state.registration_keys.contains_key(®istration_key)
554 {
555 state.available = false;
556 return Err("accepted registration contradicted invite indexes".to_owned());
557 }
558 if state.registrations.try_reserve(1).is_err()
559 || state.registration_keys.try_reserve(1).is_err()
560 {
561 state.available = false;
562 return Err(allocation_error());
563 }
564 state.registrations.insert(
565 code,
566 Registration {
567 user_id: UserId(id),
568 registration_key,
569 data: copied_data,
570 },
571 );
572 state.registration_keys.insert(registration_key, code);
573 Ok(())
574 }
575}
576
577impl Subsystem for InviteSubsystem {
578 fn submit_txn(&self, id: TxId, payload: &[u8]) -> Result<(), String> {
579 self.ensure_available()?;
580 let parsed = match parse_payload(id, payload) {
581 Ok(parsed) => parsed,
582 Err(error) => return self.fault(error),
583 };
584 let action = match projection_action(parsed) {
585 Ok(action) => action,
586 Err(error) => return self.fault(error),
587 };
588 let outcome = match self.apply_if_available(action) {
589 Ok(outcome) => outcome,
590 Err(error) => return self.fault(error),
591 };
592 match outcome {
593 ApplyOutcome::IssueAccepted(issue) => match parsed {
594 ParsedAction::Issue { id, code } if issue.issue_id == id && issue.code == code => {
595 self.accept_issue(issue.issue_id, issue.code)
596 }
597 _ => self.fault("projection Issue outcome contradicted action".to_owned()),
598 },
599 ApplyOutcome::RegistrationAccepted(accepted) => match parsed {
600 ParsedAction::Consume {
601 id,
602 issue_id,
603 registration_key,
604 data,
605 } if accepted.user_id == id
606 && accepted.issue_id == issue_id
607 && accepted.registration_key == registration_key
608 && accepted.data.as_slice() == data =>
609 {
610 self.accept_registration(
611 accepted.user_id,
612 accepted.issue_id,
613 accepted.code,
614 accepted.registration_key,
615 accepted.data.as_slice(),
616 )
617 }
618 _ => self.fault("projection registration outcome contradicted action".to_owned()),
619 },
620 ApplyOutcome::Noop => Ok(()),
621 }
622 }
623
624 fn reorg(&self) -> Result<(), String> {
625 self.mark_unavailable();
626 let projection = lock_unpoison(&self.projection);
627 projection.discard()
628 }
629}
630
631struct FacadeState {
632 available: bool,
633 issues: HashMap<[u8; 6], TxId>,
634 issue_codes: HashMap<TxId, [u8; 6]>,
635 registrations: HashMap<[u8; 6], Registration>,
636 registration_keys: HashMap<RegistrationKey, [u8; 6]>,
637 pending_issues: HashSet<[u8; 6]>,
638 pending_consumes: HashMap<[u8; 6], PendingConsume>,
639 pending_registration_keys: HashMap<RegistrationKey, [u8; 6]>,
640}
641
642impl FacadeState {
643 fn new() -> Self {
644 Self {
645 available: true,
646 issues: HashMap::new(),
647 issue_codes: HashMap::new(),
648 registrations: HashMap::new(),
649 registration_keys: HashMap::new(),
650 pending_issues: HashSet::new(),
651 pending_consumes: HashMap::new(),
652 pending_registration_keys: HashMap::new(),
653 }
654 }
655}
656
657struct PendingConsume {
658 registration_key: RegistrationKey,
659 data: Arc<Vec<u8>>,
660 cell: Arc<PendingResult>,
661}
662
663struct PendingResult {
664 outcome: Mutex<Option<Result<UserId, String>>>,
665 ready: Condvar,
666}
667
668impl PendingResult {
669 fn new() -> Self {
670 Self {
671 outcome: Mutex::new(None),
672 ready: Condvar::new(),
673 }
674 }
675}
676
677enum ConsumeRole {
678 Accepted(Registration),
679 Pending {
680 registration_key: RegistrationKey,
681 data: Arc<Vec<u8>>,
682 cell: Arc<PendingResult>,
683 },
684 Leader {
685 issue_id: TxId,
686 data: Arc<Vec<u8>>,
687 cell: Arc<PendingResult>,
688 },
689}
690
691#[derive(Clone, Copy)]
692enum ParsedAction<'a> {
693 Issue {
694 id: TxId,
695 code: [u8; 6],
696 },
697 Consume {
698 id: TxId,
699 issue_id: TxId,
700 registration_key: [u8; 32],
701 data: &'a [u8],
702 },
703}
704
705fn parse_payload<'a>(id: TxId, payload: &'a [u8]) -> Result<ParsedAction<'a>, String> {
706 if payload.len() < 2 || payload[0] != 3 {
707 return Err("malformed k1-invites payload".to_owned());
708 }
709 match payload[1] {
710 1 if payload.len() == ISSUE_LENGTH => Ok(ParsedAction::Issue {
711 id,
712 code: array_6(&payload[2..8]),
713 }),
714 2 if payload.len() >= CONSUME_HEADER_LENGTH => Ok(ParsedAction::Consume {
715 id,
716 issue_id: tx_id_from_slice(&payload[2..14]),
717 registration_key: array_32(&payload[14..46]),
718 data: &payload[46..],
719 }),
720 _ => Err("malformed k1-invites payload".to_owned()),
721 }
722}
723
724fn projection_action(parsed: ParsedAction<'_>) -> Result<InviteAction, String> {
725 match parsed {
726 ParsedAction::Issue { id, code } => Ok(InviteAction::Issue { id, code }),
727 ParsedAction::Consume {
728 id,
729 issue_id,
730 registration_key,
731 data,
732 } => Ok(InviteAction::Consume {
733 id,
734 issue_id,
735 registration_key,
736 data: fallible_copy(data)?,
737 }),
738 }
739}
740
741fn status_from_state(state: &FacadeState, code: [u8; 6]) -> InviteStatus {
742 if state.registrations.contains_key(&code) {
743 InviteStatus::Consumed
744 } else if state.issues.contains_key(&code) {
745 InviteStatus::Unused
746 } else {
747 InviteStatus::Unknown
748 }
749}
750
751fn issue_payload(code: [u8; 6]) -> [u8; ISSUE_LENGTH] {
752 let mut payload = [0_u8; ISSUE_LENGTH];
753 payload[0] = 3;
754 payload[1] = 1;
755 payload[2..].copy_from_slice(&code);
756 payload
757}
758
759fn consume_payload(
760 issue_id: TxId,
761 registration_key: RegistrationKey,
762 data: &[u8],
763) -> Result<Vec<u8>, String> {
764 let total = CONSUME_HEADER_LENGTH
765 .checked_add(data.len())
766 .ok_or_else(|| "consume payload length overflow".to_owned())?;
767 let mut payload = Vec::new();
768 payload
769 .try_reserve_exact(total)
770 .map_err(|_| allocation_error())?;
771 payload.extend_from_slice(&[3, 2]);
772 payload.extend_from_slice(issue_id.as_bytes());
773 payload.extend_from_slice(registration_key.as_bytes());
774 payload.extend_from_slice(data);
775 Ok(payload)
776}
777
778fn subsystem_id() -> Result<SubsystemId, String> {
779 SubsystemId::from_str(SUBSYSTEM_NAME).map_err(|error| error.to_string())
780}
781
782fn tx_id_from_slice(bytes: &[u8]) -> TxId {
783 let mut raw = [0_u8; 12];
784 raw.copy_from_slice(bytes);
785 TxId::from_bytes(raw)
786}
787
788fn array_6(bytes: &[u8]) -> [u8; 6] {
789 let mut array = [0_u8; 6];
790 array.copy_from_slice(bytes);
791 array
792}
793
794fn array_32(bytes: &[u8]) -> [u8; 32] {
795 let mut array = [0_u8; 32];
796 array.copy_from_slice(bytes);
797 array
798}
799
800fn fallible_copy(bytes: &[u8]) -> Result<Vec<u8>, String> {
801 let mut copied = Vec::new();
802 copied
803 .try_reserve_exact(bytes.len())
804 .map_err(|_| allocation_error())?;
805 copied.extend_from_slice(bytes);
806 Ok(copied)
807}
808
809fn allocation_error() -> String {
810 "memory allocation failed".to_owned()
811}
812
813fn pending_matches(
814 state: &FacadeState,
815 code: [u8; 6],
816 registration_key: RegistrationKey,
817 cell: &Arc<PendingResult>,
818) -> bool {
819 state.pending_consumes.get(&code).is_some_and(|pending| {
820 pending.registration_key == registration_key && Arc::ptr_eq(&pending.cell, cell)
821 }) && state.pending_registration_keys.get(®istration_key) == Some(&code)
822}
823
824fn publish_pending(cell: &PendingResult, result: &Result<UserId, String>) {
825 {
826 let mut outcome = lock_unpoison(&cell.outcome);
827 *outcome = Some(result.clone());
828 }
829 cell.ready.notify_all();
830}
831
832fn wait_for_pending(cell: &PendingResult) -> Result<UserId, String> {
833 let mut outcome = lock_unpoison(&cell.outcome);
834 loop {
835 if let Some(result) = outcome.as_ref() {
836 return result.clone();
837 }
838 outcome = match cell.ready.wait(outcome) {
839 Ok(outcome) => outcome,
840 Err(poisoned) => poisoned.into_inner(),
841 };
842 }
843}
844
845fn lock_unpoison<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
846 match mutex.lock() {
847 Ok(guard) => guard,
848 Err(poisoned) => poisoned.into_inner(),
849 }
850}
851
852#[cfg(test)]
853mod tests {
854 use super::*;
855
856 fn tx_id(byte: u8) -> TxId {
857 TxId::from_bytes([byte; 12])
858 }
859
860 #[test]
861 fn user_id_converts_from_and_to_tx_id() {
862 let txid = tx_id(6);
863 assert_eq!(UserId::from_tx_id(txid).as_tx_id(), txid);
864 }
865
866 #[test]
867 fn invite_status_uses_only_accepted_state() {
868 let code = [4; 6];
869 let registration_key = RegistrationKey::from_bytes([5; 32]);
870 let mut state = FacadeState::new();
871
872 assert_eq!(status_from_state(&state, code), InviteStatus::Unknown);
873 state.issues.insert(code, tx_id(1));
874 assert_eq!(status_from_state(&state, code), InviteStatus::Unused);
875
876 state.pending_consumes.insert(
877 code,
878 PendingConsume {
879 registration_key,
880 data: Arc::new(b"pending".to_vec()),
881 cell: Arc::new(PendingResult::new()),
882 },
883 );
884 assert_eq!(status_from_state(&state, code), InviteStatus::Unused);
885
886 state.registrations.insert(
887 code,
888 Registration {
889 user_id: UserId(tx_id(2)),
890 registration_key,
891 data: Arc::new(b"accepted".to_vec()),
892 },
893 );
894 assert_eq!(status_from_state(&state, code), InviteStatus::Consumed);
895
896 let copied = InviteStatus::Consumed;
897 assert_eq!(copied, InviteStatus::Consumed);
898 assert_eq!(format!("{copied:?}"), "Consumed");
899 }
900
901 #[test]
902 fn public_status_survives_restart() {
903 use std::sync::atomic::{AtomicU64, Ordering};
904
905 static NEXT_TEMP_ROOT: AtomicU64 = AtomicU64::new(0);
906
907 let unique = NEXT_TEMP_ROOT.fetch_add(1, Ordering::Relaxed);
908 let root = std::env::temp_dir().join(format!(
909 "kcode-k1-invites-status-{}-{unique}",
910 std::process::id()
911 ));
912 let ordering_root = root.join("ordering");
913 let peering_root = root.join("peering");
914 let invites_root = root.join("invites");
915
916 let ordering =
917 Arc::new(K1TxnOrdering::open(&ordering_root).expect("open temporary ordering"));
918 let peering = Arc::new(
919 K1Peering::open(&peering_root, Arc::clone(&ordering)).expect("open temporary peering"),
920 );
921 let invites = K1Invites::open(&invites_root, Arc::clone(&ordering), Arc::clone(&peering))
922 .expect("open temporary invites");
923
924 let unknown = InviteCode::from_str("AAAAAAAA").expect("canonical unknown code");
925 assert_eq!(
926 invites.status(&unknown).expect("unknown status"),
927 InviteStatus::Unknown
928 );
929
930 let (_, code) = invites.create().expect("create invite");
931 let retained_code = code.expose();
932 assert_eq!(
933 invites.status(&code).expect("unused status"),
934 InviteStatus::Unused
935 );
936 invites
937 .consume_with_data(&code, RegistrationKey::from_bytes([8; 32]), b"account")
938 .expect("consume invite");
939 assert_eq!(
940 invites.status(&code).expect("consumed status"),
941 InviteStatus::Consumed
942 );
943
944 drop(invites);
945 drop(peering);
946 drop(ordering);
947
948 let ordering =
949 Arc::new(K1TxnOrdering::open(&ordering_root).expect("reopen temporary ordering"));
950 let peering = Arc::new(
951 K1Peering::open(&peering_root, Arc::clone(&ordering))
952 .expect("reopen temporary peering"),
953 );
954 let invites = K1Invites::open(&invites_root, Arc::clone(&ordering), Arc::clone(&peering))
955 .expect("reopen temporary invites");
956 let code = InviteCode::from_str(&retained_code).expect("parse retained code");
957 assert_eq!(
958 invites.status(&code).expect("restarted status"),
959 InviteStatus::Consumed
960 );
961
962 drop(invites);
963 drop(peering);
964 drop(ordering);
965 std::fs::remove_dir_all(&root).expect("remove temporary root");
966 }
967
968 #[test]
969 fn invite_code_is_strict_and_round_trips() {
970 let code = InviteCode {
971 bytes: [0, 1, 2, 253, 254, 255],
972 };
973 let exposed = code.expose();
974 assert_eq!(exposed.len(), 8);
975 let parsed = InviteCode::from_str(&exposed).expect("canonical code");
976 assert_eq!(parsed.expose(), exposed);
977 for invalid in ["", "AAAAAAA", "AAAAAAAA=", "AAAAAAAAA", "AAAAAA+/", "åååå"] {
978 assert!(InviteCode::from_str(invalid).is_err());
979 }
980 }
981
982 #[test]
983 fn invite_code_is_ordinary_copyable_data() {
984 fn assert_ordinary_data<T: Clone + Copy + std::fmt::Debug + Eq + std::hash::Hash>() {}
985
986 assert_ordinary_data::<InviteCode>();
987 let code = InviteCode {
988 bytes: [1, 2, 3, 4, 5, 6],
989 };
990 let copied = code;
991 assert_eq!(copied, code);
992 }
993
994 #[test]
995 fn version_three_wire_uses_code_only_in_issue() {
996 let callback_id = tx_id(9);
997 let issue_id = tx_id(4);
998 let code = [1, 2, 3, 4, 5, 6];
999 let key = RegistrationKey::from_bytes([7; 32]);
1000 let issue = issue_payload(code);
1001 assert_eq!(issue, [3, 1, 1, 2, 3, 4, 5, 6]);
1002 match parse_payload(callback_id, &issue).expect("Issue") {
1003 ParsedAction::Issue { id, code: parsed } => {
1004 assert_eq!(id, callback_id);
1005 assert_eq!(parsed, code);
1006 }
1007 ParsedAction::Consume { .. } => panic!("wrong action"),
1008 }
1009
1010 let consume = consume_payload(issue_id, key, b"opaque\0bytes").expect("Consume payload");
1011 let mut expected = vec![3, 2];
1012 expected.extend_from_slice(issue_id.as_bytes());
1013 expected.extend_from_slice(&[7; 32]);
1014 expected.extend_from_slice(b"opaque\0bytes");
1015 assert_eq!(consume, expected);
1016 assert_eq!(consume.len(), CONSUME_HEADER_LENGTH + 12);
1017 match parse_payload(callback_id, &consume).expect("Consume") {
1018 ParsedAction::Consume {
1019 id,
1020 issue_id: parsed_issue,
1021 registration_key,
1022 data,
1023 } => {
1024 assert_eq!(id, callback_id);
1025 assert_eq!(parsed_issue, issue_id);
1026 assert_eq!(registration_key, [7; 32]);
1027 assert_eq!(data, b"opaque\0bytes");
1028 }
1029 ParsedAction::Issue { .. } => panic!("wrong action"),
1030 }
1031 }
1032
1033 #[test]
1034 fn malformed_and_legacy_payloads_are_rejected() {
1035 let id = tx_id(1);
1036 let mut extended_issue = vec![0; ISSUE_LENGTH + 1];
1037 extended_issue[0] = 3;
1038 extended_issue[1] = 1;
1039 let mut short_consume = vec![0; CONSUME_HEADER_LENGTH - 1];
1040 short_consume[0] = 3;
1041 short_consume[1] = 2;
1042 let mut legacy_issue = vec![0; ISSUE_LENGTH];
1043 legacy_issue[0] = 2;
1044 legacy_issue[1] = 1;
1045 let mut legacy_consume = vec![0; CONSUME_HEADER_LENGTH];
1046 legacy_consume[0] = 2;
1047 legacy_consume[1] = 2;
1048 let malformed = vec![
1049 Vec::new(),
1050 vec![3],
1051 vec![3, 3],
1052 vec![3, 1],
1053 extended_issue,
1054 short_consume,
1055 legacy_issue,
1056 legacy_consume,
1057 ];
1058 for payload in malformed {
1059 assert!(parse_payload(id, &payload).is_err());
1060 }
1061 }
1062
1063 #[test]
1064 fn apply_waiting_behind_reorg_linearization_does_not_reach_projection() {
1065 use std::sync::atomic::{AtomicU64, Ordering};
1066 use std::sync::mpsc;
1067 use std::thread;
1068
1069 static NEXT_TEMP_ROOT: AtomicU64 = AtomicU64::new(0);
1070
1071 let unique = NEXT_TEMP_ROOT.fetch_add(1, Ordering::Relaxed);
1072 let root = std::env::temp_dir().join(format!(
1073 "kcode-k1-invites-reorg-{}-{unique}",
1074 std::process::id()
1075 ));
1076 let projection = InviteProjection::open(&root).expect("open temporary projection");
1077 let subsystem = Arc::new(InviteSubsystem {
1078 projection: Mutex::new(projection),
1079 state: Mutex::new(FacadeState::new()),
1080 });
1081 let projection_guard = lock_unpoison(&subsystem.projection);
1082 let worker_subsystem = Arc::clone(&subsystem);
1083 let (ready_sender, ready_receiver) = mpsc::channel();
1084 let worker = thread::spawn(move || {
1085 ready_sender.send(()).expect("signal helper readiness");
1086 worker_subsystem.apply_if_available(InviteAction::Issue {
1087 id: tx_id(8),
1088 code: [9; 6],
1089 })
1090 });
1091
1092 ready_receiver.recv().expect("receive helper readiness");
1093 subsystem.mark_unavailable();
1094 drop(projection_guard);
1095
1096 match worker.join().expect("join helper thread") {
1097 Err(error) => assert_eq!(error, REOPEN_REQUIRED),
1098 Ok(_) => panic!("unavailable helper unexpectedly applied the Issue"),
1099 }
1100 drop(subsystem);
1101
1102 let projection = InviteProjection::open(&root).expect("reopen temporary projection");
1103 let snapshot = projection
1104 .snapshot()
1105 .expect("snapshot temporary projection");
1106 assert!(snapshot.checkpoint.is_none());
1107 assert!(snapshot.issues.is_empty());
1108 assert!(snapshot.registrations.is_empty());
1109 drop(projection);
1110 std::fs::remove_dir_all(&root).expect("remove temporary projection");
1111 }
1112}