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