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};
16
17// ---------------------------------------------------------------------------
18// Configuration structs
19// ---------------------------------------------------------------------------
20
21/// Configuration for creating a [`SelfAgentVerifier`].
22#[derive(Debug, Clone)]
23pub struct VerifierConfig {
24    /// Network to use: Mainnet (default) or Testnet.
25    pub network: Option<NetworkName>,
26    /// Override: custom registry address.
27    pub registry_address: Option<Address>,
28    /// Override: custom RPC URL.
29    pub rpc_url: Option<String>,
30    /// Max age for signed timestamps in ms (default: 5 min).
31    pub max_age_ms: Option<u64>,
32    /// TTL for on-chain status cache in ms (default: 1 min).
33    pub cache_ttl_ms: Option<u64>,
34    /// Max agents allowed per human (default: 1). Set to 0 to disable.
35    pub max_agents_per_human: Option<u64>,
36    /// Include ZK-attested credentials in verification result (default: false).
37    pub include_credentials: Option<bool>,
38    /// Require proof-of-human was provided by Self Protocol (default: true).
39    pub require_self_provider: Option<bool>,
40    /// Reject duplicate signatures within validity window (default: true).
41    pub enable_replay_protection: Option<bool>,
42    /// Max replay cache entries before pruning (default: 10k).
43    pub replay_cache_max_entries: Option<usize>,
44    /// Minimum age for agent's human (credential check, default: disabled).
45    pub minimum_age: Option<u64>,
46    /// Require OFAC screening passed (credential check, default: false).
47    pub require_ofac_passed: Option<bool>,
48    /// Require nationality in list (credential check, default: disabled).
49    pub allowed_nationalities: Option<Vec<String>>,
50    /// In-memory per-agent rate limiting.
51    pub rate_limit_config: Option<RateLimitConfig>,
52}
53
54impl Default for VerifierConfig {
55    fn default() -> Self {
56        Self {
57            network: None,
58            registry_address: None,
59            rpc_url: None,
60            max_age_ms: None,
61            cache_ttl_ms: None,
62            max_agents_per_human: None,
63            include_credentials: None,
64            require_self_provider: None,
65            enable_replay_protection: None,
66            replay_cache_max_entries: None,
67            minimum_age: None,
68            require_ofac_passed: None,
69            allowed_nationalities: None,
70            rate_limit_config: None,
71        }
72    }
73}
74
75/// Rate limit configuration for per-agent request throttling.
76#[derive(Debug, Clone)]
77pub struct RateLimitConfig {
78    /// Max requests per agent per minute.
79    pub per_minute: Option<u32>,
80    /// Max requests per agent per hour.
81    pub per_hour: Option<u32>,
82}
83
84/// Config object for the `from_config` static factory.
85#[derive(Debug, Clone, Default)]
86pub struct VerifierFromConfig {
87    pub network: Option<NetworkName>,
88    pub registry_address: Option<String>,
89    pub rpc_url: Option<String>,
90    pub require_age: Option<u64>,
91    pub require_ofac: Option<bool>,
92    pub require_nationality: Option<Vec<String>>,
93    pub require_self_provider: Option<bool>,
94    pub sybil_limit: Option<u64>,
95    pub rate_limit: Option<RateLimitConfig>,
96    pub replay_protection: Option<bool>,
97    pub max_age_ms: Option<u64>,
98    pub cache_ttl_ms: Option<u64>,
99}
100
101// ---------------------------------------------------------------------------
102// Credential + result types
103// ---------------------------------------------------------------------------
104
105/// ZK-attested credential claims stored on-chain for an agent.
106#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
107pub struct AgentCredentials {
108    pub issuing_state: String,
109    pub name: Vec<String>,
110    pub id_number: String,
111    pub nationality: String,
112    pub date_of_birth: String,
113    pub gender: String,
114    pub expiry_date: String,
115    pub older_than: U256,
116    pub ofac: Vec<bool>,
117}
118
119/// Result of verifying an agent request.
120#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
121pub struct VerificationResult {
122    pub valid: bool,
123    /// The agent's Ethereum address (recovered from signature).
124    pub agent_address: Address,
125    /// The agent's on-chain key (bytes32).
126    pub agent_key: B256,
127    pub agent_id: U256,
128    /// Number of agents registered by the same human.
129    pub agent_count: U256,
130    /// Human's nullifier (for rate limiting by human identity).
131    pub nullifier: U256,
132    /// ZK-attested credentials (only populated when include_credentials is true).
133    pub credentials: Option<AgentCredentials>,
134    pub error: Option<String>,
135    /// Milliseconds until the rate limit resets (only set when rate limited).
136    pub retry_after_ms: Option<u64>,
137}
138
139impl VerificationResult {
140    fn empty_with_error(error: &str) -> Self {
141        Self {
142            valid: false,
143            agent_address: Address::ZERO,
144            agent_key: B256::ZERO,
145            agent_id: U256::ZERO,
146            agent_count: U256::ZERO,
147            nullifier: U256::ZERO,
148            credentials: None,
149            error: Some(error.to_string()),
150            retry_after_ms: None,
151        }
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Rate limiter — sliding window, keyed by agent address
157// ---------------------------------------------------------------------------
158
159struct RateBucket {
160    timestamps: Vec<u64>,
161}
162
163struct RateLimitResult {
164    error: String,
165    retry_after_ms: u64,
166}
167
168/// In-memory sliding-window rate limiter keyed by agent address.
169struct RateLimiter {
170    per_minute: u32,
171    per_hour: u32,
172    buckets: HashMap<String, RateBucket>,
173}
174
175impl RateLimiter {
176    fn new(config: &RateLimitConfig) -> Self {
177        Self {
178            per_minute: config.per_minute.unwrap_or(0),
179            per_hour: config.per_hour.unwrap_or(0),
180            buckets: HashMap::new(),
181        }
182    }
183
184    /// Returns `None` if allowed, or a `RateLimitResult` if rate limited.
185    fn check(&mut self, agent_address: &str) -> Option<RateLimitResult> {
186        let now = now_millis();
187        let key = agent_address.to_ascii_lowercase();
188        let bucket = self
189            .buckets
190            .entry(key)
191            .or_insert_with(|| RateBucket { timestamps: Vec::new() });
192
193        // Prune timestamps older than 1 hour (longest window we care about)
194        let one_hour_ago = now.saturating_sub(60 * 60 * 1000);
195        bucket.timestamps.retain(|t| *t > one_hour_ago);
196
197        // Check per-minute limit
198        if self.per_minute > 0 {
199            let one_minute_ago = now.saturating_sub(60 * 1000);
200            let recent_minute: Vec<u64> = bucket
201                .timestamps
202                .iter()
203                .filter(|t| **t > one_minute_ago)
204                .copied()
205                .collect();
206            if recent_minute.len() >= self.per_minute as usize {
207                let oldest = recent_minute[0];
208                let retry_after = (oldest + 60 * 1000).saturating_sub(now).max(1);
209                return Some(RateLimitResult {
210                    error: format!("Rate limit exceeded ({}/min)", self.per_minute),
211                    retry_after_ms: retry_after,
212                });
213            }
214        }
215
216        // Check per-hour limit
217        if self.per_hour > 0 && bucket.timestamps.len() >= self.per_hour as usize {
218            let oldest = bucket.timestamps[0];
219            let retry_after = (oldest + 60 * 60 * 1000).saturating_sub(now).max(1);
220            return Some(RateLimitResult {
221                error: format!("Rate limit exceeded ({}/hr)", self.per_hour),
222                retry_after_ms: retry_after,
223            });
224        }
225
226        // Record this request
227        bucket.timestamps.push(now);
228        None
229    }
230}
231
232// ---------------------------------------------------------------------------
233// VerifierBuilder — chainable builder API
234// ---------------------------------------------------------------------------
235
236/// Chainable builder for creating a [`SelfAgentVerifier`].
237///
238/// # Example
239/// ```no_run
240/// use self_agent_sdk::{NetworkName, SelfAgentVerifier};
241///
242/// let verifier = SelfAgentVerifier::create()
243///     .network(NetworkName::Testnet)
244///     .require_age(18)
245///     .require_ofac()
246///     .require_nationality(&["US", "GB"])
247///     .rate_limit(10, 100)
248///     .build();
249/// ```
250#[derive(Default)]
251pub struct VerifierBuilder {
252    network: Option<NetworkName>,
253    registry_address: Option<String>,
254    rpc_url: Option<String>,
255    max_age_ms: Option<u64>,
256    cache_ttl_ms: Option<u64>,
257    max_agents_per_human: Option<u64>,
258    include_credentials: Option<bool>,
259    require_self_provider: Option<bool>,
260    enable_replay_protection: Option<bool>,
261    minimum_age: Option<u64>,
262    require_ofac_passed: bool,
263    allowed_nationalities: Option<Vec<String>>,
264    rate_limit_config: Option<RateLimitConfig>,
265}
266
267impl VerifierBuilder {
268    /// Set the network: `Mainnet` or `Testnet`.
269    pub fn network(mut self, name: NetworkName) -> Self {
270        self.network = Some(name);
271        self
272    }
273
274    /// Set a custom registry address.
275    pub fn registry(mut self, addr: &str) -> Self {
276        self.registry_address = Some(addr.to_string());
277        self
278    }
279
280    /// Set a custom RPC URL.
281    pub fn rpc(mut self, url: &str) -> Self {
282        self.rpc_url = Some(url.to_string());
283        self
284    }
285
286    /// Require the agent's human to be at least `n` years old.
287    pub fn require_age(mut self, n: u64) -> Self {
288        self.minimum_age = Some(n);
289        self
290    }
291
292    /// Require OFAC screening passed.
293    pub fn require_ofac(mut self) -> Self {
294        self.require_ofac_passed = true;
295        self
296    }
297
298    /// Require nationality in the given list of ISO country codes.
299    pub fn require_nationality(mut self, codes: &[&str]) -> Self {
300        self.allowed_nationalities = Some(codes.iter().map(|s| s.to_string()).collect());
301        self
302    }
303
304    /// Require Self Protocol as proof provider (default: on).
305    pub fn require_self_provider(mut self) -> Self {
306        self.require_self_provider = Some(true);
307        self
308    }
309
310    /// Max agents per human (default: 1). Set to 0 to disable sybil check.
311    pub fn sybil_limit(mut self, n: u64) -> Self {
312        self.max_agents_per_human = Some(n);
313        self
314    }
315
316    /// Enable in-memory per-agent rate limiting.
317    pub fn rate_limit(mut self, per_minute: u32, per_hour: u32) -> Self {
318        self.rate_limit_config = Some(RateLimitConfig {
319            per_minute: Some(per_minute),
320            per_hour: Some(per_hour),
321        });
322        self
323    }
324
325    /// Enable replay protection (default: on).
326    pub fn replay_protection(mut self) -> Self {
327        self.enable_replay_protection = Some(true);
328        self
329    }
330
331    /// Include ZK credentials in verification result.
332    pub fn include_credentials(mut self) -> Self {
333        self.include_credentials = Some(true);
334        self
335    }
336
337    /// Max signed timestamp age in milliseconds.
338    pub fn max_age(mut self, ms: u64) -> Self {
339        self.max_age_ms = Some(ms);
340        self
341    }
342
343    /// On-chain cache TTL in milliseconds.
344    pub fn cache_ttl(mut self, ms: u64) -> Self {
345        self.cache_ttl_ms = Some(ms);
346        self
347    }
348
349    /// Build the [`SelfAgentVerifier`] instance.
350    ///
351    /// Automatically enables `include_credentials` when any credential
352    /// requirement is set (age, OFAC, nationality).
353    pub fn build(self) -> SelfAgentVerifier {
354        // Auto-enable credentials if any credential requirement is set
355        let needs_credentials = self.minimum_age.is_some()
356            || self.require_ofac_passed
357            || self
358                .allowed_nationalities
359                .as_ref()
360                .map_or(false, |v| !v.is_empty());
361
362        let registry_address = self
363            .registry_address
364            .and_then(|s| s.parse::<Address>().ok());
365
366        SelfAgentVerifier::new(VerifierConfig {
367            network: self.network,
368            registry_address,
369            rpc_url: self.rpc_url,
370            max_age_ms: self.max_age_ms,
371            cache_ttl_ms: self.cache_ttl_ms,
372            max_agents_per_human: self.max_agents_per_human,
373            include_credentials: if needs_credentials || self.include_credentials.unwrap_or(false) {
374                Some(true)
375            } else {
376                self.include_credentials
377            },
378            require_self_provider: self.require_self_provider,
379            enable_replay_protection: self.enable_replay_protection,
380            replay_cache_max_entries: None,
381            minimum_age: self.minimum_age,
382            require_ofac_passed: if self.require_ofac_passed {
383                Some(true)
384            } else {
385                None
386            },
387            allowed_nationalities: self.allowed_nationalities,
388            rate_limit_config: self.rate_limit_config,
389        })
390    }
391}
392
393// ---------------------------------------------------------------------------
394// Internal cache types
395// ---------------------------------------------------------------------------
396
397struct CacheEntry {
398    is_verified: bool,
399    is_proof_fresh: bool,
400    agent_id: U256,
401    agent_count: U256,
402    nullifier: U256,
403    provider_address: Address,
404    expires_at: u64,
405}
406
407struct OnChainStatus {
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}
415
416// ---------------------------------------------------------------------------
417// SelfAgentVerifier
418// ---------------------------------------------------------------------------
419
420/// Service-side verifier for Self Agent ID requests.
421///
422/// Security chain:
423/// 1. Recover signer address from ECDSA signature
424/// 2. Derive agent key: zeroPadValue(recoveredAddress, 32)
425/// 3. Check on-chain: isVerifiedAgent(agentKey)
426/// 4. Check proof provider matches selfProofProvider()
427/// 5. Check timestamp freshness (replay protection)
428/// 6. Sybil resistance check
429/// 7. Credential checks (age, OFAC, nationality)
430/// 8. Rate limiting
431///
432/// # Construction
433///
434/// ```no_run
435/// use self_agent_sdk::{
436///     NetworkName, SelfAgentVerifier, VerifierConfig, VerifierFromConfig,
437/// };
438///
439/// // Direct construction
440/// let verifier = SelfAgentVerifier::new(VerifierConfig::default());
441///
442/// // Chainable builder
443/// let verifier = SelfAgentVerifier::create()
444///     .network(NetworkName::Testnet)
445///     .require_age(18)
446///     .require_ofac()
447///     .build();
448///
449/// // From config object
450/// let verifier = SelfAgentVerifier::from_config(VerifierFromConfig {
451///     network: Some(NetworkName::Testnet),
452///     require_age: Some(18),
453///     ..Default::default()
454/// });
455/// ```
456pub struct SelfAgentVerifier {
457    registry_address: Address,
458    rpc_url: String,
459    max_age_ms: u64,
460    cache_ttl_ms: u64,
461    max_agents_per_human: u64,
462    include_credentials: bool,
463    require_self_provider: bool,
464    enable_replay_protection: bool,
465    replay_cache_max_entries: usize,
466    minimum_age: Option<u64>,
467    require_ofac_passed: bool,
468    allowed_nationalities: Option<Vec<String>>,
469    rate_limiter: Option<RateLimiter>,
470    cache: HashMap<B256, CacheEntry>,
471    replay_cache: HashMap<String, u64>,
472    self_provider_cache: Option<(Address, u64)>,
473}
474
475impl SelfAgentVerifier {
476    /// Create a new verifier instance from a [`VerifierConfig`].
477    pub fn new(config: VerifierConfig) -> Self {
478        let net = network_config(config.network.unwrap_or(DEFAULT_NETWORK));
479        Self {
480            registry_address: config.registry_address.unwrap_or(net.registry_address),
481            rpc_url: config.rpc_url.unwrap_or_else(|| net.rpc_url.to_string()),
482            max_age_ms: config.max_age_ms.unwrap_or(DEFAULT_MAX_AGE_MS),
483            cache_ttl_ms: config.cache_ttl_ms.unwrap_or(DEFAULT_CACHE_TTL_MS),
484            max_agents_per_human: config.max_agents_per_human.unwrap_or(1),
485            include_credentials: config.include_credentials.unwrap_or(false),
486            require_self_provider: config.require_self_provider.unwrap_or(true),
487            enable_replay_protection: config.enable_replay_protection.unwrap_or(true),
488            replay_cache_max_entries: config.replay_cache_max_entries.unwrap_or(10_000),
489            minimum_age: config.minimum_age,
490            require_ofac_passed: config.require_ofac_passed.unwrap_or(false),
491            allowed_nationalities: config.allowed_nationalities,
492            rate_limiter: config.rate_limit_config.as_ref().map(RateLimiter::new),
493            cache: HashMap::new(),
494            replay_cache: HashMap::new(),
495            self_provider_cache: None,
496        }
497    }
498
499    /// Create a chainable [`VerifierBuilder`] for configuring a verifier.
500    pub fn create() -> VerifierBuilder {
501        VerifierBuilder::default()
502    }
503
504    /// Create a verifier from a flat config object.
505    ///
506    /// Automatically enables `include_credentials` when any credential
507    /// requirement is set (age, OFAC, nationality).
508    pub fn from_config(cfg: VerifierFromConfig) -> Self {
509        let needs_credentials = cfg.require_age.is_some()
510            || cfg.require_ofac.unwrap_or(false)
511            || cfg
512                .require_nationality
513                .as_ref()
514                .map_or(false, |v| !v.is_empty());
515
516        let registry_address = cfg
517            .registry_address
518            .and_then(|s| s.parse::<Address>().ok());
519
520        Self::new(VerifierConfig {
521            network: cfg.network,
522            registry_address,
523            rpc_url: cfg.rpc_url,
524            max_age_ms: cfg.max_age_ms,
525            cache_ttl_ms: cfg.cache_ttl_ms,
526            max_agents_per_human: cfg.sybil_limit,
527            include_credentials: if needs_credentials { Some(true) } else { None },
528            require_self_provider: cfg.require_self_provider,
529            enable_replay_protection: cfg.replay_protection,
530            replay_cache_max_entries: None,
531            minimum_age: cfg.require_age,
532            require_ofac_passed: cfg.require_ofac,
533            allowed_nationalities: cfg.require_nationality,
534            rate_limit_config: cfg.rate_limit,
535        })
536    }
537
538    fn make_provider(
539        &self,
540    ) -> Result<impl alloy::providers::Provider + Clone, crate::Error> {
541        let url: reqwest::Url = self
542            .rpc_url
543            .parse()
544            .map_err(|_| crate::Error::InvalidRpcUrl)?;
545        Ok(ProviderBuilder::new().connect_http(url))
546    }
547
548    /// Verify a signed agent request.
549    ///
550    /// The agent's identity is derived from the signature — not from any header.
551    pub async fn verify(
552        &mut self,
553        signature: &str,
554        timestamp: &str,
555        method: &str,
556        url: &str,
557        body: Option<&str>,
558    ) -> VerificationResult {
559        // 1. Check timestamp freshness (replay protection)
560        let ts: u64 = match timestamp.parse() {
561            Ok(v) => v,
562            Err(_) => return VerificationResult::empty_with_error("Timestamp expired or invalid"),
563        };
564        let now = now_millis();
565        let diff = if now > ts { now - ts } else { ts - now };
566        if diff > self.max_age_ms {
567            return VerificationResult::empty_with_error("Timestamp expired or invalid");
568        }
569
570        // 2. Reconstruct the signed message
571        let message = compute_signing_message(timestamp, method, url, body);
572        let message_key = format!("{:#x}", message);
573
574        // 3. Recover signer address from signature
575        let signer_address = match recover_address(&message, signature) {
576            Ok(addr) => addr,
577            Err(_) => return VerificationResult::empty_with_error("Invalid signature"),
578        };
579
580        // 4. Replay cache check (after signature validity to avoid cache poisoning)
581        if self.enable_replay_protection {
582            if let Some(err) = self.check_and_record_replay(signature, &message_key, ts, now) {
583                return VerificationResult {
584                    valid: false,
585                    agent_address: signer_address,
586                    agent_key: address_to_agent_key(signer_address),
587                    agent_id: U256::ZERO,
588                    agent_count: U256::ZERO,
589                    nullifier: U256::ZERO,
590                    credentials: None,
591                    error: Some(err),
592                    retry_after_ms: None,
593                };
594            }
595        }
596
597        // 5. Derive the on-chain agent key from the recovered address
598        let agent_key = address_to_agent_key(signer_address);
599
600        // 6. Check on-chain status (with cache)
601        let on_chain = match self.check_on_chain(agent_key).await {
602            Ok(v) => v,
603            Err(e) => {
604                return VerificationResult {
605                    valid: false,
606                    agent_address: signer_address,
607                    agent_key,
608                    agent_id: U256::ZERO,
609                    agent_count: U256::ZERO,
610                    nullifier: U256::ZERO,
611                    credentials: None,
612                    error: Some(format!("RPC error: {}", e)),
613                    retry_after_ms: None,
614                };
615            }
616        };
617
618        if !on_chain.is_verified {
619            return VerificationResult {
620                valid: false,
621                agent_address: signer_address,
622                agent_key,
623                agent_id: on_chain.agent_id,
624                agent_count: on_chain.agent_count,
625                nullifier: on_chain.nullifier,
626                credentials: None,
627                error: Some("Agent not verified on-chain".to_string()),
628                retry_after_ms: None,
629            };
630        }
631
632        // 6b. Check proof freshness (expired proofs should not pass verification)
633        if !on_chain.is_proof_fresh {
634            return VerificationResult {
635                valid: false,
636                agent_address: signer_address,
637                agent_key,
638                agent_id: on_chain.agent_id,
639                agent_count: on_chain.agent_count,
640                nullifier: on_chain.nullifier,
641                credentials: None,
642                error: Some("Agent's human proof has expired".to_string()),
643                retry_after_ms: None,
644            };
645        }
646
647        // 7. Provider check: ensure agent was verified by Self Protocol
648        if self.require_self_provider && on_chain.agent_id > U256::ZERO {
649            let self_provider = match self.get_self_provider_address().await {
650                Ok(addr) => addr,
651                Err(_) => {
652                    return VerificationResult {
653                        valid: false,
654                        agent_address: signer_address,
655                        agent_key,
656                        agent_id: on_chain.agent_id,
657                        agent_count: on_chain.agent_count,
658                        nullifier: on_chain.nullifier,
659                        credentials: None,
660                        error: Some(
661                            "Unable to verify proof provider — RPC error".to_string(),
662                        ),
663                        retry_after_ms: None,
664                    };
665                }
666            };
667            if on_chain.provider_address != self_provider {
668                return VerificationResult {
669                    valid: false,
670                    agent_address: signer_address,
671                    agent_key,
672                    agent_id: on_chain.agent_id,
673                    agent_count: on_chain.agent_count,
674                    nullifier: on_chain.nullifier,
675                    credentials: None,
676                    error: Some(
677                        "Agent was not verified by Self — proof provider mismatch".to_string(),
678                    ),
679                    retry_after_ms: None,
680                };
681            }
682        }
683
684        // 8. Sybil resistance: reject if human has too many agents
685        if self.max_agents_per_human > 0
686            && on_chain.agent_count > U256::from(self.max_agents_per_human)
687        {
688            return VerificationResult {
689                valid: false,
690                agent_address: signer_address,
691                agent_key,
692                agent_id: on_chain.agent_id,
693                agent_count: on_chain.agent_count,
694                nullifier: on_chain.nullifier,
695                credentials: None,
696                error: Some(format!(
697                    "Human has {} agents (max {})",
698                    on_chain.agent_count, self.max_agents_per_human
699                )),
700                retry_after_ms: None,
701            };
702        }
703
704        // 9. Fetch credentials if requested
705        let credentials = if self.include_credentials && on_chain.agent_id > U256::ZERO {
706            self.fetch_credentials(on_chain.agent_id).await.ok()
707        } else {
708            None
709        };
710
711        // 10. Credential checks (post-verify — only if credentials were fetched)
712        if let Some(ref creds) = credentials {
713            if let Some(min_age) = self.minimum_age {
714                if creds.older_than < U256::from(min_age) {
715                    return VerificationResult {
716                        valid: false,
717                        agent_address: signer_address,
718                        agent_key,
719                        agent_id: on_chain.agent_id,
720                        agent_count: on_chain.agent_count,
721                        nullifier: on_chain.nullifier,
722                        credentials: credentials.clone(),
723                        error: Some(format!(
724                            "Agent's human does not meet minimum age (required: {}, got: {})",
725                            min_age, creds.older_than
726                        )),
727                        retry_after_ms: None,
728                    };
729                }
730            }
731
732            if self.require_ofac_passed && !creds.ofac.first().copied().unwrap_or(false) {
733                return VerificationResult {
734                    valid: false,
735                    agent_address: signer_address,
736                    agent_key,
737                    agent_id: on_chain.agent_id,
738                    agent_count: on_chain.agent_count,
739                    nullifier: on_chain.nullifier,
740                    credentials: credentials.clone(),
741                    error: Some("Agent's human did not pass OFAC screening".to_string()),
742                    retry_after_ms: None,
743                };
744            }
745
746            if let Some(ref allowed) = self.allowed_nationalities {
747                if !allowed.is_empty() && !allowed.contains(&creds.nationality) {
748                    return VerificationResult {
749                        valid: false,
750                        agent_address: signer_address,
751                        agent_key,
752                        agent_id: on_chain.agent_id,
753                        agent_count: on_chain.agent_count,
754                        nullifier: on_chain.nullifier,
755                        credentials: credentials.clone(),
756                        error: Some(format!(
757                            "Nationality \"{}\" not in allowed list",
758                            creds.nationality
759                        )),
760                        retry_after_ms: None,
761                    };
762                }
763            }
764        }
765
766        // 11. Rate limiting (per-agent, in-memory sliding window)
767        if let Some(ref mut limiter) = self.rate_limiter {
768            let addr_str = format!("{:#x}", signer_address);
769            if let Some(limited) = limiter.check(&addr_str) {
770                return VerificationResult {
771                    valid: false,
772                    agent_address: signer_address,
773                    agent_key,
774                    agent_id: on_chain.agent_id,
775                    agent_count: on_chain.agent_count,
776                    nullifier: on_chain.nullifier,
777                    credentials,
778                    error: Some(limited.error),
779                    retry_after_ms: Some(limited.retry_after_ms),
780                };
781            }
782        }
783
784        VerificationResult {
785            valid: true,
786            agent_address: signer_address,
787            agent_key,
788            agent_id: on_chain.agent_id,
789            agent_count: on_chain.agent_count,
790            nullifier: on_chain.nullifier,
791            credentials,
792            error: None,
793            retry_after_ms: None,
794        }
795    }
796
797    /// Check on-chain agent status with caching.
798    async fn check_on_chain(&mut self, agent_key: B256) -> Result<OnChainStatus, crate::Error> {
799        let now = now_millis();
800        if let Some(cached) = self.cache.get(&agent_key) {
801            if cached.expires_at > now {
802                return Ok(OnChainStatus {
803                    is_verified: cached.is_verified,
804                    is_proof_fresh: cached.is_proof_fresh,
805                    agent_id: cached.agent_id,
806                    agent_count: cached.agent_count,
807                    nullifier: cached.nullifier,
808                    provider_address: cached.provider_address,
809                });
810            }
811        }
812
813        let provider = self.make_provider()?;
814        let registry = IAgentRegistry::new(self.registry_address, &provider);
815
816        let is_verified = registry
817            .isVerifiedAgent(agent_key)
818            .call()
819            .await
820            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
821        let agent_id = registry
822            .getAgentId(agent_key)
823            .call()
824            .await
825            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
826
827        let mut agent_count = U256::ZERO;
828        let mut nullifier = U256::ZERO;
829        let mut provider_address = Address::ZERO;
830        let mut is_proof_fresh = false;
831
832        if agent_id > U256::ZERO {
833            is_proof_fresh = registry
834                .isProofFresh(agent_id)
835                .call()
836                .await
837                .map_err(|e| crate::Error::RpcError(e.to_string()))?;
838
839            if self.max_agents_per_human > 0 {
840                nullifier = registry
841                    .getHumanNullifier(agent_id)
842                    .call()
843                    .await
844                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
845                agent_count = registry
846                    .getAgentCountForHuman(nullifier)
847                    .call()
848                    .await
849                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
850            }
851
852            if self.require_self_provider {
853                provider_address = registry
854                    .getProofProvider(agent_id)
855                    .call()
856                    .await
857                    .map_err(|e| crate::Error::RpcError(e.to_string()))?;
858            }
859        }
860
861        self.cache.insert(
862            agent_key,
863            CacheEntry {
864                is_verified,
865                is_proof_fresh,
866                agent_id,
867                agent_count,
868                nullifier,
869                provider_address,
870                expires_at: now + self.cache_ttl_ms,
871            },
872        );
873
874        Ok(OnChainStatus {
875            is_verified,
876            is_proof_fresh,
877            agent_id,
878            agent_count,
879            nullifier,
880            provider_address,
881        })
882    }
883
884    /// Get Self Protocol's own proof provider address from the registry.
885    async fn get_self_provider_address(&mut self) -> Result<Address, crate::Error> {
886        let now = now_millis();
887        if let Some((addr, expires_at)) = self.self_provider_cache {
888            if expires_at > now {
889                return Ok(addr);
890            }
891        }
892
893        let provider = self.make_provider()?;
894        let registry = IAgentRegistry::new(self.registry_address, &provider);
895
896        let address = registry
897            .selfProofProvider()
898            .call()
899            .await
900            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
901
902        // Cache for longer (12x normal TTL — ~1 hour at default)
903        self.self_provider_cache = Some((address, now + self.cache_ttl_ms * 12));
904
905        Ok(address)
906    }
907
908    /// Fetch ZK-attested credentials for an agent.
909    async fn fetch_credentials(&self, agent_id: U256) -> Result<AgentCredentials, crate::Error> {
910        let provider = self.make_provider()?;
911        let registry = IAgentRegistry::new(self.registry_address, &provider);
912
913        let raw = registry
914            .getAgentCredentials(agent_id)
915            .call()
916            .await
917            .map_err(|e| crate::Error::RpcError(e.to_string()))?;
918
919        Ok(AgentCredentials {
920            issuing_state: raw.issuingState,
921            name: raw.name,
922            id_number: raw.idNumber,
923            nationality: raw.nationality,
924            date_of_birth: raw.dateOfBirth,
925            gender: raw.gender,
926            expiry_date: raw.expiryDate,
927            older_than: raw.olderThan,
928            ofac: raw.ofac.to_vec(),
929        })
930    }
931
932    /// Clear the on-chain status cache.
933    pub fn clear_cache(&mut self) {
934        self.cache.clear();
935        self.replay_cache.clear();
936        self.self_provider_cache = None;
937    }
938
939    fn check_and_record_replay(
940        &mut self,
941        signature: &str,
942        message: &str,
943        ts: u64,
944        now: u64,
945    ) -> Option<String> {
946        self.prune_replay_cache(now);
947
948        let key = format!(
949            "{}:{}",
950            signature.to_ascii_lowercase(),
951            message.to_ascii_lowercase()
952        );
953        if let Some(expires_at) = self.replay_cache.get(&key) {
954            if *expires_at > now {
955                return Some("Replay detected".to_string());
956            }
957        }
958
959        self.replay_cache.insert(key, ts.saturating_add(self.max_age_ms));
960        None
961    }
962
963    fn prune_replay_cache(&mut self, now: u64) {
964        self.replay_cache.retain(|_, exp| *exp > now);
965
966        if self.replay_cache.len() <= self.replay_cache_max_entries {
967            return;
968        }
969
970        let overflow = self.replay_cache.len() - self.replay_cache_max_entries;
971        let mut items: Vec<(String, u64)> =
972            self.replay_cache.iter().map(|(k, v)| (k.clone(), *v)).collect();
973        items.sort_by_key(|(_, exp)| *exp);
974
975        for (key, _) in items.into_iter().take(overflow) {
976            self.replay_cache.remove(&key);
977        }
978    }
979}
980
981/// Recover signer address from an EIP-191 personal_sign signature over raw 32 bytes.
982///
983/// Matches TS: `ethers.verifyMessage(ethers.getBytes(message), signature)`
984fn recover_address(message: &B256, signature_hex: &str) -> Result<Address, crate::Error> {
985    let sig_bytes = hex::decode(signature_hex.strip_prefix("0x").unwrap_or(signature_hex))
986        .map_err(|_| crate::Error::InvalidSignature)?;
987
988    let signature = Signature::try_from(sig_bytes.as_slice())
989        .map_err(|_| crate::Error::InvalidSignature)?;
990
991    // EIP-191: prefix with "\x19Ethereum Signed Message:\n32" then hash
992    let prefixed = alloy::primitives::eip191_hash_message(message.as_slice());
993
994    let recovered = signature
995        .recover_address_from_prehash(&prefixed)
996        .map_err(|_| crate::Error::InvalidSignature)?;
997
998    Ok(recovered)
999}
1000
1001fn now_millis() -> u64 {
1002    SystemTime::now()
1003        .duration_since(UNIX_EPOCH)
1004        .expect("system clock before UNIX epoch")
1005        .as_millis() as u64
1006}
1007
1008// ---------------------------------------------------------------------------
1009// Tests
1010// ---------------------------------------------------------------------------
1011
1012#[cfg(test)]
1013mod tests {
1014    use super::*;
1015
1016    #[test]
1017    fn create_build_default() {
1018        let v = SelfAgentVerifier::create().build();
1019        // Defaults: mainnet, max_agents_per_human=1, require_self_provider=true
1020        assert_eq!(v.max_agents_per_human, 1);
1021        assert!(v.require_self_provider);
1022        assert!(v.enable_replay_protection);
1023        assert!(!v.include_credentials);
1024        assert!(v.minimum_age.is_none());
1025        assert!(!v.require_ofac_passed);
1026        assert!(v.allowed_nationalities.is_none());
1027        assert!(v.rate_limiter.is_none());
1028    }
1029
1030    #[test]
1031    fn create_build_testnet() {
1032        let v = SelfAgentVerifier::create()
1033            .network(NetworkName::Testnet)
1034            .build();
1035        let expected = network_config(NetworkName::Testnet);
1036        assert_eq!(v.registry_address, expected.registry_address);
1037        assert_eq!(v.rpc_url, expected.rpc_url);
1038    }
1039
1040    #[test]
1041    fn chain_credentials() {
1042        let v = SelfAgentVerifier::create()
1043            .network(NetworkName::Testnet)
1044            .require_age(18)
1045            .require_ofac()
1046            .require_nationality(&["US", "GB"])
1047            .build();
1048
1049        // Auto-enabled include_credentials
1050        assert!(v.include_credentials);
1051        assert_eq!(v.minimum_age, Some(18));
1052        assert!(v.require_ofac_passed);
1053        assert_eq!(
1054            v.allowed_nationalities.as_deref(),
1055            Some(vec!["US".to_string(), "GB".to_string()].as_slice())
1056        );
1057    }
1058
1059    #[test]
1060    fn auto_enable_credentials_age_only() {
1061        let v = SelfAgentVerifier::create()
1062            .require_age(21)
1063            .build();
1064        assert!(v.include_credentials);
1065        assert_eq!(v.minimum_age, Some(21));
1066    }
1067
1068    #[test]
1069    fn auto_enable_credentials_ofac_only() {
1070        let v = SelfAgentVerifier::create()
1071            .require_ofac()
1072            .build();
1073        assert!(v.include_credentials);
1074        assert!(v.require_ofac_passed);
1075    }
1076
1077    #[test]
1078    fn auto_enable_credentials_nationality_only() {
1079        let v = SelfAgentVerifier::create()
1080            .require_nationality(&["DE"])
1081            .build();
1082        assert!(v.include_credentials);
1083    }
1084
1085    #[test]
1086    fn no_auto_credentials_without_requirements() {
1087        let v = SelfAgentVerifier::create()
1088            .network(NetworkName::Testnet)
1089            .sybil_limit(3)
1090            .build();
1091        assert!(!v.include_credentials);
1092    }
1093
1094    #[test]
1095    fn explicit_include_credentials() {
1096        let v = SelfAgentVerifier::create()
1097            .include_credentials()
1098            .build();
1099        assert!(v.include_credentials);
1100    }
1101
1102    #[test]
1103    fn from_config_works() {
1104        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1105            network: Some(NetworkName::Testnet),
1106            require_age: Some(18),
1107            require_ofac: Some(true),
1108            sybil_limit: Some(1),
1109            ..Default::default()
1110        });
1111        assert!(v.include_credentials);
1112        assert_eq!(v.minimum_age, Some(18));
1113        assert!(v.require_ofac_passed);
1114        assert_eq!(v.max_agents_per_human, 1);
1115    }
1116
1117    #[test]
1118    fn from_config_auto_credentials_disabled() {
1119        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1120            network: Some(NetworkName::Testnet),
1121            sybil_limit: Some(5),
1122            ..Default::default()
1123        });
1124        assert!(!v.include_credentials);
1125    }
1126
1127    #[test]
1128    fn from_config_nationality() {
1129        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1130            require_nationality: Some(vec!["FR".to_string(), "IT".to_string()]),
1131            ..Default::default()
1132        });
1133        assert!(v.include_credentials);
1134        assert_eq!(
1135            v.allowed_nationalities.as_deref(),
1136            Some(vec!["FR".to_string(), "IT".to_string()].as_slice())
1137        );
1138    }
1139
1140    #[test]
1141    fn rate_limit_builder() {
1142        let v = SelfAgentVerifier::create()
1143            .rate_limit(10, 100)
1144            .build();
1145        assert!(v.rate_limiter.is_some());
1146        let limiter = v.rate_limiter.as_ref().unwrap();
1147        assert_eq!(limiter.per_minute, 10);
1148        assert_eq!(limiter.per_hour, 100);
1149    }
1150
1151    #[test]
1152    fn rate_limit_from_config() {
1153        let v = SelfAgentVerifier::from_config(VerifierFromConfig {
1154            rate_limit: Some(RateLimitConfig {
1155                per_minute: Some(5),
1156                per_hour: Some(50),
1157            }),
1158            ..Default::default()
1159        });
1160        assert!(v.rate_limiter.is_some());
1161    }
1162
1163    #[test]
1164    fn rate_limiter_allows_within_limit() {
1165        let config = RateLimitConfig {
1166            per_minute: Some(3),
1167            per_hour: None,
1168        };
1169        let mut limiter = RateLimiter::new(&config);
1170        assert!(limiter.check("0xabc").is_none());
1171        assert!(limiter.check("0xabc").is_none());
1172        assert!(limiter.check("0xabc").is_none());
1173        // 4th request should be rate limited
1174        let result = limiter.check("0xabc");
1175        assert!(result.is_some());
1176        let r = result.unwrap();
1177        assert!(r.error.contains("3/min"));
1178        assert!(r.retry_after_ms > 0);
1179    }
1180
1181    #[test]
1182    fn rate_limiter_separate_agents() {
1183        let config = RateLimitConfig {
1184            per_minute: Some(1),
1185            per_hour: None,
1186        };
1187        let mut limiter = RateLimiter::new(&config);
1188        assert!(limiter.check("0xabc").is_none());
1189        assert!(limiter.check("0xdef").is_none());
1190        // Same agent again = limited
1191        assert!(limiter.check("0xabc").is_some());
1192        // Different agent still allowed
1193        assert!(limiter.check("0xghi").is_none());
1194    }
1195
1196    #[test]
1197    fn builder_custom_max_age_and_cache_ttl() {
1198        let v = SelfAgentVerifier::create()
1199            .max_age(10_000)
1200            .cache_ttl(30_000)
1201            .build();
1202        assert_eq!(v.max_age_ms, 10_000);
1203        assert_eq!(v.cache_ttl_ms, 30_000);
1204    }
1205
1206    #[test]
1207    fn builder_sybil_limit_zero_disables() {
1208        let v = SelfAgentVerifier::create()
1209            .sybil_limit(0)
1210            .build();
1211        assert_eq!(v.max_agents_per_human, 0);
1212    }
1213
1214    #[test]
1215    fn builder_replay_protection() {
1216        let v = SelfAgentVerifier::create()
1217            .replay_protection()
1218            .build();
1219        assert!(v.enable_replay_protection);
1220    }
1221
1222    #[test]
1223    fn builder_require_self_provider() {
1224        let v = SelfAgentVerifier::create()
1225            .require_self_provider()
1226            .build();
1227        assert!(v.require_self_provider);
1228    }
1229
1230    #[test]
1231    fn new_constructor_still_works() {
1232        let v = SelfAgentVerifier::new(VerifierConfig::default());
1233        assert_eq!(v.max_age_ms, DEFAULT_MAX_AGE_MS);
1234        assert_eq!(v.cache_ttl_ms, DEFAULT_CACHE_TTL_MS);
1235        assert_eq!(v.max_agents_per_human, 1);
1236        assert!(v.require_self_provider);
1237    }
1238
1239    #[test]
1240    fn new_constructor_with_credentials() {
1241        let v = SelfAgentVerifier::new(VerifierConfig {
1242            minimum_age: Some(21),
1243            require_ofac_passed: Some(true),
1244            include_credentials: Some(true),
1245            ..Default::default()
1246        });
1247        assert!(v.include_credentials);
1248        assert_eq!(v.minimum_age, Some(21));
1249        assert!(v.require_ofac_passed);
1250    }
1251
1252    #[test]
1253    fn verification_result_has_retry_after() {
1254        let r = VerificationResult::empty_with_error("test");
1255        assert!(r.retry_after_ms.is_none());
1256    }
1257}