Files
rippled/src/tests/libxrpl/telemetry/ValidationTracker.cpp

526 lines
20 KiB
C++

/**
* @file ValidationTracker.cpp
* Unit tests for xrpl::telemetry::ValidationTracker.
*/
#include <xrpld/telemetry/ValidationTracker.h>
#include <xrpl/basics/base_uint.h>
#include <xrpl/protocol/Protocol.h>
#include <gtest/gtest.h>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <thread>
using namespace xrpl;
using namespace xrpl::telemetry;
/**
* Helper to create a unique uint256 from an integer seed.
*/
static uint256
makeHash(std::uint64_t n)
{
return uint256(n);
}
/**
* Test fixture providing a fresh ValidationTracker per test.
*/
class ValidationTrackerTest : public ::testing::Test
{
protected:
ValidationTracker tracker_;
};
// ---------------------------------------------------------------
// 1. Normal agreement
// Record both our validation and network validation for the
// same hash, then reconcile after the grace period elapses.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, NormalAgreement)
{
auto const hash = makeHash(1);
LedgerIndex const seq = 100;
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Immediately after recording, nothing is reconciled yet
// (grace period has not elapsed).
tracker_.reconcile();
EXPECT_EQ(tracker_.totalValidationsSent(), 1u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
// Wait for the grace period (8 seconds) to elapse, then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 1u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 100.0);
}
// ---------------------------------------------------------------
// 2. Missed validation
// Only the network validates; we never do. After grace period
// the event should be reconciled as a miss.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MissedValidation)
{
auto const hash = makeHash(2);
LedgerIndex const seq = 200;
tracker_.recordNetworkValidation(hash, seq);
// Wait for grace period then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 1u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
}
// ---------------------------------------------------------------
// 3. Late repair
// Network validates first, grace period elapses (miss), then
// our validation arrives within the 5-minute repair window and
// the miss is flipped to an agreement.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, LateRepair)
{
auto const hash = makeHash(3);
LedgerIndex const seq = 300;
// Network validates, but we do not (yet).
tracker_.recordNetworkValidation(hash, seq);
// Grace period elapses -- reconciled as a miss.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.missed1h(), 1u);
// Late arrival of our validation (within repair window).
tracker_.recordOurValidation(hash, seq);
tracker_.reconcile();
// Miss should be repaired to agreement.
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 1u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 100.0);
}
// ---------------------------------------------------------------
// 4. Empty window returns 0%
// When no events have been recorded the percentage methods
// must return 0.0, not NaN or any other value.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, EmptyWindowReturnsZero)
{
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
EXPECT_DOUBLE_EQ(tracker_.agreementPct24h(), 0.0);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 0u);
EXPECT_EQ(tracker_.agreements24h(), 0u);
EXPECT_EQ(tracker_.missed24h(), 0u);
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.totalAgreementsEver(), 0u);
EXPECT_EQ(tracker_.totalMissedEver(), 0u);
EXPECT_EQ(tracker_.totalValidationsSent(), 0u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 0u);
}
// ---------------------------------------------------------------
// 5. Grace period boundary
// Events recorded less than 8 seconds ago must NOT be
// reconciled. Verify that an immediate reconcile is a no-op.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, GracePeriodBoundary)
{
auto const hash = makeHash(5);
LedgerIndex const seq = 500;
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Reconcile immediately -- grace period has not elapsed.
tracker_.reconcile();
// Nothing should be reconciled yet.
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.agreements1h(), 0u);
EXPECT_EQ(tracker_.missed1h(), 0u);
// Lifetime send/check counters should still be incremented.
EXPECT_EQ(tracker_.totalValidationsSent(), 1u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
}
// ---------------------------------------------------------------
// 6. Max pending events -- trimming
// Add more than kMaxPendingEvents (1000) events. After
// reconciliation and a second reconcile pass the pending map
// should be trimmed. Lifetime totals must remain consistent.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MaxPendingEventsTrimming)
{
constexpr std::size_t kCount = 1100;
for (std::size_t i = 0; i < kCount; ++i)
{
auto const hash = makeHash(i + 1);
auto const seq = static_cast<LedgerIndex>(i + 1);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
}
EXPECT_EQ(tracker_.totalValidationsSent(), kCount);
EXPECT_EQ(tracker_.totalValidationsChecked(), kCount);
// Wait for grace period so all events can be reconciled.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// All events should be reconciled as agreements.
EXPECT_EQ(tracker_.totalAgreements(), kCount);
EXPECT_EQ(tracker_.totalMissed(), 0u);
// Reconcile again to trigger pending eviction / trimming.
// The pending map should be trimmed, but totals remain correct.
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), kCount);
EXPECT_EQ(tracker_.totalMissed(), 0u);
}
// ---------------------------------------------------------------
// 7. Multiple distinct ledgers -- mixed results
// Record a mix of agreements and misses to verify that window
// counts and percentages are computed correctly.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, MixedAgreementsAndMisses)
{
// 3 agreements: both sides validate.
for (int i = 1; i <= 3; ++i)
{
auto const hash = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordOurValidation(hash, static_cast<LedgerIndex>(i));
tracker_.recordNetworkValidation(hash, static_cast<LedgerIndex>(i));
}
// 2 misses: only network validates.
for (int i = 4; i <= 5; ++i)
{
auto const hash = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordNetworkValidation(hash, static_cast<LedgerIndex>(i));
}
// Wait for grace period then reconcile.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 3u);
EXPECT_EQ(tracker_.totalMissed(), 2u);
EXPECT_EQ(tracker_.agreements1h(), 3u);
EXPECT_EQ(tracker_.missed1h(), 2u);
// 3 out of 5 = 60%
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 60.0);
}
// ---------------------------------------------------------------
// 8. Duplicate recording for same hash
// Recording the same hash multiple times should not create
// duplicate pending entries or double-count totals beyond the
// per-call increments.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, DuplicateRecordingSameHash)
{
auto const hash = makeHash(42);
LedgerIndex const seq = 42;
// Record our validation twice for the same hash.
tracker_.recordOurValidation(hash, seq);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
// Each call increments the lifetime counter.
EXPECT_EQ(tracker_.totalValidationsSent(), 2u);
EXPECT_EQ(tracker_.totalValidationsChecked(), 1u);
// But only one pending event exists, so only one agreement.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 1u);
EXPECT_EQ(tracker_.totalMissed(), 0u);
}
// ---------------------------------------------------------------
// 9. Only-we-validated scenario
// We validate but the network does not. After grace period
// this should be a miss (not an agreement).
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, OnlyWeValidated)
{
auto const hash = makeHash(99);
LedgerIndex const seq = 99;
tracker_.recordOurValidation(hash, seq);
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.missed1h(), 1u);
EXPECT_DOUBLE_EQ(tracker_.agreementPct1h(), 0.0);
}
// ---------------------------------------------------------------
// 10. A counted ledger is never counted twice
// reconcile() drops the oldest reconciled events once the
// pending map passes kMaxPendingEvents. A validation arriving
// for one of those ledgers afterwards must not reach the
// agreement or missed totals a second time.
//
// Two hashes are made the oldest so the trim drops both:
// - evictedMiss (network only) reconciles as a miss. Our late
// validation cannot repair an entry the trim dropped, so
// totalMissed staying at 1 proves the trim really dropped
// it. A fixture where the trim did not run would repair it
// and report 0 misses.
// - evictedAgreed (both sides) reconciles as an agreement.
// Re-recording both sides is what double-counts an
// agreement.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, CountedLedgerNotCountedTwice)
{
// The trim drops the oldest reconciled entries first. Each pause makes
// the next record time strictly larger, so these two are the oldest.
auto const evictedMiss = makeHash(1);
tracker_.recordNetworkValidation(evictedMiss, 1);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
auto const evictedAgreed = makeHash(2);
tracker_.recordOurValidation(evictedAgreed, 2);
tracker_.recordNetworkValidation(evictedAgreed, 2);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
// Fill to three over the bound so the trim drops three entries: the two
// above plus one filler.
constexpr std::size_t kFill = ValidationTracker::kMaxPendingEvents + 1;
for (std::size_t i = 0; i < kFill; ++i)
{
auto const hash = makeHash(i + 3);
auto const seq = static_cast<LedgerIndex>(i + 3);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
}
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// Every filler plus evictedAgreed agrees; evictedMiss is the one miss.
EXPECT_EQ(tracker_.totalAgreements(), kFill + 1);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.agreements1h(), kFill + 1);
EXPECT_EQ(tracker_.missed1h(), 1u);
// Validations arrive again for the two dropped ledgers.
tracker_.recordOurValidation(evictedMiss, 1);
tracker_.recordOurValidation(evictedAgreed, 2);
tracker_.recordNetworkValidation(evictedAgreed, 2);
// Long enough for a re-created pending entry to pass the grace period.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// Both ledgers were already counted, so every total is unchanged.
EXPECT_EQ(tracker_.totalAgreements(), kFill + 1);
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.agreements1h(), kFill + 1);
EXPECT_EQ(tracker_.missed1h(), 1u);
// The send and check counters count messages, not ledgers, so the
// repeated validations do count towards them.
EXPECT_EQ(tracker_.totalValidationsSent(), kFill + 3);
EXPECT_EQ(tracker_.totalValidationsChecked(), kFill + 3);
}
// ---------------------------------------------------------------
// 10. Gross miss tally is monotonic across a late repair
// The gross lifetime tallies (totalAgreementsEver/totalMissedEver)
// back the monotonic Prometheus _total counters. A late repair must
// move the NET totals (miss -> agreement) but must NOT move the gross
// tallies: a miss already counted stays counted, and the repair does
// not add a second (agreement) count for the same ledger.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, GrossMissedNeverDecrementsOnRepair)
{
auto const hash = makeHash(10);
LedgerIndex const seq = 1000;
// Network validates, we do not (yet).
tracker_.recordNetworkValidation(hash, seq);
// Grace period elapses -- reconciled as a miss.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// Net and gross both show exactly one initial miss, zero agreements.
EXPECT_EQ(tracker_.totalMissed(), 1u);
EXPECT_EQ(tracker_.totalMissedEver(), 1u);
EXPECT_EQ(tracker_.totalAgreements(), 0u);
EXPECT_EQ(tracker_.totalAgreementsEver(), 0u);
// Late arrival of our validation repairs the miss to an agreement.
tracker_.recordOurValidation(hash, seq);
tracker_.reconcile();
// Net totals reflect the repair...
EXPECT_EQ(tracker_.totalMissed(), 0u);
EXPECT_EQ(tracker_.totalAgreements(), 1u);
// ...but the gross tallies are frozen at first classification: the miss
// stays counted and no agreement was added (repair path excluded).
EXPECT_EQ(tracker_.totalMissedEver(), 1u);
EXPECT_EQ(tracker_.totalAgreementsEver(), 0u);
}
// ---------------------------------------------------------------
// 11. Gross tallies count initial classification only (additive)
// With a mix of initial agreements and misses the gross tallies equal
// the net totals. A subsequent repair shifts the net totals but leaves
// the gross tallies unchanged, and the gross sum equals the number of
// reconciled ledgers (the additive invariant the _total counters rely on).
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, GrossAgreementsCountInitialOnly)
{
// 3 initial agreements: both sides validate.
for (int i = 1; i <= 3; ++i)
{
auto const h = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordOurValidation(h, static_cast<LedgerIndex>(i));
tracker_.recordNetworkValidation(h, static_cast<LedgerIndex>(i));
}
// 2 initial misses: only network validates.
for (int i = 4; i <= 5; ++i)
{
auto const h = makeHash(static_cast<std::uint64_t>(i));
tracker_.recordNetworkValidation(h, static_cast<LedgerIndex>(i));
}
// Grace period elapses -- all five reconciled at first classification.
std::this_thread::sleep_for(std::chrono::seconds(9));
tracker_.reconcile();
// Before any repair, gross equals net.
EXPECT_EQ(tracker_.totalAgreements(), 3u);
EXPECT_EQ(tracker_.totalAgreementsEver(), 3u);
EXPECT_EQ(tracker_.totalMissed(), 2u);
EXPECT_EQ(tracker_.totalMissedEver(), 2u);
// Repair one of the misses (hash 4) within the repair window.
tracker_.recordOurValidation(makeHash(4), 4);
tracker_.reconcile();
// Net totals shift by the repair...
EXPECT_EQ(tracker_.totalAgreements(), 4u);
EXPECT_EQ(tracker_.totalMissed(), 1u);
// ...gross tallies stay at the initial classification.
EXPECT_EQ(tracker_.totalAgreementsEver(), 3u);
EXPECT_EQ(tracker_.totalMissedEver(), 2u);
// Additive invariant: gross agree + gross miss == ledgers reconciled.
EXPECT_EQ(tracker_.totalAgreementsEver() + tracker_.totalMissedEver(), 5u);
}
// ---------------------------------------------------------------
// 12. Pending map stays bounded when nothing ever reconciles
// reconcile() is the only pruning path, and it runs only from
// the observable-gauge callbacks -- which need telemetry both
// compiled in and enabled. A node with telemetry off, or with
// [telemetry] enabled=0, therefore never reconciles, so the
// record methods have to bound the map themselves.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, PendingStaysBoundedWithoutReconcile)
{
constexpr std::uint64_t kFirstBatch = 4000;
constexpr std::uint64_t kSecondBatch = 8000;
for (std::uint64_t i = 0; i < kFirstBatch; ++i)
tracker_.recordOurValidation(makeHash(i), static_cast<LedgerIndex>(i));
auto const afterFirst = tracker_.pendingCount();
for (std::uint64_t i = kFirstBatch; i < kSecondBatch; ++i)
tracker_.recordOurValidation(makeHash(i), static_cast<LedgerIndex>(i));
auto const afterSecond = tracker_.pendingCount();
// Bounded at all: far fewer entries retained than recorded.
EXPECT_LT(afterFirst, kFirstBatch);
// Bounded at a fixed cap, not merely growing more slowly: doubling the
// input leaves the size unchanged. Asserted without naming the private
// constant, so the test survives a change to its value.
EXPECT_EQ(afterFirst, afterSecond);
// Every recorded validation is still counted, so bounding the map does not
// cost us the lifetime totals the gauges report.
EXPECT_EQ(tracker_.totalValidationsSent(), kSecondBatch);
}
// ---------------------------------------------------------------
// Bounding the map must not lose a ledger from the verdict totals.
//
// totalAgreements() and totalMissed() move only when an event is
// classified, which normally happens in reconcile() after the grace
// period. An event dropped to hold the bound therefore has to be
// classified on the way out, or the ledger is counted as neither an
// agreement nor a miss and both totals silently under-report.
//
// Recorded with both validations present and no reconcile() call, so
// every classification here comes from the eviction path alone.
// ---------------------------------------------------------------
TEST_F(ValidationTrackerTest, EvictionUnderPressureStillCountsEveryLedger)
{
// Comfortably past the cap, so eviction runs many times.
constexpr std::uint64_t kCount = 3000;
for (std::uint64_t i = 0; i < kCount; ++i)
{
auto const hash = makeHash(i + 1);
auto const seq = static_cast<LedgerIndex>(i + 1);
tracker_.recordOurValidation(hash, seq);
tracker_.recordNetworkValidation(hash, seq);
}
// Held at the cap, so evictions definitely happened.
auto const stillPending = tracker_.pendingCount();
EXPECT_LT(stillPending, kCount);
// Every evicted ledger reached a verdict, and each had both validations, so
// every one of them is an agreement rather than a miss.
auto const evicted = kCount - stillPending;
EXPECT_EQ(tracker_.totalAgreements(), evicted);
EXPECT_EQ(tracker_.totalMissed(), 0u);
// Nothing is counted twice: the ledgers still pending have no verdict yet.
EXPECT_EQ(tracker_.totalAgreements() + tracker_.totalMissed(), evicted);
}