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