fix(consensus): apply negative unl to sidecar validator view

This commit is contained in:
Nicholas Dudfield
2026-04-27 12:50:43 +07:00
parent 7b5865c69c
commit f4e78c9a24
3 changed files with 148 additions and 14 deletions

View File

@@ -0,0 +1,120 @@
//------------------------------------------------------------------------------
/*
This file is part of rippled: https://github.com/ripple/rippled
Permission to use, copy, modify, and/or distribute this software for any
purpose with or without fee is hereby granted, provided that the above
copyright notice and this permission notice appear in all copies.
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
*/
//==============================================================================
#include <test/jtx.h>
#include <xrpld/app/consensus/ConsensusExtensions.h>
#include <xrpld/app/ledger/Ledger.h>
#include <xrpl/basics/StringUtilities.h>
#include <xrpl/beast/unit_test.h>
#include <xrpl/protocol/Feature.h>
#include <xrpl/protocol/Indexes.h>
namespace ripple {
namespace test {
class ConsensusExtensions_test : public beast::unit_test::suite
{
std::vector<PublicKey>
makeValidatorKeys() const
{
std::vector<std::string> const rawKeys = {
"0388935426E0D08083314842EDFBB2D517BD47699F9A4527318A8E10468C97C05"
"2",
"02691AC5AE1C4C333AE5DF8A93BDC495F0EEBFC6DB0DA7EB6EF808F3AFC006E3F"
"E"};
std::vector<PublicKey> keys;
keys.reserve(rawKeys.size());
for (auto const& rawKey : rawKeys)
{
auto const pkHex = strUnHex(rawKey);
keys.emplace_back(makeSlice(*pkHex));
}
return keys;
}
void
testActiveValidatorViewAppliesNegativeUNL()
{
testcase("Active validator view applies NegativeUNL");
using namespace jtx;
Env env{
*this,
envconfig(),
supported_amendments() | featureNegativeUNL,
nullptr};
auto const vlKeys = makeValidatorKeys();
auto const genesis = std::make_shared<Ledger>(
create_genesis,
env.app().config(),
std::vector<uint256>{},
env.app().getNodeFamily());
auto l = std::make_shared<Ledger>(
*genesis, env.app().timeKeeper().closeTime());
BEAST_EXPECT(l->rules().enabled(featureNegativeUNL));
auto report = std::make_shared<SLE>(keylet::UNLReport());
std::vector<STObject> activeValidators;
for (auto const& pk : vlKeys)
{
activeValidators.push_back(
STObject::makeInnerObject(sfActiveValidator));
activeValidators.back().setFieldVL(sfPublicKey, pk);
}
report->setFieldArray(
sfActiveValidators, STArray(activeValidators, sfActiveValidators));
auto negUnl = std::make_shared<SLE>(keylet::negativeUNL());
std::vector<STObject> disabledValidators;
disabledValidators.push_back(
STObject::makeInnerObject(sfDisabledValidator));
disabledValidators.back().setFieldVL(sfPublicKey, vlKeys[0]);
disabledValidators.back().setFieldU32(sfFirstLedgerSequence, l->seq());
negUnl->setFieldArray(
sfDisabledValidators,
STArray(disabledValidators, sfDisabledValidators));
OpenView accum(&*l);
accum.rawInsert(report);
accum.rawInsert(negUnl);
accum.apply(*l);
ConsensusExtensions ce{env.app(), env.journal};
auto const view = ce.makeActiveValidatorView(l);
BEAST_EXPECT(view->fromUNLReport);
BEAST_EXPECT(view->size() == 1);
BEAST_EXPECT(!view->containsMaster(vlKeys[0]));
BEAST_EXPECT(!view->containsNode(calcNodeID(vlKeys[0])));
BEAST_EXPECT(view->containsMaster(vlKeys[1]));
BEAST_EXPECT(view->containsNode(calcNodeID(vlKeys[1])));
}
public:
void
run() override
{
testActiveValidatorViewAppliesNegativeUNL();
}
};
BEAST_DEFINE_TESTSUITE(ConsensusExtensions, consensus, ripple);
} // namespace test
} // namespace ripple

View File

@@ -92,22 +92,29 @@ buildActiveValidatorView(
}
}
// TODO: If this fork still supports Negative UNL, fold that policy into
// this active view before quorum sizing or sidecar eligibility decisions.
if (!view.masterKeys.empty())
return view;
if (view.masterKeys.empty())
{
// Fallback exists for early ledgers and dev/test networks before the
// report object is available. It is deliberately the configured trusted
// master-key set so manifest signing keys still resolve through trust.
for (auto const& masterKey : app.validators().getTrustedMasterKeys())
view.insertMaster(masterKey);
// Fallback exists for early ledgers and dev/test networks before the
// report object is available. It is deliberately the configured trusted
// master-key set so manifest signing keys still resolve through trust.
for (auto const& masterKey : app.validators().getTrustedMasterKeys())
view.insertMaster(masterKey);
// Some standalone/dev configurations trust local validation implicitly.
// insertMaster() makes this idempotent if self is already trusted.
auto const& valKeys = app.getValidatorKeys();
if (valKeys.keys && valKeys.nodeID != beast::zero)
view.insertMaster(valKeys.keys->masterPublicKey);
}
// Some standalone/dev configurations trust local validation implicitly.
// insertMaster() makes this idempotent if self is already trusted.
auto const& valKeys = app.getValidatorKeys();
if (valKeys.keys && valKeys.nodeID != beast::zero)
view.insertMaster(valKeys.keys->masterPublicKey);
if (sourceLedger && sourceLedger->rules().enabled(featureNegativeUNL))
{
// UNLReport records recently active validators; NegativeUNL is the
// separate ledger policy overlay that core consensus applies to quorum.
// Apply it to either source so sidecar quorum matches that policy.
for (auto const& masterKey : sourceLedger->negativeUNL())
view.eraseMaster(masterKey);
}
return view;
}

View File

@@ -58,6 +58,13 @@ public:
nodeIds.insert(calcNodeID(masterKey));
}
void
eraseMaster(PublicKey const& masterKey)
{
masterKeys.erase(masterKey);
nodeIds.erase(calcNodeID(masterKey));
}
std::size_t
size() const
{