Skip to main content

kcode_k1_invites/
lib.rs

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