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(®istration_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(®istration_key)
308 || state
309 .pending_registration_keys
310 .contains_key(®istration_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(®istration_key) == Some(&commitment) {
456 state.pending_registration_keys.remove(®istration_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(®istration_key) == Some(&commitment) {
506 state.pending_registration_keys.remove(®istration_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(®istration_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(®istration_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}