@@ -484,6 +484,17 @@ impl PaymentVerifier {
484484
485485 let pool_hash = merkle_proof. winner_pool_hash ( ) ;
486486
487+ // Run cheap local checks BEFORE expensive on-chain queries.
488+ // This prevents DoS via garbage proofs that trigger RPC lookups.
489+ for candidate in & merkle_proof. winner_pool . candidate_nodes {
490+ if !crate :: payment:: verify_merkle_candidate_signature ( candidate) {
491+ return Err ( Error :: Payment ( format ! (
492+ "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})" ,
493+ candidate. reward_address
494+ ) ) ) ;
495+ }
496+ }
497+
487498 // Check pool cache first
488499 let cached_info = {
489500 let mut pool_cache = self . pool_cache . lock ( ) ;
@@ -534,20 +545,8 @@ impl PaymentVerifier {
534545 on_chain_info
535546 } ;
536547
537- // pool_hash was derived from merkle_proof.winner_pool and used to query
538- // the contract. The contract only returns data if a payment exists for that
539- // hash. The ML-DSA signature check below ensures the pool contents are
540- // authentic (nodes actually signed their candidate quotes).
541-
542- // Verify ML-DSA-65 signatures and timestamp/data_type consistency
543- // on all candidate nodes in the winner pool.
548+ // Verify timestamp consistency (signatures already checked above before RPC).
544549 for candidate in & merkle_proof. winner_pool . candidate_nodes {
545- if !crate :: payment:: verify_merkle_candidate_signature ( candidate) {
546- return Err ( Error :: Payment ( format ! (
547- "Invalid ML-DSA-65 signature on merkle candidate node (reward: {})" ,
548- candidate. reward_address
549- ) ) ) ;
550- }
551550 if candidate. merkle_payment_timestamp != payment_info. merkle_payment_timestamp {
552551 return Err ( Error :: Payment ( format ! (
553552 "Candidate timestamp mismatch: expected {}, got {} (reward: {})" ,
0 commit comments