1use alloy::primitives::{Address, B256, U256};
6use alloy::providers::ProviderBuilder;
7use alloy::signers::Signature;
8use std::collections::HashMap;
9use std::time::{SystemTime, UNIX_EPOCH};
10
11use crate::agent::{address_to_agent_key, compute_signing_message};
12use crate::constants::{
13 network_config, IAgentRegistry, NetworkName, DEFAULT_CACHE_TTL_MS, DEFAULT_MAX_AGE_MS,
14 DEFAULT_NETWORK, EXPIRY_WARNING_DAYS,
15};
16use crate::ed25519_agent::derive_address_from_pubkey;
17
18#[derive(Debug, Clone)]
24pub struct VerifierConfig {
25 pub network: Option<NetworkName>,
27 pub registry_address: Option<Address>,
29 pub rpc_url: Option<String>,
31 pub max_age_ms: Option<u64>,
33 pub cache_ttl_ms: Option<u64>,
35 pub max_agents_per_human: Option<u64>,
37 pub include_credentials: Option<bool>,
39 pub require_self_provider: Option<bool>,
41 pub enable_replay_protection: Option<bool>,
43 pub replay_cache_max_entries: Option<usize>,
45 pub minimum_age: Option<u64>,
47 pub require_ofac_passed: Option<bool>,
49 pub allowed_nationalities: Option<Vec<String>>,
51 pub rate_limit_config: Option<RateLimitConfig>,
53}
54
55impl Default for VerifierConfig {
56 fn default() -> Self {
57 Self {
58 network: None,
59 registry_address: None,
60 rpc_url: None,
61 max_age_ms: None,
62 cache_ttl_ms: None,
63 max_agents_per_human: None,
64 include_credentials: None,
65 require_self_provider: None,
66 enable_replay_protection: None,
67 replay_cache_max_entries: None,
68 minimum_age: None,
69 require_ofac_passed: None,
70 allowed_nationalities: None,
71 rate_limit_config: None,
72 }
73 }
74}
75
76#[derive(Debug, Clone)]
78pub struct RateLimitConfig {
79 pub per_minute: Option<u32>,
81 pub per_hour: Option<u32>,
83}
84
85#[derive(Debug, Clone, Default)]
87pub struct VerifierFromConfig {
88 pub network: Option<NetworkName>,
89 pub registry_address: Option<String>,
90 pub rpc_url: Option<String>,
91 pub require_age: Option<u64>,
92 pub require_ofac: Option<bool>,
93 pub require_nationality: Option<Vec<String>>,
94 pub require_self_provider: Option<bool>,
95 pub sybil_limit: Option<u64>,
96 pub rate_limit: Option<RateLimitConfig>,
97 pub replay_protection: Option<bool>,
98 pub max_age_ms: Option<u64>,
99 pub cache_ttl_ms: Option<u64>,
100}
101
102#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
108pub struct AgentCredentials {
109 pub issuing_state: String,
110 pub name: Vec<String>,
111 pub id_number: String,
112 pub nationality: String,
113 pub date_of_birth: String,
114 pub gender: String,
115 pub expiry_date: String,
116 pub older_than: U256,
117 pub ofac: Vec<bool>,
118}
119
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct VerificationResult {
123 pub valid: bool,
124 pub agent_address: Address,
126 pub agent_key: B256,
128 pub agent_id: U256,
129 pub agent_count: U256,
131 pub nullifier: U256,
133 pub credentials: Option<AgentCredentials>,
135 pub error: Option<String>,
136 pub retry_after_ms: Option<u64>,
138 pub proof_expires_at: Option<u64>,
140 pub days_until_expiry: Option<i32>,
142 pub is_expiring_soon: bool,
144}
145
146impl VerificationResult {
147 fn empty_with_error(error: &str) -> Self {
148 Self {
149 valid: false,
150 agent_address: Address::ZERO,
151 agent_key: B256::ZERO,
152 agent_id: U256::ZERO,
153 agent_count: U256::ZERO,
154 nullifier: U256::ZERO,
155 credentials: None,
156 error: Some(error.to_string()),
157 retry_after_ms: None,
158 proof_expires_at: None,
159 days_until_expiry: None,
160 is_expiring_soon: false,
161 }
162 }
163}
164
165struct RateBucket {
170 timestamps: Vec<u64>,
171}
172
173struct RateLimitResult {
174 error: String,
175 retry_after_ms: u64,
176}
177
178struct RateLimiter {
180 per_minute: u32,
181 per_hour: u32,
182 buckets: HashMap<String, RateBucket>,
183}
184
185impl RateLimiter {
186 fn new(config: &RateLimitConfig) -> Self {
187 Self {
188 per_minute: config.per_minute.unwrap_or(0),
189 per_hour: config.per_hour.unwrap_or(0),
190 buckets: HashMap::new(),
191 }
192 }
193
194 fn check(&mut self, agent_address: &str) -> Option<RateLimitResult> {
196 let now = now_millis();
197 let key = agent_address.to_ascii_lowercase();
198 let bucket = self
199 .buckets
200 .entry(key)
201 .or_insert_with(|| RateBucket { timestamps: Vec::new() });
202
203 let one_hour_ago = now.saturating_sub(60 * 60 * 1000);
205 bucket.timestamps.retain(|t| *t > one_hour_ago);
206
207 if self.per_minute > 0 {
209 let one_minute_ago = now.saturating_sub(60 * 1000);
210 let recent_minute: Vec<u64> = bucket
211 .timestamps
212 .iter()
213 .filter(|t| **t > one_minute_ago)
214 .copied()
215 .collect();
216 if recent_minute.len() >= self.per_minute as usize {
217 let oldest = recent_minute[0];
218 let retry_after = (oldest + 60 * 1000).saturating_sub(now).max(1);
219 return Some(RateLimitResult {
220 error: format!("Rate limit exceeded ({}/min)", self.per_minute),
221 retry_after_ms: retry_after,
222 });
223 }
224 }
225
226 if self.per_hour > 0 && bucket.timestamps.len() >= self.per_hour as usize {
228 let oldest = bucket.timestamps[0];
229 let retry_after = (oldest + 60 * 60 * 1000).saturating_sub(now).max(1);
230 return Some(RateLimitResult {
231 error: format!("Rate limit exceeded ({}/hr)", self.per_hour),
232 retry_after_ms: retry_after,
233 });
234 }
235
236 bucket.timestamps.push(now);
238 None
239 }
240}
241
242#[derive(Default)]
261pub struct VerifierBuilder {
262 network: Option<NetworkName>,
263 registry_address: Option<String>,
264 rpc_url: Option<String>,
265 max_age_ms: Option<u64>,
266 cache_ttl_ms: Option<u64>,
267 max_agents_per_human: Option<u64>,
268 include_credentials: Option<bool>,
269 require_self_provider: Option<bool>,
270 enable_replay_protection: Option<bool>,
271 minimum_age: Option<u64>,
272 require_ofac_passed: bool,
273 allowed_nationalities: Option<Vec<String>>,
274 rate_limit_config: Option<RateLimitConfig>,
275}
276
277impl VerifierBuilder {
278 pub fn network(mut self, name: NetworkName) -> Self {
280 self.network = Some(name);
281 self
282 }
283
284 pub fn registry(mut self, addr: &str) -> Self {
286 self.registry_address = Some(addr.to_string());
287 self
288 }
289
290 pub fn rpc(mut self, url: &str) -> Self {
292 self.rpc_url = Some(url.to_string());
293 self
294 }
295
296 pub fn require_age(mut self, n: u64) -> Self {
298 self.minimum_age = Some(n);
299 self
300 }
301
302 pub fn require_ofac(mut self) -> Self {
304 self.require_ofac_passed = true;
305 self
306 }
307
308 pub fn require_nationality(mut self, codes: &[&str]) -> Self {
310 self.allowed_nationalities = Some(codes.iter().map(|s| s.to_string()).collect());
311 self
312 }
313
314 pub fn require_self_provider(mut self) -> Self {
316 self.require_self_provider = Some(true);
317 self
318 }
319
320 pub fn sybil_limit(mut self, n: u64) -> Self {
322 self.max_agents_per_human = Some(n);
323 self
324 }
325
326 pub fn rate_limit(mut self, per_minute: u32, per_hour: u32) -> Self {
328 self.rate_limit_config = Some(RateLimitConfig {
329 per_minute: Some(per_minute),
330 per_hour: Some(per_hour),
331 });
332 self
333 }
334
335 pub fn replay_protection(mut self) -> Self {
337 self.enable_replay_protection = Some(true);
338 self
339 }
340
341 pub fn include_credentials(mut self) -> Self {
343 self.include_credentials = Some(true);
344 self
345 }
346
347 pub fn max_age(mut self, ms: u64) -> Self {
349 self.max_age_ms = Some(ms);
350 self
351 }
352
353 pub fn cache_ttl(mut self, ms: u64) -> Self {
355 self.cache_ttl_ms = Some(ms);
356 self
357 }
358
359 pub fn build(self) -> SelfAgentVerifier {
364 let needs_credentials = self.minimum_age.is_some()
366 || self.require_ofac_passed
367 || self
368 .allowed_nationalities
369 .as_ref()
370 .map_or(false, |v| !v.is_empty());
371
372 let registry_address = self
373 .registry_address
374 .and_then(|s| s.parse::<Address>().ok());
375
376 SelfAgentVerifier::new(VerifierConfig {
377 network: self.network,
378 registry_address,
379 rpc_url: self.rpc_url,
380 max_age_ms: self.max_age_ms,
381 cache_ttl_ms: self.cache_ttl_ms,
382 max_agents_per_human: self.max_agents_per_human,
383 include_credentials: if needs_credentials || self.include_credentials.unwrap_or(false) {
384 Some(true)
385 } else {
386 self.include_credentials
387 },
388 require_self_provider: self.require_self_provider,
389 enable_replay_protection: self.enable_replay_protection,
390 replay_cache_max_entries: None,
391 minimum_age: self.minimum_age,
392 require_ofac_passed: if self.require_ofac_passed {
393 Some(true)
394 } else {
395 None
396 },
397 allowed_nationalities: self.allowed_nationalities,
398 rate_limit_config: self.rate_limit_config,
399 })
400 }
401}
402
403struct CacheEntry {
408 is_verified: bool,
409 is_proof_fresh: bool,
410 agent_id: U256,
411 agent_count: U256,
412 nullifier: U256,
413 provider_address: Address,
414 proof_expires_at_timestamp: U256,
415 expires_at: u64,
416}
417
418struct OnChainStatus {
419 is_verified: bool,
420 is_proof_fresh: bool,
421 agent_id: U256,
422 agent_count: U256,
423 nullifier: U256,
424 provider_address: Address,
425 proof_expires_at_timestamp: U256,
426}
427
428pub struct SelfAgentVerifier {
469 registry_address: Address,
470 rpc_url: String,
471 max_age_ms: u64,
472 cache_ttl_ms: u64,
473 max_agents_per_human: u64,
474 include_credentials: bool,
475 require_self_provider: bool,
476 enable_replay_protection: bool,
477 replay_cache_max_entries: usize,
478 minimum_age: Option<u64>,
479 require_ofac_passed: bool,
480 allowed_nationalities: Option<Vec<String>>,
481 rate_limiter: Option<RateLimiter>,
482 cache: HashMap<B256, CacheEntry>,
483 replay_cache: HashMap<String, u64>,
484 self_provider_cache: Option<(Address, u64)>,
485}
486
487impl SelfAgentVerifier {
488 pub fn new(config: VerifierConfig) -> Self {
490 let net = network_config(config.network.unwrap_or(DEFAULT_NETWORK));
491 Self {
492 registry_address: config.registry_address.unwrap_or(net.registry_address),
493 rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
494 max_age_ms: config.max_age_ms.unwrap_or(DEFAULT_MAX_AGE_MS),
495 cache_ttl_ms: config.cache_ttl_ms.unwrap_or(DEFAULT_CACHE_TTL_MS),
496 max_agents_per_human: config.max_agents_per_human.unwrap_or(1),
497 include_credentials: config.include_credentials.unwrap_or(false),
498 require_self_provider: config.require_self_provider.unwrap_or(true),
499 enable_replay_protection: config.enable_replay_protection.unwrap_or(true),
500 replay_cache_max_entries: config.replay_cache_max_entries.unwrap_or(10_000),
501 minimum_age: config.minimum_age,
502 require_ofac_passed: config.require_ofac_passed.unwrap_or(false),
503 allowed_nationalities: config.allowed_nationalities,
504 rate_limiter: config.rate_limit_config.as_ref().map(RateLimiter::new),
505 cache: HashMap::new(),
506 replay_cache: HashMap::new(),
507 self_provider_cache: None,
508 }
509 }
510
511 pub fn create() -> VerifierBuilder {
513 VerifierBuilder::default()
514 }
515
516 pub fn from_config(cfg: VerifierFromConfig) -> Self {
521 let needs_credentials = cfg.require_age.is_some()
522 || cfg.require_ofac.unwrap_or(false)
523 || cfg
524 .require_nationality
525 .as_ref()
526 .map_or(false, |v| !v.is_empty());
527
528 let registry_address = cfg
529 .registry_address
530 .and_then(|s| s.parse::<Address>().ok());
531
532 Self::new(VerifierConfig {
533 network: cfg.network,
534 registry_address,
535 rpc_url: cfg.rpc_url,
536 max_age_ms: cfg.max_age_ms,
537 cache_ttl_ms: cfg.cache_ttl_ms,
538 max_agents_per_human: cfg.sybil_limit,
539 include_credentials: if needs_credentials { Some(true) } else { None },
540 require_self_provider: cfg.require_self_provider,
541 enable_replay_protection: cfg.replay_protection,
542 replay_cache_max_entries: None,
543 minimum_age: cfg.require_age,
544 require_ofac_passed: cfg.require_ofac,
545 allowed_nationalities: cfg.require_nationality,
546 rate_limit_config: cfg.rate_limit,
547 })
548 }
549
550 fn make_provider(
551 &self,
552 ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
553 let url: reqwest::Url = self
554 .rpc_url
555 .parse()
556 .map_err(|_| crate::Error::InvalidRpcUrl)?;
557 Ok(ProviderBuilder::new().connect_http(url))
558 }
559
560 pub async fn verify(
564 &mut self,
565 signature: &str,
566 timestamp: &str,
567 method: &str,
568 url: &str,
569 body: Option<&str>,
570 ) -> VerificationResult {
571 let ts: u64 = match timestamp.parse() {
573 Ok(v) => v,
574 Err(_) => return VerificationResult::empty_with_error("Timestamp expired or invalid"),
575 };
576 let now = now_millis();
577 let diff = if now > ts { now - ts } else { ts - now };
578 if diff > self.max_age_ms {
579 return VerificationResult::empty_with_error("Timestamp expired or invalid");
580 }
581
582 let message = compute_signing_message(timestamp, method, url, body);
584 let message_key = format!("{:#x}", message);
585
586 let signer_address = match recover_address(&message, signature) {
588 Ok(addr) => addr,
589 Err(_) => return VerificationResult::empty_with_error("Invalid signature"),
590 };
591
592 if self.enable_replay_protection {
594 if let Some(err) = self.check_and_record_replay(signature, &message_key, ts, now) {
595 return VerificationResult {
596 valid: false,
597 agent_address: signer_address,
598 agent_key: address_to_agent_key(signer_address),
599 agent_id: U256::ZERO,
600 agent_count: U256::ZERO,
601 nullifier: U256::ZERO,
602 credentials: None,
603 error: Some(err),
604 retry_after_ms: None,
605 proof_expires_at: None,
606 days_until_expiry: None,
607 is_expiring_soon: false,
608 };
609 }
610 }
611
612 let agent_key = address_to_agent_key(signer_address);
614
615 let on_chain = match self.check_on_chain(agent_key).await {
617 Ok(v) => v,
618 Err(e) => {
619 return VerificationResult {
620 valid: false,
621 agent_address: signer_address,
622 agent_key,
623 agent_id: U256::ZERO,
624 agent_count: U256::ZERO,
625 nullifier: U256::ZERO,
626 credentials: None,
627 error: Some(format!("RPC error: {}", e)),
628 retry_after_ms: None,
629 proof_expires_at: None,
630 days_until_expiry: None,
631 is_expiring_soon: false,
632 };
633 }
634 };
635
636 let expiry = compute_expiry_fields(on_chain.proof_expires_at_timestamp);
638
639 if !on_chain.is_verified {
640 return VerificationResult {
641 valid: false,
642 agent_address: signer_address,
643 agent_key,
644 agent_id: on_chain.agent_id,
645 agent_count: on_chain.agent_count,
646 nullifier: on_chain.nullifier,
647 credentials: None,
648 error: Some("Agent not verified on-chain".to_string()),
649 retry_after_ms: None,
650 proof_expires_at: expiry.proof_expires_at,
651 days_until_expiry: expiry.days_until_expiry,
652 is_expiring_soon: expiry.is_expiring_soon,
653 };
654 }
655
656 if !on_chain.is_proof_fresh {
658 return VerificationResult {
659 valid: false,
660 agent_address: signer_address,
661 agent_key,
662 agent_id: on_chain.agent_id,
663 agent_count: on_chain.agent_count,
664 nullifier: on_chain.nullifier,
665 credentials: None,
666 error: Some("Agent's human proof has expired".to_string()),
667 retry_after_ms: None,
668 proof_expires_at: expiry.proof_expires_at,
669 days_until_expiry: expiry.days_until_expiry,
670 is_expiring_soon: expiry.is_expiring_soon,
671 };
672 }
673
674 if self.require_self_provider && on_chain.agent_id > U256::ZERO {
676 let self_provider = match self.get_self_provider_address().await {
677 Ok(addr) => addr,
678 Err(_) => {
679 return VerificationResult {
680 valid: false,
681 agent_address: signer_address,
682 agent_key,
683 agent_id: on_chain.agent_id,
684 agent_count: on_chain.agent_count,
685 nullifier: on_chain.nullifier,
686 credentials: None,
687 error: Some(
688 "Unable to verify proof provider — RPC error".to_string(),
689 ),
690 retry_after_ms: None,
691 proof_expires_at: expiry.proof_expires_at,
692 days_until_expiry: expiry.days_until_expiry,
693 is_expiring_soon: expiry.is_expiring_soon,
694 };
695 }
696 };
697 if on_chain.provider_address != self_provider {
698 return VerificationResult {
699 valid: false,
700 agent_address: signer_address,
701 agent_key,
702 agent_id: on_chain.agent_id,
703 agent_count: on_chain.agent_count,
704 nullifier: on_chain.nullifier,
705 credentials: None,
706 error: Some(
707 "Agent was not verified by Self — proof provider mismatch".to_string(),
708 ),
709 retry_after_ms: None,
710 proof_expires_at: expiry.proof_expires_at,
711 days_until_expiry: expiry.days_until_expiry,
712 is_expiring_soon: expiry.is_expiring_soon,
713 };
714 }
715 }
716
717 if self.max_agents_per_human > 0
719 && on_chain.agent_count > U256::from(self.max_agents_per_human)
720 {
721 return VerificationResult {
722 valid: false,
723 agent_address: signer_address,
724 agent_key,
725 agent_id: on_chain.agent_id,
726 agent_count: on_chain.agent_count,
727 nullifier: on_chain.nullifier,
728 credentials: None,
729 error: Some(format!(
730 "Human has {} agents (max {})",
731 on_chain.agent_count, self.max_agents_per_human
732 )),
733 retry_after_ms: None,
734 proof_expires_at: expiry.proof_expires_at,
735 days_until_expiry: expiry.days_until_expiry,
736 is_expiring_soon: expiry.is_expiring_soon,
737 };
738 }
739
740 let credentials = if self.include_credentials && on_chain.agent_id > U256::ZERO {
742 self.fetch_credentials(on_chain.agent_id).await.ok()
743 } else {
744 None
745 };
746
747 if let Some(ref creds) = credentials {
749 if let Some(min_age) = self.minimum_age {
750 if creds.older_than < U256::from(min_age) {
751 return VerificationResult {
752 valid: false,
753 agent_address: signer_address,
754 agent_key,
755 agent_id: on_chain.agent_id,
756 agent_count: on_chain.agent_count,
757 nullifier: on_chain.nullifier,
758 credentials: credentials.clone(),
759 error: Some(format!(
760 "Agent's human does not meet minimum age (required: {}, got: {})",
761 min_age, creds.older_than
762 )),
763 retry_after_ms: None,
764 proof_expires_at: expiry.proof_expires_at,
765 days_until_expiry: expiry.days_until_expiry,
766 is_expiring_soon: expiry.is_expiring_soon,
767 };
768 }
769 }
770
771 if self.require_ofac_passed && !creds.ofac.first().copied().unwrap_or(false) {
772 return VerificationResult {
773 valid: false,
774 agent_address: signer_address,
775 agent_key,
776 agent_id: on_chain.agent_id,
777 agent_count: on_chain.agent_count,
778 nullifier: on_chain.nullifier,
779 credentials: credentials.clone(),
780 error: Some("Agent's human did not pass OFAC screening".to_string()),
781 retry_after_ms: None,
782 proof_expires_at: expiry.proof_expires_at,
783 days_until_expiry: expiry.days_until_expiry,
784 is_expiring_soon: expiry.is_expiring_soon,
785 };
786 }
787
788 if let Some(ref allowed) = self.allowed_nationalities {
789 if !allowed.is_empty() && !allowed.contains(&creds.nationality) {
790 return VerificationResult {
791 valid: false,
792 agent_address: signer_address,
793 agent_key,
794 agent_id: on_chain.agent_id,
795 agent_count: on_chain.agent_count,
796 nullifier: on_chain.nullifier,
797 credentials: credentials.clone(),
798 error: Some(format!(
799 "Nationality \"{}\" not in allowed list",
800 creds.nationality
801 )),
802 retry_after_ms: None,
803 proof_expires_at: expiry.proof_expires_at,
804 days_until_expiry: expiry.days_until_expiry,
805 is_expiring_soon: expiry.is_expiring_soon,
806 };
807 }
808 }
809 }
810
811 if let Some(ref mut limiter) = self.rate_limiter {
813 let addr_str = format!("{:#x}", signer_address);
814 if let Some(limited) = limiter.check(&addr_str) {
815 return VerificationResult {
816 valid: false,
817 agent_address: signer_address,
818 agent_key,
819 agent_id: on_chain.agent_id,
820 agent_count: on_chain.agent_count,
821 nullifier: on_chain.nullifier,
822 credentials,
823 error: Some(limited.error),
824 retry_after_ms: Some(limited.retry_after_ms),
825 proof_expires_at: expiry.proof_expires_at,
826 days_until_expiry: expiry.days_until_expiry,
827 is_expiring_soon: expiry.is_expiring_soon,
828 };
829 }
830 }
831
832 VerificationResult {
833 valid: true,
834 agent_address: signer_address,
835 agent_key,
836 agent_id: on_chain.agent_id,
837 agent_count: on_chain.agent_count,
838 nullifier: on_chain.nullifier,
839 credentials,
840 error: None,
841 retry_after_ms: None,
842 proof_expires_at: expiry.proof_expires_at,
843 days_until_expiry: expiry.days_until_expiry,
844 is_expiring_soon: expiry.is_expiring_soon,
845 }
846 }
847
848 pub async fn verify_with_keytype(
854 &mut self,
855 signature: &str,
856 timestamp: &str,
857 method: &str,
858 url: &str,
859 body: Option<&str>,
860 keytype: Option<&str>,
861 agent_key: Option<&str>,
862 ) -> VerificationResult {
863 if keytype == Some("ed25519") {
864 return self
865 .verify_ed25519(signature, timestamp, method, url, body, agent_key)
866 .await;
867 }
868
869 self.verify(signature, timestamp, method, url, body).await
871 }
872
873 async fn verify_ed25519(
879 &mut self,
880 signature: &str,
881 timestamp: &str,
882 method: &str,
883 url: &str,
884 body: Option<&str>,
885 agent_key_hex: Option<&str>,
886 ) -> VerificationResult {
887 let key_hex = match agent_key_hex {
889 Some(k) => k,
890 None => {
891 return VerificationResult::empty_with_error(
892 "Missing agent key for Ed25519 verification",
893 );
894 }
895 };
896
897 let ts: u64 = match timestamp.parse() {
899 Ok(v) => v,
900 Err(_) => {
901 return VerificationResult::empty_with_error("Timestamp expired or invalid");
902 }
903 };
904 let now = now_millis();
905 let diff = if now > ts { now - ts } else { ts - now };
906 if diff > self.max_age_ms {
907 return VerificationResult::empty_with_error("Timestamp expired or invalid");
908 }
909
910 let message = compute_signing_message(timestamp, method, url, body);
912 let message_key = format!("{:#x}", message);
913
914 let key_stripped = key_hex.strip_prefix("0x").unwrap_or(key_hex);
916 let key_bytes = match hex::decode(key_stripped) {
917 Ok(b) => b,
918 Err(_) => {
919 return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
920 }
921 };
922 let key_array: [u8; 32] = match key_bytes.try_into() {
923 Ok(a) => a,
924 Err(_) => {
925 return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
926 }
927 };
928 let verifying_key = match ed25519_dalek::VerifyingKey::from_bytes(&key_array) {
929 Ok(vk) => vk,
930 Err(_) => {
931 return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
932 }
933 };
934
935 let sig_stripped = signature.strip_prefix("0x").unwrap_or(signature);
937 let sig_bytes = match hex::decode(sig_stripped) {
938 Ok(b) => b,
939 Err(_) => {
940 return VerificationResult::empty_with_error("Invalid Ed25519 signature");
941 }
942 };
943 let sig_array: [u8; 64] = match sig_bytes.try_into() {
944 Ok(a) => a,
945 Err(_) => {
946 return VerificationResult::empty_with_error("Invalid Ed25519 signature");
947 }
948 };
949 let ed_signature = ed25519_dalek::Signature::from_bytes(&sig_array);
950
951 {
952 use ed25519_dalek::Verifier;
953 if verifying_key.verify(message.as_ref(), &ed_signature).is_err() {
954 return VerificationResult::empty_with_error("Invalid Ed25519 signature");
955 }
956 }
957
958 let signer_address = derive_address_from_pubkey(&key_array);
960
961 let agent_key = B256::from(key_array);
963
964 if self.enable_replay_protection {
966 if let Some(err) = self.check_and_record_replay(signature, &message_key, ts, now) {
967 return VerificationResult {
968 valid: false,
969 agent_address: signer_address,
970 agent_key,
971 agent_id: U256::ZERO,
972 agent_count: U256::ZERO,
973 nullifier: U256::ZERO,
974 credentials: None,
975 error: Some(err),
976 retry_after_ms: None,
977 proof_expires_at: None,
978 days_until_expiry: None,
979 is_expiring_soon: false,
980 };
981 }
982 }
983
984 self.verify_on_chain_and_policy(signer_address, agent_key)
987 .await
988 }
989
990 async fn verify_on_chain_and_policy(
992 &mut self,
993 signer_address: Address,
994 agent_key: B256,
995 ) -> VerificationResult {
996 let on_chain = match self.check_on_chain(agent_key).await {
998 Ok(v) => v,
999 Err(e) => {
1000 return VerificationResult {
1001 valid: false,
1002 agent_address: signer_address,
1003 agent_key,
1004 agent_id: U256::ZERO,
1005 agent_count: U256::ZERO,
1006 nullifier: U256::ZERO,
1007 credentials: None,
1008 error: Some(format!("RPC error: {}", e)),
1009 retry_after_ms: None,
1010 proof_expires_at: None,
1011 days_until_expiry: None,
1012 is_expiring_soon: false,
1013 };
1014 }
1015 };
1016
1017 let expiry = compute_expiry_fields(on_chain.proof_expires_at_timestamp);
1019
1020 if !on_chain.is_verified {
1021 return VerificationResult {
1022 valid: false,
1023 agent_address: signer_address,
1024 agent_key,
1025 agent_id: on_chain.agent_id,
1026 agent_count: on_chain.agent_count,
1027 nullifier: on_chain.nullifier,
1028 credentials: None,
1029 error: Some("Agent not verified on-chain".to_string()),
1030 retry_after_ms: None,
1031 proof_expires_at: expiry.proof_expires_at,
1032 days_until_expiry: expiry.days_until_expiry,
1033 is_expiring_soon: expiry.is_expiring_soon,
1034 };
1035 }
1036
1037 if !on_chain.is_proof_fresh {
1038 return VerificationResult {
1039 valid: false,
1040 agent_address: signer_address,
1041 agent_key,
1042 agent_id: on_chain.agent_id,
1043 agent_count: on_chain.agent_count,
1044 nullifier: on_chain.nullifier,
1045 credentials: None,
1046 error: Some("Agent's human proof has expired".to_string()),
1047 retry_after_ms: None,
1048 proof_expires_at: expiry.proof_expires_at,
1049 days_until_expiry: expiry.days_until_expiry,
1050 is_expiring_soon: expiry.is_expiring_soon,
1051 };
1052 }
1053
1054 if self.require_self_provider && on_chain.agent_id > U256::ZERO {
1056 let self_provider = match self.get_self_provider_address().await {
1057 Ok(addr) => addr,
1058 Err(_) => {
1059 return VerificationResult {
1060 valid: false,
1061 agent_address: signer_address,
1062 agent_key,
1063 agent_id: on_chain.agent_id,
1064 agent_count: on_chain.agent_count,
1065 nullifier: on_chain.nullifier,
1066 credentials: None,
1067 error: Some(
1068 "Unable to verify proof provider — RPC error".to_string(),
1069 ),
1070 retry_after_ms: None,
1071 proof_expires_at: expiry.proof_expires_at,
1072 days_until_expiry: expiry.days_until_expiry,
1073 is_expiring_soon: expiry.is_expiring_soon,
1074 };
1075 }
1076 };
1077 if on_chain.provider_address != self_provider {
1078 return VerificationResult {
1079 valid: false,
1080 agent_address: signer_address,
1081 agent_key,
1082 agent_id: on_chain.agent_id,
1083 agent_count: on_chain.agent_count,
1084 nullifier: on_chain.nullifier,
1085 credentials: None,
1086 error: Some(
1087 "Agent was not verified by Self — proof provider mismatch".to_string(),
1088 ),
1089 retry_after_ms: None,
1090 proof_expires_at: expiry.proof_expires_at,
1091 days_until_expiry: expiry.days_until_expiry,
1092 is_expiring_soon: expiry.is_expiring_soon,
1093 };
1094 }
1095 }
1096
1097 if self.max_agents_per_human > 0
1099 && on_chain.agent_count > U256::from(self.max_agents_per_human)
1100 {
1101 return VerificationResult {
1102 valid: false,
1103 agent_address: signer_address,
1104 agent_key,
1105 agent_id: on_chain.agent_id,
1106 agent_count: on_chain.agent_count,
1107 nullifier: on_chain.nullifier,
1108 credentials: None,
1109 error: Some(format!(
1110 "Human has {} agents (max {})",
1111 on_chain.agent_count, self.max_agents_per_human
1112 )),
1113 retry_after_ms: None,
1114 proof_expires_at: expiry.proof_expires_at,
1115 days_until_expiry: expiry.days_until_expiry,
1116 is_expiring_soon: expiry.is_expiring_soon,
1117 };
1118 }
1119
1120 let credentials = if self.include_credentials && on_chain.agent_id > U256::ZERO {
1122 self.fetch_credentials(on_chain.agent_id).await.ok()
1123 } else {
1124 None
1125 };
1126
1127 if let Some(ref creds) = credentials {
1129 if let Some(min_age) = self.minimum_age {
1130 if creds.older_than < U256::from(min_age) {
1131 return VerificationResult {
1132 valid: false,
1133 agent_address: signer_address,
1134 agent_key,
1135 agent_id: on_chain.agent_id,
1136 agent_count: on_chain.agent_count,
1137 nullifier: on_chain.nullifier,
1138 credentials: credentials.clone(),
1139 error: Some(format!(
1140 "Agent's human does not meet minimum age (required: {}, got: {})",
1141 min_age, creds.older_than
1142 )),
1143 retry_after_ms: None,
1144 proof_expires_at: expiry.proof_expires_at,
1145 days_until_expiry: expiry.days_until_expiry,
1146 is_expiring_soon: expiry.is_expiring_soon,
1147 };
1148 }
1149 }
1150
1151 if self.require_ofac_passed && !creds.ofac.first().copied().unwrap_or(false) {
1152 return VerificationResult {
1153 valid: false,
1154 agent_address: signer_address,
1155 agent_key,
1156 agent_id: on_chain.agent_id,
1157 agent_count: on_chain.agent_count,
1158 nullifier: on_chain.nullifier,
1159 credentials: credentials.clone(),
1160 error: Some("Agent's human did not pass OFAC screening".to_string()),
1161 retry_after_ms: None,
1162 proof_expires_at: expiry.proof_expires_at,
1163 days_until_expiry: expiry.days_until_expiry,
1164 is_expiring_soon: expiry.is_expiring_soon,
1165 };
1166 }
1167
1168 if let Some(ref allowed) = self.allowed_nationalities {
1169 if !allowed.is_empty() && !allowed.contains(&creds.nationality) {
1170 return VerificationResult {
1171 valid: false,
1172 agent_address: signer_address,
1173 agent_key,
1174 agent_id: on_chain.agent_id,
1175 agent_count: on_chain.agent_count,
1176 nullifier: on_chain.nullifier,
1177 credentials: credentials.clone(),
1178 error: Some(format!(
1179 "Nationality \"{}\" not in allowed list",
1180 creds.nationality
1181 )),
1182 retry_after_ms: None,
1183 proof_expires_at: expiry.proof_expires_at,
1184 days_until_expiry: expiry.days_until_expiry,
1185 is_expiring_soon: expiry.is_expiring_soon,
1186 };
1187 }
1188 }
1189 }
1190
1191 if let Some(ref mut limiter) = self.rate_limiter {
1193 let addr_str = format!("{:#x}", signer_address);
1194 if let Some(limited) = limiter.check(&addr_str) {
1195 return VerificationResult {
1196 valid: false,
1197 agent_address: signer_address,
1198 agent_key,
1199 agent_id: on_chain.agent_id,
1200 agent_count: on_chain.agent_count,
1201 nullifier: on_chain.nullifier,
1202 credentials,
1203 error: Some(limited.error),
1204 retry_after_ms: Some(limited.retry_after_ms),
1205 proof_expires_at: expiry.proof_expires_at,
1206 days_until_expiry: expiry.days_until_expiry,
1207 is_expiring_soon: expiry.is_expiring_soon,
1208 };
1209 }
1210 }
1211
1212 VerificationResult {
1213 valid: true,
1214 agent_address: signer_address,
1215 agent_key,
1216 agent_id: on_chain.agent_id,
1217 agent_count: on_chain.agent_count,
1218 nullifier: on_chain.nullifier,
1219 credentials,
1220 error: None,
1221 retry_after_ms: None,
1222 proof_expires_at: expiry.proof_expires_at,
1223 days_until_expiry: expiry.days_until_expiry,
1224 is_expiring_soon: expiry.is_expiring_soon,
1225 }
1226 }
1227
1228 async fn check_on_chain(&mut self, agent_key: B256) -> Result<OnChainStatus, crate::Error> {
1230 let now = now_millis();
1231 if let Some(cached) = self.cache.get(&agent_key) {
1232 if cached.expires_at > now {
1233 return Ok(OnChainStatus {
1234 is_verified: cached.is_verified,
1235 is_proof_fresh: cached.is_proof_fresh,
1236 agent_id: cached.agent_id,
1237 agent_count: cached.agent_count,
1238 nullifier: cached.nullifier,
1239 provider_address: cached.provider_address,
1240 proof_expires_at_timestamp: cached.proof_expires_at_timestamp,
1241 });
1242 }
1243 }
1244
1245 let provider = self.make_provider()?;
1246 let registry = IAgentRegistry::new(self.registry_address, &provider);
1247
1248 let is_verified = registry
1249 .isVerifiedAgent(agent_key)
1250 .call()
1251 .await
1252 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1253 let agent_id = registry
1254 .getAgentId(agent_key)
1255 .call()
1256 .await
1257 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1258
1259 let mut agent_count = U256::ZERO;
1260 let mut nullifier = U256::ZERO;
1261 let mut provider_address = Address::ZERO;
1262 let mut is_proof_fresh = false;
1263 let mut proof_expires_at_timestamp = U256::ZERO;
1264
1265 if agent_id > U256::ZERO {
1266 is_proof_fresh = registry
1267 .isProofFresh(agent_id)
1268 .call()
1269 .await
1270 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1271
1272 proof_expires_at_timestamp = registry
1274 .proofExpiresAt(agent_id)
1275 .call()
1276 .await
1277 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1278
1279 if self.max_agents_per_human > 0 {
1280 nullifier = registry
1281 .getHumanNullifier(agent_id)
1282 .call()
1283 .await
1284 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1285 agent_count = registry
1286 .getAgentCountForHuman(nullifier)
1287 .call()
1288 .await
1289 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1290 }
1291
1292 if self.require_self_provider {
1293 provider_address = registry
1294 .getProofProvider(agent_id)
1295 .call()
1296 .await
1297 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1298 }
1299 }
1300
1301 self.cache.insert(
1302 agent_key,
1303 CacheEntry {
1304 is_verified,
1305 is_proof_fresh,
1306 agent_id,
1307 agent_count,
1308 nullifier,
1309 provider_address,
1310 proof_expires_at_timestamp,
1311 expires_at: now + self.cache_ttl_ms,
1312 },
1313 );
1314
1315 Ok(OnChainStatus {
1316 is_verified,
1317 is_proof_fresh,
1318 agent_id,
1319 agent_count,
1320 nullifier,
1321 provider_address,
1322 proof_expires_at_timestamp,
1323 })
1324 }
1325
1326 async fn get_self_provider_address(&mut self) -> Result<Address, crate::Error> {
1328 let now = now_millis();
1329 if let Some((addr, expires_at)) = self.self_provider_cache {
1330 if expires_at > now {
1331 return Ok(addr);
1332 }
1333 }
1334
1335 let provider = self.make_provider()?;
1336 let registry = IAgentRegistry::new(self.registry_address, &provider);
1337
1338 let address = registry
1339 .selfProofProvider()
1340 .call()
1341 .await
1342 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1343
1344 self.self_provider_cache = Some((address, now + self.cache_ttl_ms * 12));
1346
1347 Ok(address)
1348 }
1349
1350 async fn fetch_credentials(&self, agent_id: U256) -> Result<AgentCredentials, crate::Error> {
1352 let provider = self.make_provider()?;
1353 let registry = IAgentRegistry::new(self.registry_address, &provider);
1354
1355 let raw = registry
1356 .getAgentCredentials(agent_id)
1357 .call()
1358 .await
1359 .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1360
1361 Ok(AgentCredentials {
1362 issuing_state: raw.issuingState,
1363 name: raw.name,
1364 id_number: raw.idNumber,
1365 nationality: raw.nationality,
1366 date_of_birth: raw.dateOfBirth,
1367 gender: raw.gender,
1368 expiry_date: raw.expiryDate,
1369 older_than: raw.olderThan,
1370 ofac: raw.ofac.to_vec(),
1371 })
1372 }
1373
1374 pub fn clear_cache(&mut self) {
1376 self.cache.clear();
1377 self.replay_cache.clear();
1378 self.self_provider_cache = None;
1379 }
1380
1381 fn check_and_record_replay(
1382 &mut self,
1383 signature: &str,
1384 message: &str,
1385 ts: u64,
1386 now: u64,
1387 ) -> Option<String> {
1388 self.prune_replay_cache(now);
1389
1390 let key = format!(
1391 "{}:{}",
1392 signature.to_ascii_lowercase(),
1393 message.to_ascii_lowercase()
1394 );
1395 if let Some(expires_at) = self.replay_cache.get(&key) {
1396 if *expires_at > now {
1397 return Some("Replay detected".to_string());
1398 }
1399 }
1400
1401 self.replay_cache.insert(key, ts.saturating_add(self.max_age_ms));
1402 None
1403 }
1404
1405 fn prune_replay_cache(&mut self, now: u64) {
1406 self.replay_cache.retain(|_, exp| *exp > now);
1407
1408 if self.replay_cache.len() <= self.replay_cache_max_entries {
1409 return;
1410 }
1411
1412 let overflow = self.replay_cache.len() - self.replay_cache_max_entries;
1413 let mut items: Vec<(String, u64)> =
1414 self.replay_cache.iter().map(|(k, v)| (k.clone(), *v)).collect();
1415 items.sort_by_key(|(_, exp)| *exp);
1416
1417 for (key, _) in items.into_iter().take(overflow) {
1418 self.replay_cache.remove(&key);
1419 }
1420 }
1421}
1422
1423struct ExpiryFields {
1425 proof_expires_at: Option<u64>,
1426 days_until_expiry: Option<i32>,
1427 is_expiring_soon: bool,
1428}
1429
1430fn compute_expiry_fields(proof_expires_at_timestamp: U256) -> ExpiryFields {
1433 if proof_expires_at_timestamp == U256::ZERO {
1434 return ExpiryFields {
1435 proof_expires_at: None,
1436 days_until_expiry: None,
1437 is_expiring_soon: false,
1438 };
1439 }
1440
1441 let expires_at_secs: u64 = proof_expires_at_timestamp.try_into().unwrap_or(u64::MAX);
1442 let now_secs = SystemTime::now()
1443 .duration_since(UNIX_EPOCH)
1444 .expect("system clock before UNIX epoch")
1445 .as_secs();
1446 let days = ((expires_at_secs as i64) - (now_secs as i64)) / 86400;
1447 let days_i32 = days.clamp(i32::MIN as i64, i32::MAX as i64) as i32;
1448 let is_expiring_soon = days_i32 >= 0 && days_i32 <= EXPIRY_WARNING_DAYS;
1449
1450 ExpiryFields {
1451 proof_expires_at: Some(expires_at_secs),
1452 days_until_expiry: Some(days_i32),
1453 is_expiring_soon,
1454 }
1455}
1456
1457fn recover_address(message: &B256, signature_hex: &str) -> Result<Address, crate::Error> {
1461 let sig_bytes = hex::decode(signature_hex.strip_prefix("0x").unwrap_or(signature_hex))
1462 .map_err(|_| crate::Error::InvalidSignature)?;
1463
1464 let signature = Signature::try_from(sig_bytes.as_slice())
1465 .map_err(|_| crate::Error::InvalidSignature)?;
1466
1467 let prefixed = alloy::primitives::eip191_hash_message(message.as_slice());
1469
1470 let recovered = signature
1471 .recover_address_from_prehash(&prefixed)
1472 .map_err(|_| crate::Error::InvalidSignature)?;
1473
1474 Ok(recovered)
1475}
1476
1477fn now_millis() -> u64 {
1478 SystemTime::now()
1479 .duration_since(UNIX_EPOCH)
1480 .expect("system clock before UNIX epoch")
1481 .as_millis() as u64
1482}
1483
1484#[cfg(test)]
1489mod tests {
1490 use super::*;
1491
1492 #[test]
1493 fn create_build_default() {
1494 let v = SelfAgentVerifier::create().build();
1495 assert_eq!(v.max_agents_per_human, 1);
1497 assert!(v.require_self_provider);
1498 assert!(v.enable_replay_protection);
1499 assert!(!v.include_credentials);
1500 assert!(v.minimum_age.is_none());
1501 assert!(!v.require_ofac_passed);
1502 assert!(v.allowed_nationalities.is_none());
1503 assert!(v.rate_limiter.is_none());
1504 }
1505
1506 #[test]
1507 fn create_build_testnet() {
1508 let v = SelfAgentVerifier::create()
1509 .network(NetworkName::Testnet)
1510 .build();
1511 let expected = network_config(NetworkName::Testnet);
1512 assert_eq!(v.registry_address, expected.registry_address);
1513 assert_eq!(v.rpc_url, expected.rpc_url);
1514 }
1515
1516 #[test]
1517 fn chain_credentials() {
1518 let v = SelfAgentVerifier::create()
1519 .network(NetworkName::Testnet)
1520 .require_age(18)
1521 .require_ofac()
1522 .require_nationality(&["US", "GB"])
1523 .build();
1524
1525 assert!(v.include_credentials);
1527 assert_eq!(v.minimum_age, Some(18));
1528 assert!(v.require_ofac_passed);
1529 assert_eq!(
1530 v.allowed_nationalities.as_deref(),
1531 Some(vec!["US".to_string(), "GB".to_string()].as_slice())
1532 );
1533 }
1534
1535 #[test]
1536 fn auto_enable_credentials_age_only() {
1537 let v = SelfAgentVerifier::create()
1538 .require_age(21)
1539 .build();
1540 assert!(v.include_credentials);
1541 assert_eq!(v.minimum_age, Some(21));
1542 }
1543
1544 #[test]
1545 fn auto_enable_credentials_ofac_only() {
1546 let v = SelfAgentVerifier::create()
1547 .require_ofac()
1548 .build();
1549 assert!(v.include_credentials);
1550 assert!(v.require_ofac_passed);
1551 }
1552
1553 #[test]
1554 fn auto_enable_credentials_nationality_only() {
1555 let v = SelfAgentVerifier::create()
1556 .require_nationality(&["DE"])
1557 .build();
1558 assert!(v.include_credentials);
1559 }
1560
1561 #[test]
1562 fn no_auto_credentials_without_requirements() {
1563 let v = SelfAgentVerifier::create()
1564 .network(NetworkName::Testnet)
1565 .sybil_limit(3)
1566 .build();
1567 assert!(!v.include_credentials);
1568 }
1569
1570 #[test]
1571 fn explicit_include_credentials() {
1572 let v = SelfAgentVerifier::create()
1573 .include_credentials()
1574 .build();
1575 assert!(v.include_credentials);
1576 }
1577
1578 #[test]
1579 fn from_config_works() {
1580 let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1581 network: Some(NetworkName::Testnet),
1582 require_age: Some(18),
1583 require_ofac: Some(true),
1584 sybil_limit: Some(1),
1585 ..Default::default()
1586 });
1587 assert!(v.include_credentials);
1588 assert_eq!(v.minimum_age, Some(18));
1589 assert!(v.require_ofac_passed);
1590 assert_eq!(v.max_agents_per_human, 1);
1591 }
1592
1593 #[test]
1594 fn from_config_auto_credentials_disabled() {
1595 let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1596 network: Some(NetworkName::Testnet),
1597 sybil_limit: Some(5),
1598 ..Default::default()
1599 });
1600 assert!(!v.include_credentials);
1601 }
1602
1603 #[test]
1604 fn from_config_nationality() {
1605 let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1606 require_nationality: Some(vec!["FR".to_string(), "IT".to_string()]),
1607 ..Default::default()
1608 });
1609 assert!(v.include_credentials);
1610 assert_eq!(
1611 v.allowed_nationalities.as_deref(),
1612 Some(vec!["FR".to_string(), "IT".to_string()].as_slice())
1613 );
1614 }
1615
1616 #[test]
1617 fn rate_limit_builder() {
1618 let v = SelfAgentVerifier::create()
1619 .rate_limit(10, 100)
1620 .build();
1621 assert!(v.rate_limiter.is_some());
1622 let limiter = v.rate_limiter.as_ref().unwrap();
1623 assert_eq!(limiter.per_minute, 10);
1624 assert_eq!(limiter.per_hour, 100);
1625 }
1626
1627 #[test]
1628 fn rate_limit_from_config() {
1629 let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1630 rate_limit: Some(RateLimitConfig {
1631 per_minute: Some(5),
1632 per_hour: Some(50),
1633 }),
1634 ..Default::default()
1635 });
1636 assert!(v.rate_limiter.is_some());
1637 }
1638
1639 #[test]
1640 fn rate_limiter_allows_within_limit() {
1641 let config = RateLimitConfig {
1642 per_minute: Some(3),
1643 per_hour: None,
1644 };
1645 let mut limiter = RateLimiter::new(&config);
1646 assert!(limiter.check("0xabc").is_none());
1647 assert!(limiter.check("0xabc").is_none());
1648 assert!(limiter.check("0xabc").is_none());
1649 let result = limiter.check("0xabc");
1651 assert!(result.is_some());
1652 let r = result.unwrap();
1653 assert!(r.error.contains("3/min"));
1654 assert!(r.retry_after_ms > 0);
1655 }
1656
1657 #[test]
1658 fn rate_limiter_separate_agents() {
1659 let config = RateLimitConfig {
1660 per_minute: Some(1),
1661 per_hour: None,
1662 };
1663 let mut limiter = RateLimiter::new(&config);
1664 assert!(limiter.check("0xabc").is_none());
1665 assert!(limiter.check("0xdef").is_none());
1666 assert!(limiter.check("0xabc").is_some());
1668 assert!(limiter.check("0xghi").is_none());
1670 }
1671
1672 #[test]
1673 fn builder_custom_max_age_and_cache_ttl() {
1674 let v = SelfAgentVerifier::create()
1675 .max_age(10_000)
1676 .cache_ttl(30_000)
1677 .build();
1678 assert_eq!(v.max_age_ms, 10_000);
1679 assert_eq!(v.cache_ttl_ms, 30_000);
1680 }
1681
1682 #[test]
1683 fn builder_sybil_limit_zero_disables() {
1684 let v = SelfAgentVerifier::create()
1685 .sybil_limit(0)
1686 .build();
1687 assert_eq!(v.max_agents_per_human, 0);
1688 }
1689
1690 #[test]
1691 fn builder_replay_protection() {
1692 let v = SelfAgentVerifier::create()
1693 .replay_protection()
1694 .build();
1695 assert!(v.enable_replay_protection);
1696 }
1697
1698 #[test]
1699 fn builder_require_self_provider() {
1700 let v = SelfAgentVerifier::create()
1701 .require_self_provider()
1702 .build();
1703 assert!(v.require_self_provider);
1704 }
1705
1706 #[test]
1707 fn new_constructor_still_works() {
1708 let v = SelfAgentVerifier::new(VerifierConfig::default());
1709 assert_eq!(v.max_age_ms, DEFAULT_MAX_AGE_MS);
1710 assert_eq!(v.cache_ttl_ms, DEFAULT_CACHE_TTL_MS);
1711 assert_eq!(v.max_agents_per_human, 1);
1712 assert!(v.require_self_provider);
1713 }
1714
1715 #[test]
1716 fn new_constructor_with_credentials() {
1717 let v = SelfAgentVerifier::new(VerifierConfig {
1718 minimum_age: Some(21),
1719 require_ofac_passed: Some(true),
1720 include_credentials: Some(true),
1721 ..Default::default()
1722 });
1723 assert!(v.include_credentials);
1724 assert_eq!(v.minimum_age, Some(21));
1725 assert!(v.require_ofac_passed);
1726 }
1727
1728 #[test]
1729 fn verification_result_has_retry_after() {
1730 let r = VerificationResult::empty_with_error("test");
1731 assert!(r.retry_after_ms.is_none());
1732 }
1733}