Skip to main content

self_agent_sdk/
verifier.rs

1// SPDX-FileCopyrightText: 2025-2026 Social Connect Labs, Inc.
2// SPDX-License-Identifier: BUSL-1.1
3// NOTE: Converts to Apache-2.0 on 2029-06-11 per LICENSE.
4
5use 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,
15};
16use crate::ed25519_agent::derive_address_from_pubkey;
17
18// ---------------------------------------------------------------------------
19// Configuration structs
20// ---------------------------------------------------------------------------
21
22/// Configuration for creating a [`SelfAgentVerifier`].
23#[derive(Debug, Clone)]
24pub struct VerifierConfig {
25    /// Network to use: Mainnet (default) or Testnet.
26    pub network: Option<NetworkName>,
27    /// Override: custom registry address.
28    pub registry_address: Option<Address>,
29    /// Override: custom RPC URL.
30    pub rpc_url: Option<String>,
31    /// Max age for signed timestamps in ms (default: 5 min).
32    pub max_age_ms: Option<u64>,
33    /// TTL for on-chain status cache in ms (default: 1 min).
34    pub cache_ttl_ms: Option<u64>,
35    /// Max agents allowed per human (default: 1). Set to 0 to disable.
36    pub max_agents_per_human: Option<u64>,
37    /// Include ZK-attested credentials in verification result (default: false).
38    pub include_credentials: Option<bool>,
39    /// Require proof-of-human was provided by Self Protocol (default: true).
40    pub require_self_provider: Option<bool>,
41    /// Reject duplicate signatures within validity window (default: true).
42    pub enable_replay_protection: Option<bool>,
43    /// Max replay cache entries before pruning (default: 10k).
44    pub replay_cache_max_entries: Option<usize>,
45    /// Minimum age for agent's human (credential check, default: disabled).
46    pub minimum_age: Option<u64>,
47    /// Require OFAC screening passed (credential check, default: false).
48    pub require_ofac_passed: Option<bool>,
49    /// Require nationality in list (credential check, default: disabled).
50    pub allowed_nationalities: Option<Vec<String>>,
51    /// In-memory per-agent rate limiting.
52    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/// Rate limit configuration for per-agent request throttling.
77#[derive(Debug, Clone)]
78pub struct RateLimitConfig {
79    /// Max requests per agent per minute.
80    pub per_minute: Option<u32>,
81    /// Max requests per agent per hour.
82    pub per_hour: Option<u32>,
83}
84
85/// Config object for the `from_config` static factory.
86#[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// ---------------------------------------------------------------------------
103// Credential + result types
104// ---------------------------------------------------------------------------
105
106/// ZK-attested credential claims stored on-chain for an agent.
107#[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/// Result of verifying an agent request.
121#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
122pub struct VerificationResult {
123    pub valid: bool,
124    /// The agent's Ethereum address (recovered from signature).
125    pub agent_address: Address,
126    /// The agent's on-chain key (bytes32).
127    pub agent_key: B256,
128    pub agent_id: U256,
129    /// Number of agents registered by the same human.
130    pub agent_count: U256,
131    /// Human's nullifier (for rate limiting by human identity).
132    pub nullifier: U256,
133    /// ZK-attested credentials (only populated when include_credentials is true).
134    pub credentials: Option<AgentCredentials>,
135    pub error: Option<String>,
136    /// Milliseconds until the rate limit resets (only set when rate limited).
137    pub retry_after_ms: Option<u64>,
138}
139
140impl VerificationResult {
141    fn empty_with_error(error: &str) -> Self {
142        Self {
143            valid: false,
144            agent_address: Address::ZERO,
145            agent_key: B256::ZERO,
146            agent_id: U256::ZERO,
147            agent_count: U256::ZERO,
148            nullifier: U256::ZERO,
149            credentials: None,
150            error: Some(error.to_string()),
151            retry_after_ms: None,
152        }
153    }
154}
155
156// ---------------------------------------------------------------------------
157// Rate limiter — sliding window, keyed by agent address
158// ---------------------------------------------------------------------------
159
160struct RateBucket {
161    timestamps: Vec<u64>,
162}
163
164struct RateLimitResult {
165    error: String,
166    retry_after_ms: u64,
167}
168
169/// In-memory sliding-window rate limiter keyed by agent address.
170struct RateLimiter {
171    per_minute: u32,
172    per_hour: u32,
173    buckets: HashMap<String, RateBucket>,
174}
175
176impl RateLimiter {
177    fn new(config: &RateLimitConfig) -> Self {
178        Self {
179            per_minute: config.per_minute.unwrap_or(0),
180            per_hour: config.per_hour.unwrap_or(0),
181            buckets: HashMap::new(),
182        }
183    }
184
185    /// Returns `None` if allowed, or a `RateLimitResult` if rate limited.
186    fn check(&mut self, agent_address: &str) -> Option<RateLimitResult> {
187        let now = now_millis();
188        let key = agent_address.to_ascii_lowercase();
189        let bucket = self
190            .buckets
191            .entry(key)
192            .or_insert_with(|| RateBucket { timestamps: Vec::new() });
193
194        // Prune timestamps older than 1 hour (longest window we care about)
195        let one_hour_ago = now.saturating_sub(60 * 60 * 1000);
196        bucket.timestamps.retain(|t| *t > one_hour_ago);
197
198        // Check per-minute limit
199        if self.per_minute > 0 {
200            let one_minute_ago = now.saturating_sub(60 * 1000);
201            let recent_minute: Vec<u64> = bucket
202                .timestamps
203                .iter()
204                .filter(|t| **t > one_minute_ago)
205                .copied()
206                .collect();
207            if recent_minute.len() >= self.per_minute as usize {
208                let oldest = recent_minute[0];
209                let retry_after = (oldest + 60 * 1000).saturating_sub(now).max(1);
210                return Some(RateLimitResult {
211                    error: format!("Rate limit exceeded ({}/min)", self.per_minute),
212                    retry_after_ms: retry_after,
213                });
214            }
215        }
216
217        // Check per-hour limit
218        if self.per_hour > 0 && bucket.timestamps.len() >= self.per_hour as usize {
219            let oldest = bucket.timestamps[0];
220            let retry_after = (oldest + 60 * 60 * 1000).saturating_sub(now).max(1);
221            return Some(RateLimitResult {
222                error: format!("Rate limit exceeded ({}/hr)", self.per_hour),
223                retry_after_ms: retry_after,
224            });
225        }
226
227        // Record this request
228        bucket.timestamps.push(now);
229        None
230    }
231}
232
233// ---------------------------------------------------------------------------
234// VerifierBuilder — chainable builder API
235// ---------------------------------------------------------------------------
236
237/// Chainable builder for creating a [`SelfAgentVerifier`].
238///
239/// # Example
240/// ```no_run
241/// use self_agent_sdk::{NetworkName, SelfAgentVerifier};
242///
243/// let verifier = SelfAgentVerifier::create()
244///     .network(NetworkName::Testnet)
245///     .require_age(18)
246///     .require_ofac()
247///     .require_nationality(&["US", "GB"])
248///     .rate_limit(10, 100)
249///     .build();
250/// ```
251#[derive(Default)]
252pub struct VerifierBuilder {
253    network: Option<NetworkName>,
254    registry_address: Option<String>,
255    rpc_url: Option<String>,
256    max_age_ms: Option<u64>,
257    cache_ttl_ms: Option<u64>,
258    max_agents_per_human: Option<u64>,
259    include_credentials: Option<bool>,
260    require_self_provider: Option<bool>,
261    enable_replay_protection: Option<bool>,
262    minimum_age: Option<u64>,
263    require_ofac_passed: bool,
264    allowed_nationalities: Option<Vec<String>>,
265    rate_limit_config: Option<RateLimitConfig>,
266}
267
268impl VerifierBuilder {
269    /// Set the network: `Mainnet` or `Testnet`.
270    pub fn network(mut self, name: NetworkName) -> Self {
271        self.network = Some(name);
272        self
273    }
274
275    /// Set a custom registry address.
276    pub fn registry(mut self, addr: &str) -> Self {
277        self.registry_address = Some(addr.to_string());
278        self
279    }
280
281    /// Set a custom RPC URL.
282    pub fn rpc(mut self, url: &str) -> Self {
283        self.rpc_url = Some(url.to_string());
284        self
285    }
286
287    /// Require the agent's human to be at least `n` years old.
288    pub fn require_age(mut self, n: u64) -> Self {
289        self.minimum_age = Some(n);
290        self
291    }
292
293    /// Require OFAC screening passed.
294    pub fn require_ofac(mut self) -> Self {
295        self.require_ofac_passed = true;
296        self
297    }
298
299    /// Require nationality in the given list of ISO country codes.
300    pub fn require_nationality(mut self, codes: &[&str]) -> Self {
301        self.allowed_nationalities = Some(codes.iter().map(|s| s.to_string()).collect());
302        self
303    }
304
305    /// Require Self Protocol as proof provider (default: on).
306    pub fn require_self_provider(mut self) -> Self {
307        self.require_self_provider = Some(true);
308        self
309    }
310
311    /// Max agents per human (default: 1). Set to 0 to disable sybil check.
312    pub fn sybil_limit(mut self, n: u64) -> Self {
313        self.max_agents_per_human = Some(n);
314        self
315    }
316
317    /// Enable in-memory per-agent rate limiting.
318    pub fn rate_limit(mut self, per_minute: u32, per_hour: u32) -> Self {
319        self.rate_limit_config = Some(RateLimitConfig {
320            per_minute: Some(per_minute),
321            per_hour: Some(per_hour),
322        });
323        self
324    }
325
326    /// Enable replay protection (default: on).
327    pub fn replay_protection(mut self) -> Self {
328        self.enable_replay_protection = Some(true);
329        self
330    }
331
332    /// Include ZK credentials in verification result.
333    pub fn include_credentials(mut self) -> Self {
334        self.include_credentials = Some(true);
335        self
336    }
337
338    /// Max signed timestamp age in milliseconds.
339    pub fn max_age(mut self, ms: u64) -> Self {
340        self.max_age_ms = Some(ms);
341        self
342    }
343
344    /// On-chain cache TTL in milliseconds.
345    pub fn cache_ttl(mut self, ms: u64) -> Self {
346        self.cache_ttl_ms = Some(ms);
347        self
348    }
349
350    /// Build the [`SelfAgentVerifier`] instance.
351    ///
352    /// Automatically enables `include_credentials` when any credential
353    /// requirement is set (age, OFAC, nationality).
354    pub fn build(self) -> SelfAgentVerifier {
355        // Auto-enable credentials if any credential requirement is set
356        let needs_credentials = self.minimum_age.is_some()
357            || self.require_ofac_passed
358            || self
359                .allowed_nationalities
360                .as_ref()
361                .map_or(false, |v| !v.is_empty());
362
363        let registry_address = self
364            .registry_address
365            .and_then(|s| s.parse::<Address>().ok());
366
367        SelfAgentVerifier::new(VerifierConfig {
368            network: self.network,
369            registry_address,
370            rpc_url: self.rpc_url,
371            max_age_ms: self.max_age_ms,
372            cache_ttl_ms: self.cache_ttl_ms,
373            max_agents_per_human: self.max_agents_per_human,
374            include_credentials: if needs_credentials || self.include_credentials.unwrap_or(false) {
375                Some(true)
376            } else {
377                self.include_credentials
378            },
379            require_self_provider: self.require_self_provider,
380            enable_replay_protection: self.enable_replay_protection,
381            replay_cache_max_entries: None,
382            minimum_age: self.minimum_age,
383            require_ofac_passed: if self.require_ofac_passed {
384                Some(true)
385            } else {
386                None
387            },
388            allowed_nationalities: self.allowed_nationalities,
389            rate_limit_config: self.rate_limit_config,
390        })
391    }
392}
393
394// ---------------------------------------------------------------------------
395// Internal cache types
396// ---------------------------------------------------------------------------
397
398struct CacheEntry {
399    is_verified: bool,
400    is_proof_fresh: bool,
401    agent_id: U256,
402    agent_count: U256,
403    nullifier: U256,
404    provider_address: Address,
405    expires_at: u64,
406}
407
408struct OnChainStatus {
409    is_verified: bool,
410    is_proof_fresh: bool,
411    agent_id: U256,
412    agent_count: U256,
413    nullifier: U256,
414    provider_address: Address,
415}
416
417// ---------------------------------------------------------------------------
418// SelfAgentVerifier
419// ---------------------------------------------------------------------------
420
421/// Service-side verifier for Self Agent ID requests.
422///
423/// Security chain:
424/// 1. Recover signer address from ECDSA signature
425/// 2. Derive agent key: zeroPadValue(recoveredAddress, 32)
426/// 3. Check on-chain: isVerifiedAgent(agentKey)
427/// 4. Check proof provider matches selfProofProvider()
428/// 5. Check timestamp freshness (replay protection)
429/// 6. Sybil resistance check
430/// 7. Credential checks (age, OFAC, nationality)
431/// 8. Rate limiting
432///
433/// # Construction
434///
435/// ```no_run
436/// use self_agent_sdk::{
437///     NetworkName, SelfAgentVerifier, VerifierConfig, VerifierFromConfig,
438/// };
439///
440/// // Direct construction
441/// let verifier = SelfAgentVerifier::new(VerifierConfig::default());
442///
443/// // Chainable builder
444/// let verifier = SelfAgentVerifier::create()
445///     .network(NetworkName::Testnet)
446///     .require_age(18)
447///     .require_ofac()
448///     .build();
449///
450/// // From config object
451/// let verifier = SelfAgentVerifier::from_config(VerifierFromConfig {
452///     network: Some(NetworkName::Testnet),
453///     require_age: Some(18),
454///     ..Default::default()
455/// });
456/// ```
457pub struct SelfAgentVerifier {
458    registry_address: Address,
459    rpc_url: String,
460    max_age_ms: u64,
461    cache_ttl_ms: u64,
462    max_agents_per_human: u64,
463    include_credentials: bool,
464    require_self_provider: bool,
465    enable_replay_protection: bool,
466    replay_cache_max_entries: usize,
467    minimum_age: Option<u64>,
468    require_ofac_passed: bool,
469    allowed_nationalities: Option<Vec<String>>,
470    rate_limiter: Option<RateLimiter>,
471    cache: HashMap<B256, CacheEntry>,
472    replay_cache: HashMap<String, u64>,
473    self_provider_cache: Option<(Address, u64)>,
474}
475
476impl SelfAgentVerifier {
477    /// Create a new verifier instance from a [`VerifierConfig`].
478    pub fn new(config: VerifierConfig) -> Self {
479        let net = network_config(config.network.unwrap_or(DEFAULT_NETWORK));
480        Self {
481            registry_address: config.registry_address.unwrap_or(net.registry_address),
482            rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
483            max_age_ms: config.max_age_ms.unwrap_or(DEFAULT_MAX_AGE_MS),
484            cache_ttl_ms: config.cache_ttl_ms.unwrap_or(DEFAULT_CACHE_TTL_MS),
485            max_agents_per_human: config.max_agents_per_human.unwrap_or(1),
486            include_credentials: config.include_credentials.unwrap_or(false),
487            require_self_provider: config.require_self_provider.unwrap_or(true),
488            enable_replay_protection: config.enable_replay_protection.unwrap_or(true),
489            replay_cache_max_entries: config.replay_cache_max_entries.unwrap_or(10_000),
490            minimum_age: config.minimum_age,
491            require_ofac_passed: config.require_ofac_passed.unwrap_or(false),
492            allowed_nationalities: config.allowed_nationalities,
493            rate_limiter: config.rate_limit_config.as_ref().map(RateLimiter::new),
494            cache: HashMap::new(),
495            replay_cache: HashMap::new(),
496            self_provider_cache: None,
497        }
498    }
499
500    /// Create a chainable [`VerifierBuilder`] for configuring a verifier.
501    pub fn create() -> VerifierBuilder {
502        VerifierBuilder::default()
503    }
504
505    /// Create a verifier from a flat config object.
506    ///
507    /// Automatically enables `include_credentials` when any credential
508    /// requirement is set (age, OFAC, nationality).
509    pub fn from_config(cfg: VerifierFromConfig) -> Self {
510        let needs_credentials = cfg.require_age.is_some()
511            || cfg.require_ofac.unwrap_or(false)
512            || cfg
513                .require_nationality
514                .as_ref()
515                .map_or(false, |v| !v.is_empty());
516
517        let registry_address = cfg
518            .registry_address
519            .and_then(|s| s.parse::<Address>().ok());
520
521        Self::new(VerifierConfig {
522            network: cfg.network,
523            registry_address,
524            rpc_url: cfg.rpc_url,
525            max_age_ms: cfg.max_age_ms,
526            cache_ttl_ms: cfg.cache_ttl_ms,
527            max_agents_per_human: cfg.sybil_limit,
528            include_credentials: if needs_credentials { Some(true) } else { None },
529            require_self_provider: cfg.require_self_provider,
530            enable_replay_protection: cfg.replay_protection,
531            replay_cache_max_entries: None,
532            minimum_age: cfg.require_age,
533            require_ofac_passed: cfg.require_ofac,
534            allowed_nationalities: cfg.require_nationality,
535            rate_limit_config: cfg.rate_limit,
536        })
537    }
538
539    fn make_provider(
540        &self,
541    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
542        let url: reqwest::Url = self
543            .rpc_url
544            .parse()
545            .map_err(|_| crate::Error::InvalidRpcUrl)?;
546        Ok(ProviderBuilder::new().connect_http(url))
547    }
548
549    /// Verify a signed agent request.
550    ///
551    /// The agent's identity is derived from the signature — not from any header.
552    pub async fn verify(
553        &mut self,
554        signature: &str,
555        timestamp: &str,
556        method: &str,
557        url: &str,
558        body: Option<&str>,
559    ) -> VerificationResult {
560        // 1. Check timestamp freshness (replay protection)
561        let ts: u64 = match timestamp.parse() {
562            Ok(v) => v,
563            Err(_) => return VerificationResult::empty_with_error("Timestamp expired or invalid"),
564        };
565        let now = now_millis();
566        let diff = if now > ts { now - ts } else { ts - now };
567        if diff > self.max_age_ms {
568            return VerificationResult::empty_with_error("Timestamp expired or invalid");
569        }
570
571        // 2. Reconstruct the signed message
572        let message = compute_signing_message(timestamp, method, url, body);
573        let message_key = format!("{:#x}", message);
574
575        // 3. Recover signer address from signature
576        let signer_address = match recover_address(&message, signature) {
577            Ok(addr) => addr,
578            Err(_) => return VerificationResult::empty_with_error("Invalid signature"),
579        };
580
581        // 4. Replay cache check (after signature validity to avoid cache poisoning)
582        if self.enable_replay_protection {
583            if let Some(err) = self.check_and_record_replay(signature, &message_key, ts, now) {
584                return VerificationResult {
585                    valid: false,
586                    agent_address: signer_address,
587                    agent_key: address_to_agent_key(signer_address),
588                    agent_id: U256::ZERO,
589                    agent_count: U256::ZERO,
590                    nullifier: U256::ZERO,
591                    credentials: None,
592                    error: Some(err),
593                    retry_after_ms: None,
594                };
595            }
596        }
597
598        // 5. Derive the on-chain agent key from the recovered address
599        let agent_key = address_to_agent_key(signer_address);
600
601        // 6. Check on-chain status (with cache)
602        let on_chain = match self.check_on_chain(agent_key).await {
603            Ok(v) => v,
604            Err(e) => {
605                return VerificationResult {
606                    valid: false,
607                    agent_address: signer_address,
608                    agent_key,
609                    agent_id: U256::ZERO,
610                    agent_count: U256::ZERO,
611                    nullifier: U256::ZERO,
612                    credentials: None,
613                    error: Some(format!("RPC error: {}", e)),
614                    retry_after_ms: None,
615                };
616            }
617        };
618
619        if !on_chain.is_verified {
620            return VerificationResult {
621                valid: false,
622                agent_address: signer_address,
623                agent_key,
624                agent_id: on_chain.agent_id,
625                agent_count: on_chain.agent_count,
626                nullifier: on_chain.nullifier,
627                credentials: None,
628                error: Some("Agent not verified on-chain".to_string()),
629                retry_after_ms: None,
630            };
631        }
632
633        // 6b. Check proof freshness (expired proofs should not pass verification)
634        if !on_chain.is_proof_fresh {
635            return VerificationResult {
636                valid: false,
637                agent_address: signer_address,
638                agent_key,
639                agent_id: on_chain.agent_id,
640                agent_count: on_chain.agent_count,
641                nullifier: on_chain.nullifier,
642                credentials: None,
643                error: Some("Agent's human proof has expired".to_string()),
644                retry_after_ms: None,
645            };
646        }
647
648        // 7. Provider check: ensure agent was verified by Self Protocol
649        if self.require_self_provider && on_chain.agent_id > U256::ZERO {
650            let self_provider = match self.get_self_provider_address().await {
651                Ok(addr) => addr,
652                Err(_) => {
653                    return VerificationResult {
654                        valid: false,
655                        agent_address: signer_address,
656                        agent_key,
657                        agent_id: on_chain.agent_id,
658                        agent_count: on_chain.agent_count,
659                        nullifier: on_chain.nullifier,
660                        credentials: None,
661                        error: Some(
662                            "Unable to verify proof provider — RPC error".to_string(),
663                        ),
664                        retry_after_ms: None,
665                    };
666                }
667            };
668            if on_chain.provider_address != self_provider {
669                return VerificationResult {
670                    valid: false,
671                    agent_address: signer_address,
672                    agent_key,
673                    agent_id: on_chain.agent_id,
674                    agent_count: on_chain.agent_count,
675                    nullifier: on_chain.nullifier,
676                    credentials: None,
677                    error: Some(
678                        "Agent was not verified by Self — proof provider mismatch".to_string(),
679                    ),
680                    retry_after_ms: None,
681                };
682            }
683        }
684
685        // 8. Sybil resistance: reject if human has too many agents
686        if self.max_agents_per_human > 0
687            && on_chain.agent_count > U256::from(self.max_agents_per_human)
688        {
689            return VerificationResult {
690                valid: false,
691                agent_address: signer_address,
692                agent_key,
693                agent_id: on_chain.agent_id,
694                agent_count: on_chain.agent_count,
695                nullifier: on_chain.nullifier,
696                credentials: None,
697                error: Some(format!(
698                    "Human has {} agents (max {})",
699                    on_chain.agent_count, self.max_agents_per_human
700                )),
701                retry_after_ms: None,
702            };
703        }
704
705        // 9. Fetch credentials if requested
706        let credentials = if self.include_credentials && on_chain.agent_id > U256::ZERO {
707            self.fetch_credentials(on_chain.agent_id).await.ok()
708        } else {
709            None
710        };
711
712        // 10. Credential checks (post-verify — only if credentials were fetched)
713        if let Some(ref creds) = credentials {
714            if let Some(min_age) = self.minimum_age {
715                if creds.older_than < U256::from(min_age) {
716                    return VerificationResult {
717                        valid: false,
718                        agent_address: signer_address,
719                        agent_key,
720                        agent_id: on_chain.agent_id,
721                        agent_count: on_chain.agent_count,
722                        nullifier: on_chain.nullifier,
723                        credentials: credentials.clone(),
724                        error: Some(format!(
725                            "Agent's human does not meet minimum age (required: {}, got: {})",
726                            min_age, creds.older_than
727                        )),
728                        retry_after_ms: None,
729                    };
730                }
731            }
732
733            if self.require_ofac_passed && !creds.ofac.first().copied().unwrap_or(false) {
734                return VerificationResult {
735                    valid: false,
736                    agent_address: signer_address,
737                    agent_key,
738                    agent_id: on_chain.agent_id,
739                    agent_count: on_chain.agent_count,
740                    nullifier: on_chain.nullifier,
741                    credentials: credentials.clone(),
742                    error: Some("Agent's human did not pass OFAC screening".to_string()),
743                    retry_after_ms: None,
744                };
745            }
746
747            if let Some(ref allowed) = self.allowed_nationalities {
748                if !allowed.is_empty() && !allowed.contains(&creds.nationality) {
749                    return VerificationResult {
750                        valid: false,
751                        agent_address: signer_address,
752                        agent_key,
753                        agent_id: on_chain.agent_id,
754                        agent_count: on_chain.agent_count,
755                        nullifier: on_chain.nullifier,
756                        credentials: credentials.clone(),
757                        error: Some(format!(
758                            "Nationality \"{}\" not in allowed list",
759                            creds.nationality
760                        )),
761                        retry_after_ms: None,
762                    };
763                }
764            }
765        }
766
767        // 11. Rate limiting (per-agent, in-memory sliding window)
768        if let Some(ref mut limiter) = self.rate_limiter {
769            let addr_str = format!("{:#x}", signer_address);
770            if let Some(limited) = limiter.check(&addr_str) {
771                return VerificationResult {
772                    valid: false,
773                    agent_address: signer_address,
774                    agent_key,
775                    agent_id: on_chain.agent_id,
776                    agent_count: on_chain.agent_count,
777                    nullifier: on_chain.nullifier,
778                    credentials,
779                    error: Some(limited.error),
780                    retry_after_ms: Some(limited.retry_after_ms),
781                };
782            }
783        }
784
785        VerificationResult {
786            valid: true,
787            agent_address: signer_address,
788            agent_key,
789            agent_id: on_chain.agent_id,
790            agent_count: on_chain.agent_count,
791            nullifier: on_chain.nullifier,
792            credentials,
793            error: None,
794            retry_after_ms: None,
795        }
796    }
797
798    /// Verify a signed agent request with key type awareness.
799    ///
800    /// When `keytype` is `Some("ed25519")`, uses Ed25519 verification with the
801    /// provided `agent_key` (32-byte public key). Otherwise falls through to
802    /// standard ECDSA verification via [`verify`].
803    pub async fn verify_with_keytype(
804        &mut self,
805        signature: &str,
806        timestamp: &str,
807        method: &str,
808        url: &str,
809        body: Option<&str>,
810        keytype: Option<&str>,
811        agent_key: Option<&str>,
812    ) -> VerificationResult {
813        if keytype == Some("ed25519") {
814            return self
815                .verify_ed25519(signature, timestamp, method, url, body, agent_key)
816                .await;
817        }
818
819        // Default: ECDSA verification
820        self.verify(signature, timestamp, method, url, body).await
821    }
822
823    /// Verify an Ed25519-signed agent request.
824    ///
825    /// The agent's public key must be provided in `agent_key_hex`. The signature
826    /// is verified directly against the Ed25519 public key (no EIP-191 prefix).
827    /// The agent address is derived from `keccak256(pubkey)`.
828    async fn verify_ed25519(
829        &mut self,
830        signature: &str,
831        timestamp: &str,
832        method: &str,
833        url: &str,
834        body: Option<&str>,
835        agent_key_hex: Option<&str>,
836    ) -> VerificationResult {
837        // 1. Require agent key for Ed25519
838        let key_hex = match agent_key_hex {
839            Some(k) => k,
840            None => {
841                return VerificationResult::empty_with_error(
842                    "Missing agent key for Ed25519 verification",
843                );
844            }
845        };
846
847        // 2. Check timestamp freshness
848        let ts: u64 = match timestamp.parse() {
849            Ok(v) => v,
850            Err(_) => {
851                return VerificationResult::empty_with_error("Timestamp expired or invalid");
852            }
853        };
854        let now = now_millis();
855        let diff = if now > ts { now - ts } else { ts - now };
856        if diff > self.max_age_ms {
857            return VerificationResult::empty_with_error("Timestamp expired or invalid");
858        }
859
860        // 3. Reconstruct the signed message
861        let message = compute_signing_message(timestamp, method, url, body);
862        let message_key = format!("{:#x}", message);
863
864        // 4. Parse the Ed25519 public key
865        let key_stripped = key_hex.strip_prefix("0x").unwrap_or(key_hex);
866        let key_bytes = match hex::decode(key_stripped) {
867            Ok(b) => b,
868            Err(_) => {
869                return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
870            }
871        };
872        let key_array: [u8; 32] = match key_bytes.try_into() {
873            Ok(a) => a,
874            Err(_) => {
875                return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
876            }
877        };
878        let verifying_key = match ed25519_dalek::VerifyingKey::from_bytes(&key_array) {
879            Ok(vk) => vk,
880            Err(_) => {
881                return VerificationResult::empty_with_error("Invalid Ed25519 agent key");
882            }
883        };
884
885        // 5. Parse and verify the signature
886        let sig_stripped = signature.strip_prefix("0x").unwrap_or(signature);
887        let sig_bytes = match hex::decode(sig_stripped) {
888            Ok(b) => b,
889            Err(_) => {
890                return VerificationResult::empty_with_error("Invalid Ed25519 signature");
891            }
892        };
893        let sig_array: [u8; 64] = match sig_bytes.try_into() {
894            Ok(a) => a,
895            Err(_) => {
896                return VerificationResult::empty_with_error("Invalid Ed25519 signature");
897            }
898        };
899        let ed_signature = ed25519_dalek::Signature::from_bytes(&sig_array);
900
901        {
902            use ed25519_dalek::Verifier;
903            if verifying_key.verify(message.as_ref(), &ed_signature).is_err() {
904                return VerificationResult::empty_with_error("Invalid Ed25519 signature");
905            }
906        }
907
908        // 6. Derive address from keccak256(pubkey)
909        let signer_address = derive_address_from_pubkey(&key_array);
910
911        // Agent key for Ed25519 is the raw 32-byte public key (already bytes32)
912        let agent_key = B256::from(key_array);
913
914        // 7. Replay cache check
915        if self.enable_replay_protection {
916            if let Some(err) = self.check_and_record_replay(signature, &message_key, ts, now) {
917                return VerificationResult {
918                    valid: false,
919                    agent_address: signer_address,
920                    agent_key,
921                    agent_id: U256::ZERO,
922                    agent_count: U256::ZERO,
923                    nullifier: U256::ZERO,
924                    credentials: None,
925                    error: Some(err),
926                    retry_after_ms: None,
927                };
928            }
929        }
930
931        // 8+ Remaining checks (on-chain, provider, sybil, credentials, rate limit)
932        // are identical to the ECDSA path — delegate to shared logic.
933        self.verify_on_chain_and_policy(signer_address, agent_key)
934            .await
935    }
936
937    /// Shared on-chain + policy verification logic used by both ECDSA and Ed25519 paths.
938    async fn verify_on_chain_and_policy(
939        &mut self,
940        signer_address: Address,
941        agent_key: B256,
942    ) -> VerificationResult {
943        // Check on-chain status (with cache)
944        let on_chain = match self.check_on_chain(agent_key).await {
945            Ok(v) => v,
946            Err(e) => {
947                return VerificationResult {
948                    valid: false,
949                    agent_address: signer_address,
950                    agent_key,
951                    agent_id: U256::ZERO,
952                    agent_count: U256::ZERO,
953                    nullifier: U256::ZERO,
954                    credentials: None,
955                    error: Some(format!("RPC error: {}", e)),
956                    retry_after_ms: None,
957                };
958            }
959        };
960
961        if !on_chain.is_verified {
962            return VerificationResult {
963                valid: false,
964                agent_address: signer_address,
965                agent_key,
966                agent_id: on_chain.agent_id,
967                agent_count: on_chain.agent_count,
968                nullifier: on_chain.nullifier,
969                credentials: None,
970                error: Some("Agent not verified on-chain".to_string()),
971                retry_after_ms: None,
972            };
973        }
974
975        if !on_chain.is_proof_fresh {
976            return VerificationResult {
977                valid: false,
978                agent_address: signer_address,
979                agent_key,
980                agent_id: on_chain.agent_id,
981                agent_count: on_chain.agent_count,
982                nullifier: on_chain.nullifier,
983                credentials: None,
984                error: Some("Agent's human proof has expired".to_string()),
985                retry_after_ms: None,
986            };
987        }
988
989        // Provider check
990        if self.require_self_provider && on_chain.agent_id > U256::ZERO {
991            let self_provider = match self.get_self_provider_address().await {
992                Ok(addr) => addr,
993                Err(_) => {
994                    return VerificationResult {
995                        valid: false,
996                        agent_address: signer_address,
997                        agent_key,
998                        agent_id: on_chain.agent_id,
999                        agent_count: on_chain.agent_count,
1000                        nullifier: on_chain.nullifier,
1001                        credentials: None,
1002                        error: Some(
1003                            "Unable to verify proof provider — RPC error".to_string(),
1004                        ),
1005                        retry_after_ms: None,
1006                    };
1007                }
1008            };
1009            if on_chain.provider_address != self_provider {
1010                return VerificationResult {
1011                    valid: false,
1012                    agent_address: signer_address,
1013                    agent_key,
1014                    agent_id: on_chain.agent_id,
1015                    agent_count: on_chain.agent_count,
1016                    nullifier: on_chain.nullifier,
1017                    credentials: None,
1018                    error: Some(
1019                        "Agent was not verified by Self — proof provider mismatch".to_string(),
1020                    ),
1021                    retry_after_ms: None,
1022                };
1023            }
1024        }
1025
1026        // Sybil resistance
1027        if self.max_agents_per_human > 0
1028            && on_chain.agent_count > U256::from(self.max_agents_per_human)
1029        {
1030            return VerificationResult {
1031                valid: false,
1032                agent_address: signer_address,
1033                agent_key,
1034                agent_id: on_chain.agent_id,
1035                agent_count: on_chain.agent_count,
1036                nullifier: on_chain.nullifier,
1037                credentials: None,
1038                error: Some(format!(
1039                    "Human has {} agents (max {})",
1040                    on_chain.agent_count, self.max_agents_per_human
1041                )),
1042                retry_after_ms: None,
1043            };
1044        }
1045
1046        // Fetch credentials if requested
1047        let credentials = if self.include_credentials && on_chain.agent_id > U256::ZERO {
1048            self.fetch_credentials(on_chain.agent_id).await.ok()
1049        } else {
1050            None
1051        };
1052
1053        // Credential checks
1054        if let Some(ref creds) = credentials {
1055            if let Some(min_age) = self.minimum_age {
1056                if creds.older_than < U256::from(min_age) {
1057                    return VerificationResult {
1058                        valid: false,
1059                        agent_address: signer_address,
1060                        agent_key,
1061                        agent_id: on_chain.agent_id,
1062                        agent_count: on_chain.agent_count,
1063                        nullifier: on_chain.nullifier,
1064                        credentials: credentials.clone(),
1065                        error: Some(format!(
1066                            "Agent's human does not meet minimum age (required: {}, got: {})",
1067                            min_age, creds.older_than
1068                        )),
1069                        retry_after_ms: None,
1070                    };
1071                }
1072            }
1073
1074            if self.require_ofac_passed && !creds.ofac.first().copied().unwrap_or(false) {
1075                return VerificationResult {
1076                    valid: false,
1077                    agent_address: signer_address,
1078                    agent_key,
1079                    agent_id: on_chain.agent_id,
1080                    agent_count: on_chain.agent_count,
1081                    nullifier: on_chain.nullifier,
1082                    credentials: credentials.clone(),
1083                    error: Some("Agent's human did not pass OFAC screening".to_string()),
1084                    retry_after_ms: None,
1085                };
1086            }
1087
1088            if let Some(ref allowed) = self.allowed_nationalities {
1089                if !allowed.is_empty() && !allowed.contains(&creds.nationality) {
1090                    return VerificationResult {
1091                        valid: false,
1092                        agent_address: signer_address,
1093                        agent_key,
1094                        agent_id: on_chain.agent_id,
1095                        agent_count: on_chain.agent_count,
1096                        nullifier: on_chain.nullifier,
1097                        credentials: credentials.clone(),
1098                        error: Some(format!(
1099                            "Nationality \"{}\" not in allowed list",
1100                            creds.nationality
1101                        )),
1102                        retry_after_ms: None,
1103                    };
1104                }
1105            }
1106        }
1107
1108        // Rate limiting
1109        if let Some(ref mut limiter) = self.rate_limiter {
1110            let addr_str = format!("{:#x}", signer_address);
1111            if let Some(limited) = limiter.check(&addr_str) {
1112                return VerificationResult {
1113                    valid: false,
1114                    agent_address: signer_address,
1115                    agent_key,
1116                    agent_id: on_chain.agent_id,
1117                    agent_count: on_chain.agent_count,
1118                    nullifier: on_chain.nullifier,
1119                    credentials,
1120                    error: Some(limited.error),
1121                    retry_after_ms: Some(limited.retry_after_ms),
1122                };
1123            }
1124        }
1125
1126        VerificationResult {
1127            valid: true,
1128            agent_address: signer_address,
1129            agent_key,
1130            agent_id: on_chain.agent_id,
1131            agent_count: on_chain.agent_count,
1132            nullifier: on_chain.nullifier,
1133            credentials,
1134            error: None,
1135            retry_after_ms: None,
1136        }
1137    }
1138
1139    /// Check on-chain agent status with caching.
1140    async fn check_on_chain(&mut self, agent_key: B256) -> Result<OnChainStatus, crate::Error> {
1141        let now = now_millis();
1142        if let Some(cached) = self.cache.get(&agent_key) {
1143            if cached.expires_at > now {
1144                return Ok(OnChainStatus {
1145                    is_verified: cached.is_verified,
1146                    is_proof_fresh: cached.is_proof_fresh,
1147                    agent_id: cached.agent_id,
1148                    agent_count: cached.agent_count,
1149                    nullifier: cached.nullifier,
1150                    provider_address: cached.provider_address,
1151                });
1152            }
1153        }
1154
1155        let provider = self.make_provider()?;
1156        let registry = IAgentRegistry::new(self.registry_address, &provider);
1157
1158        let is_verified = registry
1159            .isVerifiedAgent(agent_key)
1160            .call()
1161            .await
1162            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1163        let agent_id = registry
1164            .getAgentId(agent_key)
1165            .call()
1166            .await
1167            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1168
1169        let mut agent_count = U256::ZERO;
1170        let mut nullifier = U256::ZERO;
1171        let mut provider_address = Address::ZERO;
1172        let mut is_proof_fresh = false;
1173
1174        if agent_id > U256::ZERO {
1175            is_proof_fresh = registry
1176                .isProofFresh(agent_id)
1177                .call()
1178                .await
1179                .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1180
1181            if self.max_agents_per_human > 0 {
1182                nullifier = registry
1183                    .getHumanNullifier(agent_id)
1184                    .call()
1185                    .await
1186                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1187                agent_count = registry
1188                    .getAgentCountForHuman(nullifier)
1189                    .call()
1190                    .await
1191                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1192            }
1193
1194            if self.require_self_provider {
1195                provider_address = registry
1196                    .getProofProvider(agent_id)
1197                    .call()
1198                    .await
1199                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1200            }
1201        }
1202
1203        self.cache.insert(
1204            agent_key,
1205            CacheEntry {
1206                is_verified,
1207                is_proof_fresh,
1208                agent_id,
1209                agent_count,
1210                nullifier,
1211                provider_address,
1212                expires_at: now + self.cache_ttl_ms,
1213            },
1214        );
1215
1216        Ok(OnChainStatus {
1217            is_verified,
1218            is_proof_fresh,
1219            agent_id,
1220            agent_count,
1221            nullifier,
1222            provider_address,
1223        })
1224    }
1225
1226    /// Get Self Protocol's own proof provider address from the registry.
1227    async fn get_self_provider_address(&mut self) -> Result<Address, crate::Error> {
1228        let now = now_millis();
1229        if let Some((addr, expires_at)) = self.self_provider_cache {
1230            if expires_at > now {
1231                return Ok(addr);
1232            }
1233        }
1234
1235        let provider = self.make_provider()?;
1236        let registry = IAgentRegistry::new(self.registry_address, &provider);
1237
1238        let address = registry
1239            .selfProofProvider()
1240            .call()
1241            .await
1242            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1243
1244        // Cache for longer (12x normal TTL — ~1 hour at default)
1245        self.self_provider_cache = Some((address, now + self.cache_ttl_ms * 12));
1246
1247        Ok(address)
1248    }
1249
1250    /// Fetch ZK-attested credentials for an agent.
1251    async fn fetch_credentials(&self, agent_id: U256) -> Result<AgentCredentials, crate::Error> {
1252        let provider = self.make_provider()?;
1253        let registry = IAgentRegistry::new(self.registry_address, &provider);
1254
1255        let raw = registry
1256            .getAgentCredentials(agent_id)
1257            .call()
1258            .await
1259            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
1260
1261        Ok(AgentCredentials {
1262            issuing_state: raw.issuingState,
1263            name: raw.name,
1264            id_number: raw.idNumber,
1265            nationality: raw.nationality,
1266            date_of_birth: raw.dateOfBirth,
1267            gender: raw.gender,
1268            expiry_date: raw.expiryDate,
1269            older_than: raw.olderThan,
1270            ofac: raw.ofac.to_vec(),
1271        })
1272    }
1273
1274    /// Clear the on-chain status cache.
1275    pub fn clear_cache(&mut self) {
1276        self.cache.clear();
1277        self.replay_cache.clear();
1278        self.self_provider_cache = None;
1279    }
1280
1281    fn check_and_record_replay(
1282        &mut self,
1283        signature: &str,
1284        message: &str,
1285        ts: u64,
1286        now: u64,
1287    ) -> Option<String> {
1288        self.prune_replay_cache(now);
1289
1290        let key = format!(
1291            "{}:{}",
1292            signature.to_ascii_lowercase(),
1293            message.to_ascii_lowercase()
1294        );
1295        if let Some(expires_at) = self.replay_cache.get(&key) {
1296            if *expires_at > now {
1297                return Some("Replay detected".to_string());
1298            }
1299        }
1300
1301        self.replay_cache.insert(key, ts.saturating_add(self.max_age_ms));
1302        None
1303    }
1304
1305    fn prune_replay_cache(&mut self, now: u64) {
1306        self.replay_cache.retain(|_, exp| *exp > now);
1307
1308        if self.replay_cache.len() <= self.replay_cache_max_entries {
1309            return;
1310        }
1311
1312        let overflow = self.replay_cache.len() - self.replay_cache_max_entries;
1313        let mut items: Vec<(String, u64)> =
1314            self.replay_cache.iter().map(|(k, v)| (k.clone(), *v)).collect();
1315        items.sort_by_key(|(_, exp)| *exp);
1316
1317        for (key, _) in items.into_iter().take(overflow) {
1318            self.replay_cache.remove(&key);
1319        }
1320    }
1321}
1322
1323/// Recover signer address from an EIP-191 personal_sign signature over raw 32 bytes.
1324///
1325/// Matches TS: `ethers.verifyMessage(ethers.getBytes(message), signature)`
1326fn recover_address(message: &B256, signature_hex: &str) -> Result<Address, crate::Error> {
1327    let sig_bytes = hex::decode(signature_hex.strip_prefix("0x").unwrap_or(signature_hex))
1328        .map_err(|_| crate::Error::InvalidSignature)?;
1329
1330    let signature = Signature::try_from(sig_bytes.as_slice())
1331        .map_err(|_| crate::Error::InvalidSignature)?;
1332
1333    // EIP-191: prefix with "\x19Ethereum Signed Message:\n32" then hash
1334    let prefixed = alloy::primitives::eip191_hash_message(message.as_slice());
1335
1336    let recovered = signature
1337        .recover_address_from_prehash(&prefixed)
1338        .map_err(|_| crate::Error::InvalidSignature)?;
1339
1340    Ok(recovered)
1341}
1342
1343fn now_millis() -> u64 {
1344    SystemTime::now()
1345        .duration_since(UNIX_EPOCH)
1346        .expect("system clock before UNIX epoch")
1347        .as_millis() as u64
1348}
1349
1350// ---------------------------------------------------------------------------
1351// Tests
1352// ---------------------------------------------------------------------------
1353
1354#[cfg(test)]
1355mod tests {
1356    use super::*;
1357
1358    #[test]
1359    fn create_build_default() {
1360        let v = SelfAgentVerifier::create().build();
1361        // Defaults: mainnet, max_agents_per_human=1, require_self_provider=true
1362        assert_eq!(v.max_agents_per_human, 1);
1363        assert!(v.require_self_provider);
1364        assert!(v.enable_replay_protection);
1365        assert!(!v.include_credentials);
1366        assert!(v.minimum_age.is_none());
1367        assert!(!v.require_ofac_passed);
1368        assert!(v.allowed_nationalities.is_none());
1369        assert!(v.rate_limiter.is_none());
1370    }
1371
1372    #[test]
1373    fn create_build_testnet() {
1374        let v = SelfAgentVerifier::create()
1375            .network(NetworkName::Testnet)
1376            .build();
1377        let expected = network_config(NetworkName::Testnet);
1378        assert_eq!(v.registry_address, expected.registry_address);
1379        assert_eq!(v.rpc_url, expected.rpc_url);
1380    }
1381
1382    #[test]
1383    fn chain_credentials() {
1384        let v = SelfAgentVerifier::create()
1385            .network(NetworkName::Testnet)
1386            .require_age(18)
1387            .require_ofac()
1388            .require_nationality(&["US", "GB"])
1389            .build();
1390
1391        // Auto-enabled include_credentials
1392        assert!(v.include_credentials);
1393        assert_eq!(v.minimum_age, Some(18));
1394        assert!(v.require_ofac_passed);
1395        assert_eq!(
1396            v.allowed_nationalities.as_deref(),
1397            Some(vec!["US".to_string(), "GB".to_string()].as_slice())
1398        );
1399    }
1400
1401    #[test]
1402    fn auto_enable_credentials_age_only() {
1403        let v = SelfAgentVerifier::create()
1404            .require_age(21)
1405            .build();
1406        assert!(v.include_credentials);
1407        assert_eq!(v.minimum_age, Some(21));
1408    }
1409
1410    #[test]
1411    fn auto_enable_credentials_ofac_only() {
1412        let v = SelfAgentVerifier::create()
1413            .require_ofac()
1414            .build();
1415        assert!(v.include_credentials);
1416        assert!(v.require_ofac_passed);
1417    }
1418
1419    #[test]
1420    fn auto_enable_credentials_nationality_only() {
1421        let v = SelfAgentVerifier::create()
1422            .require_nationality(&["DE"])
1423            .build();
1424        assert!(v.include_credentials);
1425    }
1426
1427    #[test]
1428    fn no_auto_credentials_without_requirements() {
1429        let v = SelfAgentVerifier::create()
1430            .network(NetworkName::Testnet)
1431            .sybil_limit(3)
1432            .build();
1433        assert!(!v.include_credentials);
1434    }
1435
1436    #[test]
1437    fn explicit_include_credentials() {
1438        let v = SelfAgentVerifier::create()
1439            .include_credentials()
1440            .build();
1441        assert!(v.include_credentials);
1442    }
1443
1444    #[test]
1445    fn from_config_works() {
1446        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1447            network: Some(NetworkName::Testnet),
1448            require_age: Some(18),
1449            require_ofac: Some(true),
1450            sybil_limit: Some(1),
1451            ..Default::default()
1452        });
1453        assert!(v.include_credentials);
1454        assert_eq!(v.minimum_age, Some(18));
1455        assert!(v.require_ofac_passed);
1456        assert_eq!(v.max_agents_per_human, 1);
1457    }
1458
1459    #[test]
1460    fn from_config_auto_credentials_disabled() {
1461        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1462            network: Some(NetworkName::Testnet),
1463            sybil_limit: Some(5),
1464            ..Default::default()
1465        });
1466        assert!(!v.include_credentials);
1467    }
1468
1469    #[test]
1470    fn from_config_nationality() {
1471        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1472            require_nationality: Some(vec!["FR".to_string(), "IT".to_string()]),
1473            ..Default::default()
1474        });
1475        assert!(v.include_credentials);
1476        assert_eq!(
1477            v.allowed_nationalities.as_deref(),
1478            Some(vec!["FR".to_string(), "IT".to_string()].as_slice())
1479        );
1480    }
1481
1482    #[test]
1483    fn rate_limit_builder() {
1484        let v = SelfAgentVerifier::create()
1485            .rate_limit(10, 100)
1486            .build();
1487        assert!(v.rate_limiter.is_some());
1488        let limiter = v.rate_limiter.as_ref().unwrap();
1489        assert_eq!(limiter.per_minute, 10);
1490        assert_eq!(limiter.per_hour, 100);
1491    }
1492
1493    #[test]
1494    fn rate_limit_from_config() {
1495        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1496            rate_limit: Some(RateLimitConfig {
1497                per_minute: Some(5),
1498                per_hour: Some(50),
1499            }),
1500            ..Default::default()
1501        });
1502        assert!(v.rate_limiter.is_some());
1503    }
1504
1505    #[test]
1506    fn rate_limiter_allows_within_limit() {
1507        let config = RateLimitConfig {
1508            per_minute: Some(3),
1509            per_hour: None,
1510        };
1511        let mut limiter = RateLimiter::new(&config);
1512        assert!(limiter.check("0xabc").is_none());
1513        assert!(limiter.check("0xabc").is_none());
1514        assert!(limiter.check("0xabc").is_none());
1515        // 4th request should be rate limited
1516        let result = limiter.check("0xabc");
1517        assert!(result.is_some());
1518        let r = result.unwrap();
1519        assert!(r.error.contains("3/min"));
1520        assert!(r.retry_after_ms > 0);
1521    }
1522
1523    #[test]
1524    fn rate_limiter_separate_agents() {
1525        let config = RateLimitConfig {
1526            per_minute: Some(1),
1527            per_hour: None,
1528        };
1529        let mut limiter = RateLimiter::new(&config);
1530        assert!(limiter.check("0xabc").is_none());
1531        assert!(limiter.check("0xdef").is_none());
1532        // Same agent again = limited
1533        assert!(limiter.check("0xabc").is_some());
1534        // Different agent still allowed
1535        assert!(limiter.check("0xghi").is_none());
1536    }
1537
1538    #[test]
1539    fn builder_custom_max_age_and_cache_ttl() {
1540        let v = SelfAgentVerifier::create()
1541            .max_age(10_000)
1542            .cache_ttl(30_000)
1543            .build();
1544        assert_eq!(v.max_age_ms, 10_000);
1545        assert_eq!(v.cache_ttl_ms, 30_000);
1546    }
1547
1548    #[test]
1549    fn builder_sybil_limit_zero_disables() {
1550        let v = SelfAgentVerifier::create()
1551            .sybil_limit(0)
1552            .build();
1553        assert_eq!(v.max_agents_per_human, 0);
1554    }
1555
1556    #[test]
1557    fn builder_replay_protection() {
1558        let v = SelfAgentVerifier::create()
1559            .replay_protection()
1560            .build();
1561        assert!(v.enable_replay_protection);
1562    }
1563
1564    #[test]
1565    fn builder_require_self_provider() {
1566        let v = SelfAgentVerifier::create()
1567            .require_self_provider()
1568            .build();
1569        assert!(v.require_self_provider);
1570    }
1571
1572    #[test]
1573    fn new_constructor_still_works() {
1574        let v = SelfAgentVerifier::new(VerifierConfig::default());
1575        assert_eq!(v.max_age_ms, DEFAULT_MAX_AGE_MS);
1576        assert_eq!(v.cache_ttl_ms, DEFAULT_CACHE_TTL_MS);
1577        assert_eq!(v.max_agents_per_human, 1);
1578        assert!(v.require_self_provider);
1579    }
1580
1581    #[test]
1582    fn new_constructor_with_credentials() {
1583        let v = SelfAgentVerifier::new(VerifierConfig {
1584            minimum_age: Some(21),
1585            require_ofac_passed: Some(true),
1586            include_credentials: Some(true),
1587            ..Default::default()
1588        });
1589        assert!(v.include_credentials);
1590        assert_eq!(v.minimum_age, Some(21));
1591        assert!(v.require_ofac_passed);
1592    }
1593
1594    #[test]
1595    fn verification_result_has_retry_after() {
1596        let r = VerificationResult::empty_with_error("test");
1597        assert!(r.retry_after_ms.is_none());
1598    }
1599}