Skip to main content

kcode_k1_invites/
lib.rs

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(&registration_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(&registration_key)
290                    || state
291                        .pending_registration_keys
292                        .contains_key(&registration_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(&registration_key) == Some(&code) {
427                state.pending_registration_keys.remove(&registration_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(&registration_key) == Some(&code) {
477            state.pending_registration_keys.remove(&registration_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(&registration_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(&registration_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}