Skip to content

Commit 133f666

Browse files
mickvandijkeclaude
andcommitted
test: add close-group membership validation tests and handle set_p2p_node errors
Address Copilot review feedback: add 4 unit tests covering the accept, reject, skip, and local-peer-known paths of validate_close_group_membership, and handle set_p2p_node errors explicitly in devnet (warn) and testnet (propagate as TestnetError::Startup). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f0cabbf commit 133f666

3 files changed

Lines changed: 172 additions & 2 deletions

File tree

src/devnet.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -646,7 +646,9 @@ impl Devnet {
646646

647647
if let (Some(ref p2p), Some(ref protocol)) = (&node.p2p_node, &node.ant_protocol) {
648648
// Inject P2P node into protocol handler for close-group lookups.
649-
let _ = protocol.set_p2p_node(Arc::clone(p2p));
649+
if protocol.set_p2p_node(Arc::clone(p2p)).is_err() {
650+
warn!("P2P node already set on protocol handler for devnet node {index}");
651+
}
650652
let mut events = p2p.subscribe_events();
651653
let p2p_clone = Arc::clone(p2p);
652654
let protocol_clone = Arc::clone(protocol);

src/payment/verifier.rs

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -752,6 +752,9 @@ impl PaymentVerifier {
752752
#[allow(clippy::expect_used)]
753753
mod tests {
754754
use super::*;
755+
use ant_evm::EncodedPeerId;
756+
use saorsa_core::MlDsa65;
757+
use saorsa_pqc::pqc::MlDsaOperations;
755758

756759
/// Create a verifier for unit tests. EVM is always on, but tests can
757760
/// pre-populate the cache to bypass on-chain verification.
@@ -2026,4 +2029,164 @@ mod tests {
20262029
"Error should mention depth/count mismatch: {err_msg}"
20272030
);
20282031
}
2032+
2033+
// =========================================================================
2034+
// Close-group membership validation tests
2035+
// =========================================================================
2036+
2037+
#[test]
2038+
fn test_close_group_all_peers_recognised_accepted() {
2039+
let ml_dsa = MlDsa65::new();
2040+
let mut peer_quotes = Vec::new();
2041+
let mut close_group_ids: Vec<[u8; 32]> = Vec::new();
2042+
2043+
// Generate CLOSE_GROUP_SIZE peers with real ML-DSA keys.
2044+
for _ in 0..CLOSE_GROUP_SIZE {
2045+
let (public_key, _) = ml_dsa.generate_keypair().expect("keygen");
2046+
let pub_key_bytes = public_key.as_bytes().to_vec();
2047+
let ant_peer_id =
2048+
peer_id_from_public_key_bytes(&pub_key_bytes).expect("peer id from pub key");
2049+
close_group_ids.push(*ant_peer_id.as_bytes());
2050+
2051+
let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
2052+
let mut quote = make_fake_quote(
2053+
[0xAA; 32],
2054+
SystemTime::now(),
2055+
RewardsAddress::new([1u8; 20]),
2056+
);
2057+
quote.pub_key = pub_key_bytes;
2058+
peer_quotes.push((encoded, quote));
2059+
}
2060+
2061+
let payment = ProofOfPayment { peer_quotes };
2062+
2063+
// Verifier whose local_peer_id is NOT one of the proof peers (but that's
2064+
// fine — it only needs to be in the known set, and we insert it).
2065+
let config = PaymentVerifierConfig {
2066+
evm: EvmVerifierConfig::default(),
2067+
cache_capacity: 100,
2068+
local_rewards_address: RewardsAddress::new([1u8; 20]),
2069+
local_peer_id: [0xBBu8; 32],
2070+
};
2071+
let verifier = PaymentVerifier::new(config);
2072+
2073+
let result = verifier.validate_close_group_membership(&payment, &close_group_ids);
2074+
assert!(
2075+
result.is_ok(),
2076+
"All proof peers are in close group — should accept: {result:?}"
2077+
);
2078+
}
2079+
2080+
#[test]
2081+
fn test_close_group_unknown_peer_rejected() {
2082+
let ml_dsa = MlDsa65::new();
2083+
let mut peer_quotes = Vec::new();
2084+
let mut close_group_ids: Vec<[u8; 32]> = Vec::new();
2085+
2086+
// Generate CLOSE_GROUP_SIZE peers; include all but the last in the
2087+
// close group so one peer is "unknown".
2088+
for i in 0..CLOSE_GROUP_SIZE {
2089+
let (public_key, _) = ml_dsa.generate_keypair().expect("keygen");
2090+
let pub_key_bytes = public_key.as_bytes().to_vec();
2091+
let ant_peer_id =
2092+
peer_id_from_public_key_bytes(&pub_key_bytes).expect("peer id from pub key");
2093+
2094+
// Only add the first N-1 peers to the close group.
2095+
if i < CLOSE_GROUP_SIZE - 1 {
2096+
close_group_ids.push(*ant_peer_id.as_bytes());
2097+
}
2098+
2099+
let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
2100+
let mut quote = make_fake_quote(
2101+
[0xAA; 32],
2102+
SystemTime::now(),
2103+
RewardsAddress::new([1u8; 20]),
2104+
);
2105+
quote.pub_key = pub_key_bytes;
2106+
peer_quotes.push((encoded, quote));
2107+
}
2108+
2109+
let payment = ProofOfPayment { peer_quotes };
2110+
2111+
let config = PaymentVerifierConfig {
2112+
evm: EvmVerifierConfig::default(),
2113+
cache_capacity: 100,
2114+
local_rewards_address: RewardsAddress::new([1u8; 20]),
2115+
local_peer_id: [0xBBu8; 32],
2116+
};
2117+
let verifier = PaymentVerifier::new(config);
2118+
2119+
let result = verifier.validate_close_group_membership(&payment, &close_group_ids);
2120+
assert!(result.is_err(), "One unknown peer — should reject");
2121+
let err_msg = format!("{}", result.expect_err("should fail"));
2122+
assert!(
2123+
err_msg.contains("not in the current close group"),
2124+
"Error should mention close group: {err_msg}"
2125+
);
2126+
}
2127+
2128+
#[test]
2129+
fn test_close_group_empty_skips_validation() {
2130+
// With an empty close group (unit test / no DHT), validation is skipped.
2131+
let verifier = create_test_verifier();
2132+
2133+
let quote = make_fake_quote(
2134+
[0xAA; 32],
2135+
SystemTime::now(),
2136+
RewardsAddress::new([1u8; 20]),
2137+
);
2138+
let keypair = libp2p::identity::Keypair::generate_ed25519();
2139+
let peer_id = libp2p::PeerId::from_public_key(&keypair.public());
2140+
let peer_quotes = vec![(EncodedPeerId::from(peer_id), quote)];
2141+
2142+
let payment = ProofOfPayment { peer_quotes };
2143+
2144+
let result = verifier.validate_close_group_membership(&payment, &[]);
2145+
assert!(
2146+
result.is_ok(),
2147+
"Empty close group should skip validation: {result:?}"
2148+
);
2149+
}
2150+
2151+
#[test]
2152+
fn test_close_group_local_peer_is_implicitly_known() {
2153+
let ml_dsa = MlDsa65::new();
2154+
2155+
// Generate a single peer whose BLAKE3 ID we'll set as local_peer_id.
2156+
let (public_key, _) = ml_dsa.generate_keypair().expect("keygen");
2157+
let pub_key_bytes = public_key.as_bytes().to_vec();
2158+
let ant_peer_id =
2159+
peer_id_from_public_key_bytes(&pub_key_bytes).expect("peer id from pub key");
2160+
2161+
let encoded = encoded_peer_id_for_pub_key(&pub_key_bytes);
2162+
let mut quote = make_fake_quote(
2163+
[0xAA; 32],
2164+
SystemTime::now(),
2165+
RewardsAddress::new([1u8; 20]),
2166+
);
2167+
quote.pub_key = pub_key_bytes;
2168+
2169+
let payment = ProofOfPayment {
2170+
peer_quotes: vec![(encoded, quote)],
2171+
};
2172+
2173+
// The local_peer_id matches the proof peer, and the close group
2174+
// contains at least one entry (so validation isn't skipped) but
2175+
// does NOT contain the proof peer — only local_peer_id does.
2176+
let config = PaymentVerifierConfig {
2177+
evm: EvmVerifierConfig::default(),
2178+
cache_capacity: 100,
2179+
local_rewards_address: RewardsAddress::new([1u8; 20]),
2180+
local_peer_id: *ant_peer_id.as_bytes(),
2181+
};
2182+
let verifier = PaymentVerifier::new(config);
2183+
2184+
// Close group has a dummy entry so validation isn't skipped.
2185+
let dummy_peer = [0xFFu8; 32];
2186+
let result = verifier.validate_close_group_membership(&payment, &[dummy_peer]);
2187+
assert!(
2188+
result.is_ok(),
2189+
"Proof peer matches local_peer_id — should accept: {result:?}"
2190+
);
2191+
}
20292192
}

tests/e2e/testnet.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1156,7 +1156,12 @@ impl TestNetwork {
11561156
// Start protocol handler that routes incoming P2P messages to AntProtocol
11571157
if let (Some(ref p2p), Some(ref protocol)) = (&node.p2p_node, &node.ant_protocol) {
11581158
// Inject P2P node into protocol handler for close-group lookups.
1159-
let _ = protocol.set_p2p_node(Arc::clone(p2p));
1159+
protocol.set_p2p_node(Arc::clone(p2p)).map_err(|_| {
1160+
TestnetError::Startup(format!(
1161+
"P2P node already set on protocol handler for node {}",
1162+
node.index,
1163+
))
1164+
})?;
11601165
let mut events = p2p.subscribe_events();
11611166
let p2p_clone = Arc::clone(p2p);
11621167
let protocol_clone = Arc::clone(protocol);

0 commit comments

Comments
 (0)