mirror of
https://github.com/Xahau/xahaud.git
synced 2026-09-27 23:48:05 +00:00
test(consensus): include incomplete origin in the global Export candidate
Origin A uses committee {0,1} (qC=2); B uses {0,1,2} (qC=3). Drop only
validator 2's B frames toward the observer. At a real job boundary the
observer's existing sidecar map must show A=2 and B below qC. Validators
may still witness B; the observer may acquire.
This commit is contained in:
@@ -7,6 +7,9 @@
|
||||
#include <xrpld/app/misc/ValidatorKeys.h>
|
||||
#include <xrpl/basics/strHex.h>
|
||||
#include <xrpl/protocol/Serializer.h>
|
||||
#include <xrpl/protocol/SerialIter.h>
|
||||
#include <xrpld/shamap/SHAMap.h>
|
||||
#include <xrpld/shamap/SHAMapItem.h>
|
||||
#include <xrpl/protocol/ExportCommittee.h>
|
||||
#include <xrpl/protocol/ExportLimits.h>
|
||||
#include <xrpl/protocol/Feature.h>
|
||||
@@ -79,6 +82,11 @@ class SteppingExtensions_test : public beast::unit_test::suite
|
||||
std::uint32_t invertedSharePairs = 0;
|
||||
std::vector<uint256> shareSendOrder;
|
||||
std::vector<uint256> shareRecvOrder;
|
||||
std::uint32_t droppedStarvedDirect = 0;
|
||||
std::uint32_t droppedStarvedProposals = 0;
|
||||
bool sawPartialCandidate = false;
|
||||
std::size_t candidateLeavesA = 0;
|
||||
std::size_t candidateLeavesB = 0;
|
||||
};
|
||||
|
||||
template <class T>
|
||||
@@ -108,6 +116,27 @@ class SteppingExtensions_test : public beast::unit_test::suite
|
||||
return s.getSHA512Half();
|
||||
}
|
||||
|
||||
static std::size_t
|
||||
originSidecarLeaves(SHAMap const& map, uint256 const& origin)
|
||||
{
|
||||
std::size_t n = 0;
|
||||
map.visitLeaves(
|
||||
[&](boost::intrusive_ptr<SHAMapItem const> const& item) {
|
||||
try
|
||||
{
|
||||
SerialIter sit{item->slice()};
|
||||
STObject const obj{sit, sfGeneric};
|
||||
if (obj.isFieldPresent(sfTransactionHash) &&
|
||||
obj.getFieldH256(sfTransactionHash) == origin)
|
||||
++n;
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
}
|
||||
});
|
||||
return n;
|
||||
}
|
||||
|
||||
static bool
|
||||
authorizedAtEmission(Application& app, ExportShare const& share)
|
||||
{
|
||||
@@ -299,10 +328,11 @@ class SteppingExtensions_test : public beast::unit_test::suite
|
||||
intent(
|
||||
jtx::Account const& acct,
|
||||
std::uint32_t ticket,
|
||||
std::uint32_t lastLedger)
|
||||
std::uint32_t lastLedger,
|
||||
std::vector<std::uint32_t> const& slots = {0, 1, 2})
|
||||
{
|
||||
std::vector<PublicKey> keys;
|
||||
for (std::uint32_t i = 0; i < observer; ++i)
|
||||
for (auto const i : slots)
|
||||
keys.push_back(
|
||||
net.node(i).app().getValidatorKeys().keys->masterPublicKey);
|
||||
auto const roster = canonicalizeExportCommittee(
|
||||
@@ -1016,6 +1046,199 @@ class SteppingExtensions_test : public beast::unit_test::suite
|
||||
return outcome;
|
||||
}
|
||||
|
||||
std::optional<std::vector<uint256>>
|
||||
incompleteOriginInCandidate(SteppingNetwork& net)
|
||||
{
|
||||
World world(net, false, true);
|
||||
if (!ready(world))
|
||||
return std::nullopt;
|
||||
auto const fund = world.submit(
|
||||
observer,
|
||||
jtx::pay(jtx::Account::master, world.owner, jtx::XRP(10'000)),
|
||||
jtx::Account::master);
|
||||
if (!BEAST_EXPECT(fund && fund->getResult() == tesSUCCESS))
|
||||
return std::nullopt;
|
||||
net.runTo(warmLedger + 2);
|
||||
if (!BEAST_EXPECT(net.minValidatedSeq() >= warmLedger + 2))
|
||||
return std::nullopt;
|
||||
|
||||
auto const open =
|
||||
net.node(observer).app().openLedger().current()->seq();
|
||||
auto const window = open + ExportLimits::maxAdmissionWindowLedgers;
|
||||
auto const txA = world.submit(
|
||||
observer,
|
||||
world.intent(world.owner, 1, window, {0, 1}),
|
||||
world.owner);
|
||||
auto const txB = world.submit(
|
||||
observer,
|
||||
world.intent(world.owner, 2, window, {0, 1, 2}),
|
||||
world.owner);
|
||||
if (!BEAST_EXPECT(
|
||||
txA && txA->getResult() == tesSUCCESS && txB &&
|
||||
txB->getResult() == tesSUCCESS))
|
||||
return std::nullopt;
|
||||
auto const originA = txA->getID();
|
||||
auto const originB = txB->getID();
|
||||
auto const qA = ExportLimits::committeeQuorumThreshold(2);
|
||||
auto const qB = ExportLimits::committeeQuorumThreshold(3);
|
||||
auto const stats = world.observed;
|
||||
net.faultFrames(
|
||||
2,
|
||||
observer,
|
||||
[stats, originB](std::uint16_t type, SimPipe::Frame bytes) {
|
||||
SimFault f;
|
||||
auto const hit = [&](std::string const& blob) {
|
||||
auto const share = ExportShare::parse(makeSlice(blob));
|
||||
return share && share->originTxn == originB;
|
||||
};
|
||||
if (type == protocol::mtEXPORT_SHARES)
|
||||
{
|
||||
auto const batch =
|
||||
decodeFrame<protocol::TMExportShares>(bytes);
|
||||
for (auto const& share : batch->shares())
|
||||
if (hit(share))
|
||||
{
|
||||
++stats->droppedStarvedDirect;
|
||||
f.drop = true;
|
||||
return f;
|
||||
}
|
||||
}
|
||||
if (type == protocol::mtPROPOSE_LEDGER)
|
||||
{
|
||||
auto const proposal =
|
||||
decodeFrame<protocol::TMProposeSet>(bytes);
|
||||
for (auto const& share : proposal->exportsignatures())
|
||||
if (hit(share))
|
||||
{
|
||||
++stats->droppedStarvedProposals;
|
||||
f.drop = true;
|
||||
return f;
|
||||
}
|
||||
}
|
||||
return f;
|
||||
});
|
||||
net.controller().observeJobs(
|
||||
[this, &net, stats, originA, originB, qA, qB](
|
||||
std::uint32_t id, JobType, std::string const&) {
|
||||
if (id != observer || !net.isLive(id) ||
|
||||
stats->sawPartialCandidate)
|
||||
return;
|
||||
auto& ce = net.node(id).app().getConsensusExtensions();
|
||||
if (!ce.roundParentLedger_ || !ce.exportSigSetMap_)
|
||||
return;
|
||||
auto const& parent = *ce.roundParentLedger_;
|
||||
auto const pending =
|
||||
ce.pendingRoundExports(parent.info().seq + 1);
|
||||
if (!pending.contains(originA) || !pending.contains(originB))
|
||||
return;
|
||||
auto const leavesA =
|
||||
originSidecarLeaves(*ce.exportSigSetMap_, originA);
|
||||
auto const leavesB =
|
||||
originSidecarLeaves(*ce.exportSigSetMap_, originB);
|
||||
if (leavesA == qA && leavesB > 0 && leavesB < qB)
|
||||
{
|
||||
stats->sawPartialCandidate = true;
|
||||
stats->candidateLeavesA = leavesA;
|
||||
stats->candidateLeavesB = leavesB;
|
||||
}
|
||||
});
|
||||
|
||||
auto const mid = warmLedger + 8;
|
||||
net.runTo(mid, SteppingNetwork::RunBudget{1600, 1'200'000});
|
||||
if (!BEAST_EXPECT(net.minValidatedSeq() >= mid))
|
||||
return std::nullopt;
|
||||
BEAST_EXPECT(stats->droppedStarvedDirect != 0);
|
||||
BEAST_EXPECT(stats->droppedStarvedProposals != 0);
|
||||
BEAST_EXPECT(stats->sawPartialCandidate);
|
||||
BEAST_EXPECT(stats->candidateLeavesA == qA);
|
||||
BEAST_EXPECT(stats->candidateLeavesB > 0);
|
||||
BEAST_EXPECT(stats->candidateLeavesB < qB);
|
||||
log << " candidate: A=" << stats->candidateLeavesA << "/" << qA
|
||||
<< " B=" << stats->candidateLeavesB << "/" << qB
|
||||
<< " droppedDirect=" << stats->droppedStarvedDirect
|
||||
<< " droppedProposals=" << stats->droppedStarvedProposals
|
||||
<< std::endl;
|
||||
|
||||
net.faultFrames(2, observer, {});
|
||||
auto const target = warmLedger + 12;
|
||||
net.runTo(target, SteppingNetwork::RunBudget{1600, 1'200'000});
|
||||
if (!BEAST_EXPECT(net.minValidatedSeq() >= target))
|
||||
return std::nullopt;
|
||||
BEAST_EXPECT(net.ledgersAgree(target));
|
||||
BEAST_EXPECT(net.validatedForkFree());
|
||||
BEAST_EXPECT(net.offThreadJobs() == 0);
|
||||
BEAST_EXPECT(stats->secrets[observer] == 0);
|
||||
BEAST_EXPECT(stats->ownReleases[observer] == 0);
|
||||
auto const seqA = witnessAt(net, originA, warmLedger);
|
||||
BEAST_EXPECT(seqA != 0);
|
||||
if (seqA)
|
||||
BEAST_EXPECT(stats->builds[observer].contains(
|
||||
{seqA,
|
||||
net.ledgerHash(0, seqA - 1),
|
||||
net.ledgerHash(0, seqA)}));
|
||||
std::map<uint256, std::uint32_t> hits;
|
||||
std::set<uint256> unexpected;
|
||||
std::vector<uint256> outcome;
|
||||
for (auto seq = warmLedger; seq <= target; ++seq)
|
||||
{
|
||||
auto const canonical = net.ledger(0, seq);
|
||||
if (!BEAST_EXPECT(canonical != nullptr))
|
||||
return std::nullopt;
|
||||
for (std::uint32_t i = 1; i <= observer; ++i)
|
||||
{
|
||||
auto const ledger = net.ledger(i, seq);
|
||||
if (!BEAST_EXPECT(ledger != nullptr))
|
||||
return std::nullopt;
|
||||
BEAST_EXPECT(ledger->info().hash == canonical->info().hash);
|
||||
}
|
||||
outcome.push_back(canonical->info().hash);
|
||||
for (auto const& [tx, meta] : canonical->txs)
|
||||
{
|
||||
if (tx->getTxnType() != ttEXPORT_SIGNATURES)
|
||||
continue;
|
||||
if (!BEAST_EXPECT(meta != nullptr))
|
||||
return std::nullopt;
|
||||
auto const origin = tx->getFieldH256(sfTransactionHash);
|
||||
if (origin != originA && origin != originB)
|
||||
unexpected.insert(origin);
|
||||
else
|
||||
++hits[origin];
|
||||
auto const txBytes = tx->getSerializer().getData();
|
||||
auto const metaBytes = meta->getSerializer().getData();
|
||||
outcome.push_back(
|
||||
sha512Half(makeSlice(txBytes), makeSlice(metaBytes)));
|
||||
for (std::uint32_t i = 0; i <= observer; ++i)
|
||||
{
|
||||
auto const ledger = net.ledger(i, seq);
|
||||
bool found = false;
|
||||
for (auto const& [peerTx, peerMeta] : ledger->txs)
|
||||
{
|
||||
if (peerTx->getTxnType() != ttEXPORT_SIGNATURES ||
|
||||
peerTx->getFieldH256(sfTransactionHash) != origin)
|
||||
continue;
|
||||
found = true;
|
||||
BEAST_EXPECT(
|
||||
peerTx->getSerializer().getData() == txBytes);
|
||||
BEAST_EXPECT(
|
||||
peerMeta != nullptr &&
|
||||
peerMeta->getSerializer().getData() == metaBytes);
|
||||
}
|
||||
BEAST_EXPECT(found);
|
||||
}
|
||||
}
|
||||
}
|
||||
BEAST_EXPECT(unexpected.empty());
|
||||
BEAST_EXPECT(hits[originA] == 1);
|
||||
// B may be witnessed by the informed cohort; observer may acquire.
|
||||
BEAST_EXPECT(hits[originB] <= 1);
|
||||
outcome.push_back(originA);
|
||||
outcome.push_back(originB);
|
||||
outcome.push_back(sha512Half(
|
||||
static_cast<std::uint32_t>(stats->candidateLeavesA),
|
||||
static_cast<std::uint32_t>(stats->candidateLeavesB)));
|
||||
return outcome;
|
||||
}
|
||||
|
||||
public:
|
||||
void
|
||||
run() override
|
||||
@@ -1143,6 +1366,16 @@ public:
|
||||
return overlappingOrigins(net);
|
||||
});
|
||||
}
|
||||
if (matches("incomplete origin in global candidate"))
|
||||
{
|
||||
testcase("incomplete origin in global candidate");
|
||||
expectReplays(
|
||||
*this,
|
||||
"incomplete origin in global candidate",
|
||||
[this](SteppingNetwork& net) {
|
||||
return incompleteOriginInCandidate(net);
|
||||
});
|
||||
}
|
||||
BEAST_EXPECT(selected != 0);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user