diff --git a/src/net.cpp b/src/net.cpp --- a/src/net.cpp +++ b/src/net.cpp @@ -3144,7 +3144,7 @@ uint32_t votes = windowInvCounters >> 32; availabilityScore = - AVALANCHE_STATISTICS_DECAY_FACTOR * (2 * votes - polls) + + AVALANCHE_STATISTICS_DECAY_FACTOR * (int64_t(2 * votes - polls) - 1) + (1. - AVALANCHE_STATISTICS_DECAY_FACTOR) * previousScore; } diff --git a/src/test/net_tests.cpp b/src/test/net_tests.cpp --- a/src/test/net_tests.cpp +++ b/src/test/net_tests.cpp @@ -981,6 +981,29 @@ protectedNodes, random_context)); } + // Nodes with a negative avalanche availability score should not be + // protected. + // In the test below nodes with an odd id number have a negative + // score, and others have a positive one, so this test is only + // guaranteed to work with a number of nodes <= 2 * 128. + if (number_of_nodes <= 256) { + std::vector candidates = + GetRandomNodeEvictionCandidates(number_of_nodes, + random_context); + for (NodeEvictionCandidate &candidate : candidates) { + candidate.availabilityScore = + candidate.id % 2 == 0 ? 100. : -1.; + candidate.peerid = PeerId(); + } + + const std::optional evicted_node_id = + SelectNodeToEvict(std::move(candidates)); + + if (evicted_node_id) { + BOOST_CHECK_EQUAL(*evicted_node_id % 2, 1); + } + } + // An eviction is expected given >= 165 random eviction candidates. // The eviction logic protects at most four peers by net group, // eight by lowest ping time, four by last time of novel tx, four by @@ -1027,9 +1050,11 @@ // Check the statistics follow an exponential response for 1 to 10 tau for (size_t i = 1; i <= 10; i++) { for (uint32_t j = 0; j < tau; j += step) { - avastats.invsPolled(1); + // target score = 2 * votes - polls - 1 + // => target score = 1 with votes = polls = 2 + avastats.invsPolled(2); // Always respond to everything correctly - avastats.invsVoted(1); + avastats.invsVoted(2); avastats.updateAvailabilityScore(); @@ -1050,7 +1075,9 @@ for (size_t i = 1; i <= 3; i++) { for (uint32_t j = 0; j < tau; j += step) { - avastats.invsPolled(2); + // target score = 2 * votes - polls - 1 + // => target score = 0 with votes = polls = 1 + avastats.invsPolled(1); // Stop responding to the polls. avastats.invsVoted(1); @@ -1074,4 +1101,37 @@ BOOST_CHECK_LT(previousScore, .05); } +BOOST_AUTO_TEST_CASE(avalanche_statistics_no_poll) { + const uint32_t step = AVALANCHE_STATISTICS_REFRESH_PERIOD.count(); + const uint32_t tau = AVALANCHE_STATISTICS_TIME_CONSTANT.count(); + + CNode::AvalancheState avastats; + + double previousScore = avastats.getAvailabilityScore(); + BOOST_CHECK_SMALL(previousScore, 1e-6); + + // Check the statistics follow an exponential response for 1 to 10 tau + for (size_t i = 1; i <= 10; i++) { + for (uint32_t j = 0; j < tau; j += step) { + // target score = 2 * votes - polls - 1 + // => target score = -1 with votes = polls = 0 + avastats.updateAvailabilityScore(); + + // Expect a monotonic faill + double currentScore = avastats.getAvailabilityScore(); + BOOST_CHECK_LT(currentScore, 0.); + BOOST_CHECK_LE(currentScore, previousScore); + previousScore = currentScore; + } + + // We expect -(1 - e^-i) after i * tau. The tolerance is expressed + // as a percentage, and we add a (large) 0.1% margin to account for + // floating point errors. + BOOST_CHECK_CLOSE(previousScore, std::expm1(-1. * i), 100.1 / tau); + } + + // After 10 tau we should be very close to -1 (about 99.995%) + BOOST_CHECK_CLOSE(previousScore, -1., 0.01); +} + BOOST_AUTO_TEST_SUITE_END()