@@ -10,6 +10,7 @@ use crate::payment::proof::{
1010 deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType ,
1111} ;
1212use crate :: payment:: quote:: { verify_quote_content, verify_quote_signature} ;
13+ use crate :: payment:: verify_merkle_candidate_signature;
1314use evmlib:: contract:: merkle_payment_vault;
1415use evmlib:: merkle_batch_payment:: PoolHash ;
1516use evmlib:: merkle_payments:: OnChainPaymentInfo ;
@@ -20,6 +21,7 @@ use lru::LruCache;
2021use parking_lot:: Mutex ;
2122use saorsa_core:: identity:: node_identity:: peer_id_from_public_key_bytes;
2223use std:: num:: NonZeroUsize ;
24+ use std:: sync:: Arc ;
2325use std:: time:: SystemTime ;
2426use tracing:: { debug, info} ;
2527
@@ -66,7 +68,7 @@ impl Default for EvmVerifierConfig {
6668/// Configuration for the payment verifier.
6769///
6870/// Callback to check if the local node is in the close group for a given address.
69- pub type CloseGroupChecker = std :: sync :: Arc < dyn Fn ( & [ u8 ; 32 ] ) -> Vec < RewardsAddress > + Send + Sync > ;
71+ pub type CloseGroupChecker = Arc < dyn Fn ( & [ u8 ; 32 ] ) -> Vec < RewardsAddress > + Send + Sync > ;
7072
7173/// All new data requires EVM payment on Arbitrum. The cache stores
7274/// previously verified payments to avoid redundant on-chain lookups.
@@ -544,6 +546,17 @@ impl PaymentVerifier {
544546
545547 let pool_hash = merkle_proof. winner_pool_hash ( ) ;
546548
549+ // Run cheap local checks BEFORE expensive on-chain queries.
550+ // This prevents DoS via garbage proofs that trigger RPC lookups.
551+ for candidate in & merkle_proof. winner_pool . candidate_nodes {
552+ if !verify_merkle_candidate_signature ( candidate) {
553+ return Err ( Error :: Payment ( format ! (
554+ "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})" ,
555+ candidate. reward_address
556+ ) ) ) ;
557+ }
558+ }
559+
547560 // Check pool cache first
548561 let cached_info = {
549562 let mut pool_cache = self . pool_cache . lock ( ) ;
@@ -619,20 +632,8 @@ impl PaymentVerifier {
619632 on_chain_info
620633 } ;
621634
622- // pool_hash was derived from merkle_proof.winner_pool and used to query
623- // the contract. The contract only returns data if a payment exists for that
624- // hash. The ML-DSA signature check below ensures the pool contents are
625- // authentic (nodes actually signed their candidate quotes).
626-
627- // Verify ML-DSA-65 signatures and timestamp/data_type consistency
628- // on all candidate nodes in the winner pool.
635+ // Verify timestamp consistency (signatures already checked above before RPC).
629636 for candidate in & merkle_proof. winner_pool . candidate_nodes {
630- if !crate :: payment:: verify_merkle_candidate_signature ( candidate) {
631- return Err ( Error :: Payment ( format ! (
632- "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})" ,
633- candidate. reward_address
634- ) ) ) ;
635- }
636637 if candidate. merkle_payment_timestamp != payment_info. merkle_payment_timestamp {
637638 return Err ( Error :: Payment ( format ! (
638639 "Candidate timestamp mismatch: expected {}, got {} (reward: {})" ,
0 commit comments