diff --git a/src/payment/verifier.rs b/src/payment/verifier.rs index 3d0c944..e701545 100644 --- a/src/payment/verifier.rs +++ b/src/payment/verifier.rs @@ -11,7 +11,6 @@ use crate::payment::pricing::{calculate_price, derive_records_stored_from_price} use crate::payment::proof::{ deserialize_merkle_proof, deserialize_proof, detect_proof_type, ProofType, }; -use crate::replication::config::storage_admission_width; use crate::storage::lmdb::LmdbStorage; use ant_protocol::payment::verify::{verify_quote_content, verify_quote_signature}; use evmlib::common::{Amount, QuoteHash}; @@ -920,27 +919,24 @@ impl PaymentVerifier { } }; - // Verify against the close group PLUS the storage-admission margin - // rather than the bare configured close-group width. The check compares - // the client's network-derived quote set against this node's *local* - // routing-table view, which on a real (young / large / NAT-heavy) - // network does not perfectly reflect the global K-closest: a peer the - // client legitimately quoted routinely ranks just outside this node's - // local close-group window. Bare `close_group_size` (no margin) falsely - // rejects those honest payments — the same divergence the sibling - // receiver admission check absorbs via `storage_admission_width` - // (PR #137); apply the same margin here for symmetry. - let admission_width = storage_admission_width(self.config.close_group_size); + // Closeness *verification* must mirror the uploader's pure XOR-distance + // peer selection. `find_closest_nodes_local_with_self` reranks the local + // routing table by reachability (preferring directly-reachable peers, + // XOR only as a tiebreaker), which demotes an XOR-close relay-only / + // NAT'd peer out of the compared window and falsely rejects an honest + // payment that legitimately quoted that peer. Use the XOR-only sibling + // so this check matches how the client chose the quoted close group. + let close_group_size = self.config.close_group_size; let closest = p2p_node .dht_manager() - .find_closest_nodes_local_with_self(xorname, admission_width) + .find_closest_nodes_local_by_distance_with_self(xorname, close_group_size) .await; if closest.iter().any(|node| node.peer_id == *issuer_peer_id) { return Ok(()); } Err(Error::Payment(format!( - "Paid quote issuer {} is not among this node's local {admission_width} closest peers for {}", + "Paid quote issuer {} is not among this node's local {close_group_size} closest peers for {}", issuer_peer_id.to_hex(), hex::encode(xorname) ))) diff --git a/src/storage/handler.rs b/src/storage/handler.rs index d51d717..dc7c690 100644 --- a/src/storage/handler.rs +++ b/src/storage/handler.rs @@ -413,9 +413,17 @@ impl AntProtocol { let self_id = *p2p_node.peer_id(); let admission_width = storage_admission_width(self.payment_verifier.close_group_size()); + // Storage-responsibility *verification* must mirror the uploader's pure + // XOR-distance peer selection. `find_closest_nodes_local_with_self` + // reranks the local routing table by reachability (preferring + // directly-reachable peers, XOR only as a tiebreaker), which demotes + // this node out of the compared window when it is an XOR-close + // relay-only / NAT'd peer and falsely rejects an honest PUT it is + // legitimately responsible for. Use the XOR-only sibling so the local + // admission check matches how the client chose the close group. let closest = p2p_node .dht_manager() - .find_closest_nodes_local_with_self(address, admission_width) + .find_closest_nodes_local_by_distance_with_self(address, admission_width) .await; if closest.iter().any(|node| node.peer_id == self_id) { return Ok(());