@@ -752,6 +752,9 @@ impl PaymentVerifier {
752752#[ allow( clippy:: expect_used) ]
753753mod 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}
0 commit comments