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