From 726dde697e344ede86cbbcae272a40ec77cedec8 Mon Sep 17 00:00:00 2001 From: Nicholas Dudfield Date: Sun, 12 Jul 2026 09:27:34 +0700 Subject: [PATCH] test(consensus): cover member registry replay --- src/test/app/LedgerReplay_test.cpp | 199 +++++++++++++++++++- src/test/consensus/UNLReportMember_test.cpp | 92 +++++++++ 2 files changed, 283 insertions(+), 8 deletions(-) diff --git a/src/test/app/LedgerReplay_test.cpp b/src/test/app/LedgerReplay_test.cpp index 20b7f1d52..c3cdcebcc 100644 --- a/src/test/app/LedgerReplay_test.cpp +++ b/src/test/app/LedgerReplay_test.cpp @@ -19,6 +19,8 @@ #include #include +#include +#include #include #include #include @@ -27,11 +29,19 @@ #include #include #include +#include +#include +#include +#include #include #include #include +#include +#include +#include #include +#include #include namespace ripple { @@ -39,8 +49,86 @@ namespace test { struct LedgerReplay_test : public beast::unit_test::suite { + static std::unique_ptr + replayConfig() + { + auto c = jtx::envconfig(); + auto& sectionNode = c->section(ConfigSection::nodeDatabase()); + sectionNode.set("type", "memory"); + c->overwrite(SECTION_RELATIONAL_DB, "backend", "sqlite"); + return c; + } + + bool + expectUNLReportMemberSLE( + std::shared_ptr const& ledger, + Manifest const& manifest) + { + auto const sle = + ledger->read(keylet::UNLReportMember(manifest.masterKey)); + BEAST_EXPECT(sle); + if (!sle) + return false; + + Blob const expectedBlob{ + manifest.serialized.begin(), manifest.serialized.end()}; + + BEAST_EXPECT( + PublicKey{makeSlice(sle->getFieldVL(sfPublicKey))} == + manifest.masterKey); + BEAST_EXPECT(sle->getFieldU32(sfSequence) == manifest.sequence); + BEAST_EXPECT(sle->getFieldVL(sfBlob) == expectedBlob); + BEAST_EXPECT(sle->getFieldH256(sfDigest) == manifest.bindingID()); + BEAST_EXPECT(sle->getFlags() == 0); + BEAST_EXPECT(manifest.signingKey); + BEAST_EXPECT(sle->isFieldPresent(sfSigningPubKey)); + if (manifest.signingKey && sle->isFieldPresent(sfSigningPubKey)) + { + BEAST_EXPECT( + PublicKey{makeSlice(sle->getFieldVL(sfSigningPubKey))} == + *manifest.signingKey); + } + + return true; + } + + bool + expectSameUNLReportMemberSLE( + std::shared_ptr const& lhs, + std::shared_ptr const& rhs, + PublicKey const& masterKey) + { + auto const left = lhs->read(keylet::UNLReportMember(masterKey)); + auto const right = rhs->read(keylet::UNLReportMember(masterKey)); + BEAST_EXPECT(left); + BEAST_EXPECT(right); + if (!left || !right) + return false; + + BEAST_EXPECT(left->getFlags() == right->getFlags()); + BEAST_EXPECT( + left->getFieldU32(sfSequence) == right->getFieldU32(sfSequence)); + BEAST_EXPECT( + left->getFieldVL(sfPublicKey) == right->getFieldVL(sfPublicKey)); + BEAST_EXPECT(left->getFieldVL(sfBlob) == right->getFieldVL(sfBlob)); + BEAST_EXPECT( + left->getFieldH256(sfDigest) == right->getFieldH256(sfDigest)); + BEAST_EXPECT( + left->isFieldPresent(sfSigningPubKey) == + right->isFieldPresent(sfSigningPubKey)); + if (left->isFieldPresent(sfSigningPubKey) && + right->isFieldPresent(sfSigningPubKey)) + { + BEAST_EXPECT( + left->getFieldVL(sfSigningPubKey) == + right->getFieldVL(sfSigningPubKey)); + } + + return true; + } + void - run() override + testReplayLedger() { testcase("Replay ledger"); @@ -50,13 +138,7 @@ struct LedgerReplay_test : public beast::unit_test::suite auto const alice = Account("alice"); auto const bob = Account("bob"); - Env env = [&] { - auto c = jtx::envconfig(); - auto& sectionNode = c->section(ConfigSection::nodeDatabase()); - sectionNode.set("type", "memory"); - c->overwrite(SECTION_RELATIONAL_DB, "backend", "sqlite"); - return jtx::Env(*this, std::move(c)); - }(); + Env env(*this, replayConfig()); env.fund(XRP(100000), alice, bob); env.close(); @@ -73,6 +155,107 @@ struct LedgerReplay_test : public beast::unit_test::suite BEAST_EXPECT(replayed->info().hash == lastClosed->info().hash); } + + void + testReplayUNLReportMemberBootstrap() + { + testcase("Replay UNLReport member bootstrap"); + + using namespace jtx; + + Env env{ + *this, replayConfig(), supported_amendments() | featureUNLReportV2}; + + auto const validator = xpop::TestValidator::create(); + auto const manifest = + deserializeManifest(validator.manifestRaw, env.journal); + BEAST_EXPECT(manifest); + BEAST_EXPECT(manifest && manifest->verify()); + if (!manifest || !manifest->verify()) + return; + + auto evidenceInAppCache = [&]() { + auto const snapshot = + env.app() + .validatorManifests() + .getUNLReportMemberManifestEvidenceSnapshot(); + return std::any_of( + snapshot.begin(), snapshot.end(), [&](Manifest const& m) { + return m.masterKey == validator.masterPublic; + }); + }; + BEAST_EXPECT(!evidenceInAppCache()); + + env.app().openLedger().modify( + [&](OpenView& view, beast::Journal) -> bool { + auto const tx = unl::createUNLReportTx( + env.current()->seq(), + validator.masterPublic, + validator.masterPublic); + auto const txID = tx.getTransactionID(); + auto s = std::make_shared(); + tx.add(*s); + env.app().getHashRouter().setFlags(txID, SF_PRIVATE2); + view.rawTxInsert(txID, std::move(s), nullptr); + return true; + }); + + BEAST_EXPECT(env.close()); + + auto const parent = env.app().getLedgerMaster().getClosedLedger(); + BEAST_EXPECT(parent); + if (!parent) + return; + BEAST_EXPECT(parent->read(keylet::UNLReport())); + BEAST_EXPECT(!evidenceInAppCache()); + + ManifestCache localManifests{env.journal}; + BEAST_EXPECT(localManifests.observeUNLReportMemberManifestEvidence( + makeSlice(validator.manifestRaw))); + + auto updates = buildUNLReportMemberUpdates( + *parent, + localManifests.getUNLReportMemberManifestEvidenceSnapshot()); + BEAST_EXPECT(updates.size() == 1); + if (updates.size() != 1) + return; + + auto const& memberTx = updates.front(); + BEAST_EXPECT(memberTx.getTxnType() == ttUNL_REPORT_MEMBER); + BEAST_EXPECT(!evidenceInAppCache()); + + CanonicalTXSet txns{parent->info().hash}; + txns.insert(std::make_shared(memberTx)); + std::set failed; + + auto const closed = buildLedger( + parent, + env.app().timeKeeper().closeTime(), + true, + parent->info().closeTimeResolution, + env.app(), + txns, + failed, + env.journal); + BEAST_EXPECT(txns.empty()); + BEAST_EXPECT(failed.empty()); + expectUNLReportMemberSLE(closed, *manifest); + BEAST_EXPECT(!evidenceInAppCache()); + + auto const replayed = buildLedger( + LedgerReplay(parent, closed), tapNONE, env.app(), env.journal); + BEAST_EXPECT(replayed->info().hash == closed->info().hash); + expectUNLReportMemberSLE(replayed, *manifest); + expectSameUNLReportMemberSLE(closed, replayed, validator.masterPublic); + BEAST_EXPECT(!evidenceInAppCache()); + } + + void + run() override + { + testReplayLedger(); + testReplayUNLReportMemberBootstrap(); + } }; enum class InboundLedgersBehavior { diff --git a/src/test/consensus/UNLReportMember_test.cpp b/src/test/consensus/UNLReportMember_test.cpp index 4493f6231..5f344e176 100644 --- a/src/test/consensus/UNLReportMember_test.cpp +++ b/src/test/consensus/UNLReportMember_test.cpp @@ -17,8 +17,10 @@ #include #include #include +#include #include +#include #include #include @@ -347,6 +349,95 @@ class UNLReportMember_test : public beast::unit_test::suite BEAST_EXPECT(sle && sle->getFlags() == 0); } + void + testEquivocationConvergesAcrossParentRounds() + { + testcase( + "equal-sequence equivocation converges across parent-ledger " + "rounds"); + jtx::Env env(*this, jtx::supported_amendments() | featureUNLReportV2); + auto const [masterPublic, masterSecret] = generateKeyPair( + KeyType::ed25519, + generateSeed("unl-report-member-converge-master")); + auto const [signingPublic, signingSecret] = generateKeyPair( + KeyType::secp256k1, + generateSeed("unl-report-member-converge-signing-0")); + Validator const v{ + masterPublic, masterSecret, signingPublic, signingSecret}; + auto const [otherPublic, otherSecret] = generateKeyPair( + KeyType::secp256k1, + generateSeed("unl-report-member-converge-signing-1")); + auto const [thirdPublic, thirdSecret] = generateKeyPair( + KeyType::secp256k1, + generateSeed("unl-report-member-converge-signing-2")); + + struct Statement + { + std::string serialized; + uint256 binding; + }; + + std::array statements{{ + {manifest(v, 1), {}}, + {manifest(v, otherPublic, otherSecret, 1), {}}, + {manifest(v, thirdPublic, thirdSecret, 1), {}}, + }}; + for (auto& statement : statements) + statement.binding = parsed(statement.serialized).bindingID(); + std::sort( + statements.begin(), + statements.end(), + [](Statement const& lhs, Statement const& rhs) { + return lhs.binding < rhs.binding; + }); + BEAST_EXPECT(statements[0].binding < statements[1].binding); + BEAST_EXPECT(statements[1].binding < statements[2].binding); + + auto evidence = [](Statement const& first, Statement const& second) { + std::vector result; + result.emplace_back(parsed(first.serialized)); + result.emplace_back(parsed(second.serialized)); + return result; + }; + auto ledger = parentLedger(env, {v.masterPublic}); + auto applyRound = [&](std::vector observed) { + auto const updates = + buildUNLReportMemberUpdates(*ledger, std::move(observed)); + auto next = std::make_shared( + *ledger, env.app().timeKeeper().closeTime()); + OpenView view(&*next); + for (auto const& update : updates) + BEAST_EXPECT(apply(env, view, update) == tesSUCCESS); + view.apply(*next); + ledger = std::move(next); + return updates.size(); + }; + auto expectStored = [&](Statement const& statement) { + ReadView const& view = *ledger; + auto const sle = view.read(keylet::UNLReportMember(v.masterPublic)); + BEAST_EXPECT(sle); + BEAST_EXPECT(sle && sle->getFieldU32(sfSequence) == 1); + BEAST_EXPECT( + sle && + (sle->getFlags() & lsfUNLReportMemberEquivocationFreeze)); + BEAST_EXPECT( + sle && sle->getFieldH256(sfDigest) == statement.binding); + }; + + BEAST_EXPECT(applyRound(evidence(statements[1], statements[2])) == 2); + expectStored(statements[1]); + + BEAST_EXPECT(applyRound(evidence(statements[0], statements[2])) == 1); + expectStored(statements[0]); + + BEAST_EXPECT(buildUNLReportMemberUpdates( + *ledger, evidence(statements[1], statements[2])) + .empty()); + BEAST_EXPECT(buildUNLReportMemberUpdates( + *ledger, evidence(statements[0], statements[2])) + .empty()); + } + void testSigningKeyCollision() { @@ -474,6 +565,7 @@ public: testRotationAndRevocation(); testInactiveMemberCanRevoke(); testEquivocationIsOrderIndependent(); + testEquivocationConvergesAcrossParentRounds(); testSigningKeyCollision(); testDeltaSelection(); }