Skip to content

Commit 03d94db

Browse files
committed
fix: review fixes -- validate signatures before RPC, fix inline paths
- Move ML-DSA-65 signature validation BEFORE on-chain queries to prevent DoS via garbage proofs triggering expensive RPC lookups - Move verify_merkle_candidate_signature import to top-level - Import Arc at top of file instead of inline std::sync::Arc
1 parent 0d10703 commit 03d94db

1 file changed

Lines changed: 15 additions & 14 deletions

File tree

src/payment/verifier.rs

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ use crate::payment::proof::{
1010
deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType,
1111
};
1212
use crate::payment::quote::{verify_quote_content, verify_quote_signature};
13+
use crate::payment::verify_merkle_candidate_signature;
1314
use evmlib::contract::merkle_payment_vault;
1415
use evmlib::merkle_batch_payment::PoolHash;
1516
use evmlib::merkle_payments::OnChainPaymentInfo;
@@ -20,6 +21,7 @@ use lru::LruCache;
2021
use parking_lot::Mutex;
2122
use saorsa_core::identity::node_identity::peer_id_from_public_key_bytes;
2223
use std::num::NonZeroUsize;
24+
use std::sync::Arc;
2325
use std::time::SystemTime;
2426
use 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

Comments
 (0)