1use std::io::Write as _;
7use std::path::{Path, PathBuf};
8
9use serde::{Deserialize, Serialize};
10
11use crate::util::is_lower_hex_64;
12
13const FETCH_REQUEST_SCHEMA: &str = "igc-net/fetch-request";
14const FETCH_REQUEST_SCHEMA_VERSION: u32 = 1;
15const GROUP_FETCH_REQUEST_SCHEMA: &str = "igc-net/group-fetch-request";
16const GROUP_FETCH_REQUEST_SCHEMA_VERSION: u32 = 1;
17const SEQ_NUM_DIRNAME: &str = "seq-nums";
18const GROUP_SEQ_NUM_DIRNAME: &str = "seq-nums-group";
19
20#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
24#[serde(rename_all = "snake_case")]
25pub enum ArtifactClass {
26 ProtectedRawCompanion,
27 PrivateRawIgc,
28}
29
30#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct FetchProof {
36 pub schema: String,
37 pub schema_version: u32,
38 pub raw_igc_hash: String,
39 pub artifact_class: ArtifactClass,
40 pub requester_key: String,
41 pub seq_num: u64,
42 pub signature: String,
43}
44
45#[derive(Serialize)]
47struct FetchProofPayload<'a> {
48 schema: &'static str,
49 schema_version: u32,
50 raw_igc_hash: &'a str,
51 artifact_class: &'a ArtifactClass,
52 requester_key: &'a str,
53 seq_num: u64,
54}
55
56#[derive(Debug, thiserror::Error)]
59pub enum FetchProofError {
60 #[error("JSON: {0}")]
61 Json(#[from] serde_json::Error),
62 #[error("raw_igc_hash must be 64 lowercase hex chars")]
63 InvalidHash,
64 #[error("seq_num must be ≥ 1")]
65 SeqNumZero,
66 #[error("signature must be 128 lowercase hex chars")]
67 InvalidSignatureEncoding,
68 #[error("requester_key must be a valid 64-char lowercase hex Ed25519 public key")]
69 InvalidRequesterKey,
70 #[error("signature verification failed")]
71 SignatureVerification,
72 #[error("requester_key does not match the authorized public key")]
73 RequesterKeyMismatch,
74 #[error("signed artifact_class does not match the expected artifact class")]
75 ArtifactClassMismatch,
76 #[error("seq_num {got} is not strictly greater than last seen {last_seen}")]
77 SeqNumNotMonotonic { got: u64, last_seen: u64 },
78}
79
80pub fn sign_fetch_proof(
87 raw_igc_hash: &str,
88 artifact_class: ArtifactClass,
89 seq_num: u64,
90 private_key: &iroh::SecretKey,
91) -> Result<FetchProof, FetchProofError> {
92 if !is_lower_hex_64(raw_igc_hash) {
93 return Err(FetchProofError::InvalidHash);
94 }
95 if seq_num == 0 {
96 return Err(FetchProofError::SeqNumZero);
97 }
98
99 let requester_key = private_key.public().to_string();
100 let payload = FetchProofPayload {
101 schema: FETCH_REQUEST_SCHEMA,
102 schema_version: FETCH_REQUEST_SCHEMA_VERSION,
103 raw_igc_hash,
104 artifact_class: &artifact_class,
105 requester_key: &requester_key,
106 seq_num,
107 };
108 let signing_bytes = json_canon::to_vec(&payload)?;
109 let signature = hex::encode(private_key.sign(&signing_bytes).to_bytes());
110
111 Ok(FetchProof {
112 schema: FETCH_REQUEST_SCHEMA.to_string(),
113 schema_version: FETCH_REQUEST_SCHEMA_VERSION,
114 raw_igc_hash: raw_igc_hash.to_string(),
115 artifact_class,
116 requester_key,
117 seq_num,
118 signature,
119 })
120}
121
122pub fn verify_fetch_proof(
135 proof: &FetchProof,
136 authorized_public_key: &iroh::PublicKey,
137 expected_artifact_class: &ArtifactClass,
138 last_seen_seq_num: u64,
139) -> Result<(), FetchProofError> {
140 if proof.requester_key != authorized_public_key.to_string() {
142 return Err(FetchProofError::RequesterKeyMismatch);
143 }
144
145 if &proof.artifact_class != expected_artifact_class {
147 return Err(FetchProofError::ArtifactClassMismatch);
148 }
149
150 if proof.seq_num == 0 {
152 return Err(FetchProofError::SeqNumZero);
153 }
154 if proof.seq_num <= last_seen_seq_num {
155 return Err(FetchProofError::SeqNumNotMonotonic {
156 got: proof.seq_num,
157 last_seen: last_seen_seq_num,
158 });
159 }
160
161 if !is_lower_hex_64(&proof.raw_igc_hash) {
163 return Err(FetchProofError::InvalidHash);
164 }
165
166 let signature = decode_signature_hex(&proof.signature)?;
168 let payload = FetchProofPayload {
169 schema: FETCH_REQUEST_SCHEMA,
170 schema_version: FETCH_REQUEST_SCHEMA_VERSION,
171 raw_igc_hash: &proof.raw_igc_hash,
172 artifact_class: &proof.artifact_class,
173 requester_key: &proof.requester_key,
174 seq_num: proof.seq_num,
175 };
176 let signing_bytes = json_canon::to_vec(&payload)?;
177 authorized_public_key
178 .verify(&signing_bytes, &signature)
179 .map_err(|_| FetchProofError::SignatureVerification)?;
180
181 Ok(())
182}
183
184fn decode_signature_hex(value: &str) -> Result<iroh::Signature, FetchProofError> {
185 if value.len() != 128
186 || !value
187 .bytes()
188 .all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f'))
189 {
190 return Err(FetchProofError::InvalidSignatureEncoding);
191 }
192 let bytes = hex::decode(value).map_err(|_| FetchProofError::InvalidSignatureEncoding)?;
193 let sig_bytes: [u8; 64] = bytes
194 .try_into()
195 .map_err(|_| FetchProofError::InvalidSignatureEncoding)?;
196 Ok(iroh::Signature::from_bytes(&sig_bytes))
197}
198
199#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct GroupFetchProof {
205 pub schema: String,
206 pub schema_version: u32,
207 pub raw_igc_hash: String,
208 pub artifact_class: ArtifactClass,
209 pub requester_pilot_id: String,
210 pub group_id: String,
211 pub seq_num: u64,
212 pub signature: String,
213}
214
215#[derive(Serialize)]
217struct GroupFetchProofPayload<'a> {
218 schema: &'static str,
219 schema_version: u32,
220 raw_igc_hash: &'a str,
221 artifact_class: &'a ArtifactClass,
222 requester_pilot_id: &'a str,
223 group_id: &'a str,
224 seq_num: u64,
225}
226
227#[derive(Debug, thiserror::Error)]
230pub enum GroupFetchProofError {
231 #[error("JSON: {0}")]
232 Json(#[from] serde_json::Error),
233 #[error("raw_igc_hash must be 64 lowercase hex chars")]
234 InvalidHash,
235 #[error("seq_num must be ≥ 1")]
236 SeqNumZero,
237 #[error("signature must be 128 lowercase hex chars")]
238 InvalidSignatureEncoding,
239 #[error("requester_pilot_id is not a valid PilotId (expected igcnet:id:<64-hex>)")]
240 InvalidRequesterPilotId,
241 #[error("group_id is not a valid GroupId (expected igcnet:group:<32-hex>)")]
242 InvalidGroupId,
243 #[error("signature verification failed")]
244 SignatureVerification,
245 #[error("signed artifact_class does not match the expected artifact class")]
246 ArtifactClassMismatch,
247 #[error("seq_num {got} is not strictly greater than last seen {last_seen}")]
248 SeqNumNotMonotonic { got: u64, last_seen: u64 },
249}
250
251pub fn sign_group_fetch_proof(
255 raw_igc_hash: &str,
256 artifact_class: ArtifactClass,
257 requester_pilot_id: &str,
258 group_id: &str,
259 seq_num: u64,
260 pilot_root_secret_key: &iroh::SecretKey,
261) -> Result<GroupFetchProof, GroupFetchProofError> {
262 if !is_lower_hex_64(raw_igc_hash) {
263 return Err(GroupFetchProofError::InvalidHash);
264 }
265 if seq_num == 0 {
266 return Err(GroupFetchProofError::SeqNumZero);
267 }
268 parse_pilot_id(requester_pilot_id)?;
269 parse_group_id(group_id)?;
270
271 let payload = GroupFetchProofPayload {
272 schema: GROUP_FETCH_REQUEST_SCHEMA,
273 schema_version: GROUP_FETCH_REQUEST_SCHEMA_VERSION,
274 raw_igc_hash,
275 artifact_class: &artifact_class,
276 requester_pilot_id,
277 group_id,
278 seq_num,
279 };
280 let signing_bytes = json_canon::to_vec(&payload)?;
281 let signature = hex::encode(pilot_root_secret_key.sign(&signing_bytes).to_bytes());
282
283 Ok(GroupFetchProof {
284 schema: GROUP_FETCH_REQUEST_SCHEMA.to_string(),
285 schema_version: GROUP_FETCH_REQUEST_SCHEMA_VERSION,
286 raw_igc_hash: raw_igc_hash.to_string(),
287 artifact_class,
288 requester_pilot_id: requester_pilot_id.to_string(),
289 group_id: group_id.to_string(),
290 seq_num,
291 signature,
292 })
293}
294
295pub fn verify_group_fetch_proof(
305 proof: &GroupFetchProof,
306 expected_artifact_class: &ArtifactClass,
307 last_seen_seq_num: u64,
308) -> Result<(), GroupFetchProofError> {
309 let authorized_public_key = parse_pilot_id(&proof.requester_pilot_id)?;
310 parse_group_id(&proof.group_id)?;
311
312 if &proof.artifact_class != expected_artifact_class {
313 return Err(GroupFetchProofError::ArtifactClassMismatch);
314 }
315 if proof.seq_num == 0 {
316 return Err(GroupFetchProofError::SeqNumZero);
317 }
318 if proof.seq_num <= last_seen_seq_num {
319 return Err(GroupFetchProofError::SeqNumNotMonotonic {
320 got: proof.seq_num,
321 last_seen: last_seen_seq_num,
322 });
323 }
324 if !is_lower_hex_64(&proof.raw_igc_hash) {
325 return Err(GroupFetchProofError::InvalidHash);
326 }
327
328 let signature = decode_signature_hex(&proof.signature)
329 .map_err(|_| GroupFetchProofError::InvalidSignatureEncoding)?;
330 let payload = GroupFetchProofPayload {
331 schema: GROUP_FETCH_REQUEST_SCHEMA,
332 schema_version: GROUP_FETCH_REQUEST_SCHEMA_VERSION,
333 raw_igc_hash: &proof.raw_igc_hash,
334 artifact_class: &proof.artifact_class,
335 requester_pilot_id: &proof.requester_pilot_id,
336 group_id: &proof.group_id,
337 seq_num: proof.seq_num,
338 };
339 let signing_bytes = json_canon::to_vec(&payload)?;
340 authorized_public_key
341 .verify(&signing_bytes, &signature)
342 .map_err(|_| GroupFetchProofError::SignatureVerification)?;
343
344 Ok(())
345}
346
347fn parse_pilot_id(pilot_id: &str) -> Result<iroh::PublicKey, GroupFetchProofError> {
348 let key_hex = pilot_id
349 .strip_prefix("igcnet:id:")
350 .filter(|h| is_lower_hex_64(h))
351 .ok_or(GroupFetchProofError::InvalidRequesterPilotId)?;
352 let bytes = hex::decode(key_hex).map_err(|_| GroupFetchProofError::InvalidRequesterPilotId)?;
353 let arr: [u8; 32] = bytes
354 .try_into()
355 .map_err(|_| GroupFetchProofError::InvalidRequesterPilotId)?;
356 iroh::PublicKey::from_bytes(&arr).map_err(|_| GroupFetchProofError::InvalidRequesterPilotId)
357}
358
359fn parse_group_id(group_id: &str) -> Result<(), GroupFetchProofError> {
360 let id_hex = group_id
361 .strip_prefix("igcnet:group:")
362 .ok_or(GroupFetchProofError::InvalidGroupId)?;
363 if id_hex.len() != 32 || !id_hex.bytes().all(|b| matches!(b, b'0'..=b'9' | b'a'..=b'f')) {
364 return Err(GroupFetchProofError::InvalidGroupId);
365 }
366 Ok(())
367}
368
369pub struct SeqNumStore {
377 root: PathBuf,
378}
379
380#[derive(Debug, thiserror::Error)]
381pub enum SeqNumStoreError {
382 #[error("I/O: {0}")]
383 Io(#[from] std::io::Error),
384 #[error("JSON: {0}")]
385 Json(#[from] serde_json::Error),
386 #[error("new seq_num {new} is not greater than stored {stored}")]
387 NotMonotonic { new: u64, stored: u64 },
388}
389
390#[derive(Serialize, Deserialize)]
391struct SeqNumRecord {
392 seq_num: u64,
393}
394
395impl SeqNumStore {
396 pub fn open(root: impl Into<PathBuf>) -> Self {
397 Self { root: root.into() }
398 }
399
400 pub fn for_data_dir(data_dir: impl AsRef<Path>) -> Self {
401 Self::open(data_dir.as_ref().join(SEQ_NUM_DIRNAME))
402 }
403
404 pub fn for_group_fetch_data_dir(data_dir: impl AsRef<Path>) -> Self {
405 Self::open(data_dir.as_ref().join(GROUP_SEQ_NUM_DIRNAME))
406 }
407
408 pub fn last_seen(&self, requester_key_hex: &str) -> Result<u64, SeqNumStoreError> {
411 let path = self.seq_file_path(requester_key_hex);
412 if !path.exists() {
413 return Ok(0);
414 }
415 let bytes = std::fs::read(&path)?;
416 let record: SeqNumRecord = serde_json::from_slice(&bytes)?;
417 Ok(record.seq_num)
418 }
419
420 pub fn advance(
425 &self,
426 requester_key_hex: &str,
427 new_seq_num: u64,
428 ) -> Result<(), SeqNumStoreError> {
429 let current = self.last_seen(requester_key_hex)?;
430 if new_seq_num <= current {
431 return Err(SeqNumStoreError::NotMonotonic {
432 new: new_seq_num,
433 stored: current,
434 });
435 }
436 self.write_seq_num(requester_key_hex, new_seq_num)
437 }
438
439 fn seq_file_path(&self, requester_key_hex: &str) -> PathBuf {
440 self.root.join(format!("{requester_key_hex}.json"))
441 }
442
443 fn write_seq_num(&self, requester_key_hex: &str, seq_num: u64) -> Result<(), SeqNumStoreError> {
444 std::fs::create_dir_all(&self.root)?;
445 let record = SeqNumRecord { seq_num };
446 let data = serde_json::to_vec(&record)?;
447 let tmp_name = format!(".{requester_key_hex}-{}.tmp", rand::random::<u64>());
448 let tmp_path = self.root.join(tmp_name);
449 {
450 let mut file = std::fs::OpenOptions::new()
451 .create(true)
452 .write(true)
453 .truncate(true)
454 .open(&tmp_path)?;
455 file.write_all(&data)?;
456 file.flush()?;
457 file.sync_all()?;
458 }
459 std::fs::rename(&tmp_path, self.seq_file_path(requester_key_hex))?;
460 Ok(())
461 }
462}
463
464#[cfg(test)]
467mod tests {
468 use super::*;
469
470 fn secret_key(byte: u8) -> iroh::SecretKey {
471 iroh::SecretKey::from_bytes(&[byte; 32])
472 }
473
474 fn valid_hash() -> &'static str {
475 "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890"
476 }
477
478 #[test]
481 fn sign_round_trip_private_raw_igc() {
482 let key = secret_key(1);
483 let proof = sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &key).unwrap();
484
485 assert_eq!(proof.schema, "igc-net/fetch-request");
486 assert_eq!(proof.schema_version, 1);
487 assert_eq!(proof.raw_igc_hash, valid_hash());
488 assert_eq!(proof.artifact_class, ArtifactClass::PrivateRawIgc);
489 assert_eq!(proof.seq_num, 1);
490 assert_eq!(proof.requester_key, key.public().to_string());
491 assert_eq!(proof.signature.len(), 128);
492 }
493
494 #[test]
495 fn sign_round_trip_protected_raw_companion() {
496 let key = secret_key(2);
497 let proof =
498 sign_fetch_proof(valid_hash(), ArtifactClass::ProtectedRawCompanion, 42, &key).unwrap();
499 assert_eq!(proof.artifact_class, ArtifactClass::ProtectedRawCompanion);
500 assert_eq!(proof.seq_num, 42);
501 }
502
503 #[test]
504 fn sign_rejects_invalid_hash() {
505 let key = secret_key(3);
506 assert!(matches!(
507 sign_fetch_proof("not-a-hash", ArtifactClass::PrivateRawIgc, 1, &key),
508 Err(FetchProofError::InvalidHash)
509 ));
510 assert!(matches!(
511 sign_fetch_proof(&"a".repeat(63), ArtifactClass::PrivateRawIgc, 1, &key),
512 Err(FetchProofError::InvalidHash)
513 ));
514 assert!(matches!(
515 sign_fetch_proof(&"A".repeat(64), ArtifactClass::PrivateRawIgc, 1, &key),
516 Err(FetchProofError::InvalidHash)
517 ));
518 }
519
520 #[test]
521 fn sign_rejects_zero_seq_num() {
522 let key = secret_key(4);
523 assert!(matches!(
524 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 0, &key),
525 Err(FetchProofError::SeqNumZero)
526 ));
527 }
528
529 #[test]
532 fn verify_accepts_valid_proof() {
533 let key = secret_key(10);
534 let proof = sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 5, &key).unwrap();
535
536 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 4).unwrap();
537 }
538
539 #[test]
540 fn verify_rejects_wrong_requester_key() {
541 let signer = secret_key(11);
542 let other = secret_key(12);
543 let proof =
544 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &signer).unwrap();
545
546 assert!(matches!(
547 verify_fetch_proof(&proof, &other.public(), &ArtifactClass::PrivateRawIgc, 0),
548 Err(FetchProofError::RequesterKeyMismatch)
549 ));
550 }
551
552 #[test]
553 fn verify_rejects_artifact_class_mismatch() {
554 let key = secret_key(13);
555 let proof = sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &key).unwrap();
556
557 assert!(matches!(
558 verify_fetch_proof(
559 &proof,
560 &key.public(),
561 &ArtifactClass::ProtectedRawCompanion,
562 0
563 ),
564 Err(FetchProofError::ArtifactClassMismatch)
565 ));
566 }
567
568 #[test]
569 fn verify_rejects_replayed_seq_num() {
570 let key = secret_key(14);
571 let proof = sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 3, &key).unwrap();
572
573 assert!(matches!(
575 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 3),
576 Err(FetchProofError::SeqNumNotMonotonic { .. })
577 ));
578 assert!(matches!(
580 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 4),
581 Err(FetchProofError::SeqNumNotMonotonic { .. })
582 ));
583 }
584
585 #[test]
586 fn verify_rejects_zero_seq_num() {
587 let key = secret_key(15);
588 let mut proof =
590 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &key).unwrap();
591 proof.seq_num = 0;
592 assert!(matches!(
593 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 0),
594 Err(FetchProofError::SeqNumZero)
595 ));
596 }
597
598 #[test]
599 fn verify_rejects_tampered_signature() {
600 let key = secret_key(16);
601 let mut proof =
602 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &key).unwrap();
603 let last = proof.signature.pop().unwrap();
605 proof.signature.push(if last == 'a' { 'b' } else { 'a' });
606
607 assert!(matches!(
608 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 0),
609 Err(FetchProofError::SignatureVerification)
610 ));
611 }
612
613 #[test]
614 fn verify_rejects_tampered_seq_num() {
615 let key = secret_key(17);
616 let mut proof =
617 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 5, &key).unwrap();
618 proof.seq_num = 6;
620
621 assert!(matches!(
622 verify_fetch_proof(&proof, &key.public(), &ArtifactClass::PrivateRawIgc, 0),
623 Err(FetchProofError::SignatureVerification)
624 ));
625 }
626
627 #[test]
628 fn verify_rejects_tampered_artifact_class() {
629 let key = secret_key(18);
630 let mut proof =
631 sign_fetch_proof(valid_hash(), ArtifactClass::PrivateRawIgc, 1, &key).unwrap();
632 proof.artifact_class = ArtifactClass::ProtectedRawCompanion;
634
635 assert!(matches!(
636 verify_fetch_proof(
637 &proof,
638 &key.public(),
639 &ArtifactClass::ProtectedRawCompanion,
640 0
641 ),
642 Err(FetchProofError::SignatureVerification)
643 ));
644 }
645
646 #[test]
647 fn proof_serializes_to_expected_json_field_names() {
648 let key = secret_key(19);
649 let proof =
650 sign_fetch_proof(valid_hash(), ArtifactClass::ProtectedRawCompanion, 1, &key).unwrap();
651 let json = serde_json::to_value(&proof).unwrap();
652 assert_eq!(json["schema"], "igc-net/fetch-request");
653 assert_eq!(json["schema_version"], 1);
654 assert_eq!(json["artifact_class"], "protected_raw_companion");
655 assert_eq!(json["seq_num"], 1);
656 assert!(json["signature"].as_str().unwrap().len() == 128);
657 }
658
659 fn temp_seq_store() -> (SeqNumStore, tempfile::TempDir) {
662 let dir = tempfile::tempdir().unwrap();
663 let store = SeqNumStore::open(dir.path());
664 (store, dir)
665 }
666
667 const REQUESTER_KEY: &str = "abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890";
668
669 #[test]
670 fn last_seen_returns_zero_for_unknown_key() {
671 let (store, _dir) = temp_seq_store();
672 assert_eq!(store.last_seen(REQUESTER_KEY).unwrap(), 0);
673 }
674
675 #[test]
676 fn advance_and_last_seen_round_trip() {
677 let (store, _dir) = temp_seq_store();
678 store.advance(REQUESTER_KEY, 1).unwrap();
679 assert_eq!(store.last_seen(REQUESTER_KEY).unwrap(), 1);
680 store.advance(REQUESTER_KEY, 100).unwrap();
681 assert_eq!(store.last_seen(REQUESTER_KEY).unwrap(), 100);
682 }
683
684 #[test]
685 fn advance_rejects_equal_seq_num() {
686 let (store, _dir) = temp_seq_store();
687 store.advance(REQUESTER_KEY, 5).unwrap();
688 assert!(matches!(
689 store.advance(REQUESTER_KEY, 5),
690 Err(SeqNumStoreError::NotMonotonic { .. })
691 ));
692 }
693
694 #[test]
695 fn advance_rejects_lower_seq_num() {
696 let (store, _dir) = temp_seq_store();
697 store.advance(REQUESTER_KEY, 10).unwrap();
698 assert!(matches!(
699 store.advance(REQUESTER_KEY, 9),
700 Err(SeqNumStoreError::NotMonotonic { .. })
701 ));
702 }
703
704 #[test]
705 fn seq_num_survives_store_reopen() {
706 let dir = tempfile::tempdir().unwrap();
707 {
708 let store = SeqNumStore::open(dir.path());
709 store.advance(REQUESTER_KEY, 42).unwrap();
710 }
711 let store = SeqNumStore::open(dir.path());
712 assert_eq!(store.last_seen(REQUESTER_KEY).unwrap(), 42);
713 }
714
715 #[test]
716 fn seq_num_is_per_requester_key() {
717 let (store, _dir) = temp_seq_store();
718 let key_b = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";
719 store.advance(REQUESTER_KEY, 10).unwrap();
720 store.advance(key_b, 3).unwrap();
721 assert_eq!(store.last_seen(REQUESTER_KEY).unwrap(), 10);
722 assert_eq!(store.last_seen(key_b).unwrap(), 3);
723 }
724}