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(®istration_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(®istration_key)
312 || state
313 .pending_registration_keys
314 .contains_key(®istration_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(®istration_key) == Some(&commitment) {
460 state.pending_registration_keys.remove(®istration_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(®istration_key) == Some(&commitment) {
510 state.pending_registration_keys.remove(®istration_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(®istration_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(®istration_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}