test(consensus): make share reorder deterministic; drop below-qC starve

Delay by sender id toward the observer so even frames overtake node 0
without a global counter. Overlapping origins keep three distinct
witnesses; below-qC isolation needs per-origin proposal surgery and is
left as a gap.
This commit is contained in:
Nicholas Dudfield
2026-09-18 18:22:01 +07:00
parent eb9dde1a20
commit ab36bfa652

View File

@@ -76,8 +76,6 @@ class SteppingExtensions_test : public beast::unit_test::suite
std::uint32_t serviceLag = 0;
std::uint32_t reorderedShares = 0;
std::uint32_t invertedSharePairs = 0;
std::chrono::steady_clock::duration lastShareDelay{};
bool haveShareDelay = false;
};
template <class T>
@@ -540,35 +538,26 @@ class SteppingExtensions_test : public beast::unit_test::suite
{
using namespace std::chrono_literals;
for (std::uint32_t from = 0; from < observer; ++from)
for (std::uint32_t to = 0; to <= observer; ++to)
if (from != to)
net.faultFrames(
from,
to,
[stats](
std::uint16_t type, SimPipe::Frame bytes) {
SimFault f;
if (type != protocol::mtEXPORT_SHARES)
return f;
auto const batch =
decodeFrame<protocol::TMExportShares>(
bytes);
if (!batch || batch->shares_size() == 0)
return f;
++stats->reorderedShares;
// Odd frames sit in-flight; even frames go
// almost immediately and overtake them.
auto const delay =
(stats->reorderedShares % 2 == 1) ? 800ms
: 50ms;
if (stats->haveShareDelay &&
delay < stats->lastShareDelay)
++stats->invertedSharePairs;
stats->lastShareDelay = delay;
stats->haveShareDelay = true;
f.delay = delay;
return f;
});
net.faultFrames(
from,
observer,
[stats, from](
std::uint16_t type, SimPipe::Frame bytes) {
SimFault f;
if (type != protocol::mtEXPORT_SHARES)
return f;
auto const batch =
decodeFrame<protocol::TMExportShares>(bytes);
if (!batch || batch->shares_size() == 0)
return f;
++stats->reorderedShares;
// Node 0's frames sit; nodes 1 and 2 overtake them.
auto const delay = (from == 0) ? 800ms : 50ms;
if (from != 0)
++stats->invertedSharePairs;
f.delay = delay;
return f;
});
}
if (fault == Fault::slowObserver)
net.lagAccept(observer, 5s);
@@ -616,9 +605,7 @@ class SteppingExtensions_test : public beast::unit_test::suite
net.faultLink(from, 0, {});
if (fault == Fault::reorderShares)
for (std::uint32_t from = 0; from < observer; ++from)
for (std::uint32_t to = 0; to <= observer; ++to)
if (from != to)
net.faultFrames(from, to, {});
net.faultFrames(from, observer, {});
});
}
auto const payment = world.submit(
@@ -873,7 +860,7 @@ class SteppingExtensions_test : public beast::unit_test::suite
net.node(observer).app().openLedger().current()->seq();
auto const longWindow =
open + ExportLimits::maxAdmissionWindowLedgers;
auto const shortWindow = open + 24;
auto const shortWindow = open + 3;
std::vector<uint256> origins;
auto const submitIntent =
[&](jtx::Account const& acct,
@@ -892,55 +879,13 @@ class SteppingExtensions_test : public beast::unit_test::suite
!submitIntent(world.owner, 2, shortWindow) ||
!submitIntent(owner2, 1, longWindow))
return std::nullopt;
auto const starved = origins.back();
for (std::uint32_t from = 1; from < observer; ++from)
for (std::uint32_t to = 0; to <= observer; ++to)
if (from != to)
net.faultFrames(
from,
to,
[starved](
std::uint16_t type, SimPipe::Frame bytes) {
SimFault f;
auto const starve = [&](std::string const& blob) {
auto const share =
ExportShare::parse(makeSlice(blob));
return share && share->originTxn == starved;
};
if (type == protocol::mtEXPORT_SHARES)
{
auto const batch =
decodeFrame<protocol::TMExportShares>(
bytes);
for (auto const& share : batch->shares())
if (starve(share))
{
f.drop = true;
return f;
}
}
if (type == protocol::mtPROPOSE_LEDGER)
{
auto const proposal =
decodeFrame<protocol::TMProposeSet>(
bytes);
for (auto const& share :
proposal->exportsignatures())
if (starve(share))
{
f.drop = true;
return f;
}
}
return f;
});
// Below-qC isolation of one origin would require dropping selected
// contributions out of signed proposal frames. Dropping the whole
// proposal also drops other origins and ordinary agreement. Left
// as a gap.
auto const target = warmLedger + 12;
net.runTo(target, SteppingNetwork::RunBudget{1600, 1'200'000});
for (std::uint32_t from = 1; from < observer; ++from)
for (std::uint32_t to = 0; to <= observer; ++to)
if (from != to)
net.faultFrames(from, to, {});
if (!BEAST_EXPECT(net.minValidatedSeq() >= target))
return std::nullopt;
BEAST_EXPECT(net.ledgersAgree(target));
@@ -959,37 +904,29 @@ class SteppingExtensions_test : public beast::unit_test::suite
if (!BEAST_EXPECT(found != collected.end()))
return std::nullopt;
auto const seq = witnessAt(net, origin, warmLedger);
if (origin == starved)
BEAST_EXPECT(found->second.size() == observer);
BEAST_EXPECT(seq != 0);
if (seq)
{
BEAST_EXPECT(found->second.size() == 1);
BEAST_EXPECT(seq == 0);
}
else
{
BEAST_EXPECT(found->second.size() == observer);
BEAST_EXPECT(seq != 0);
if (seq)
++witnessed;
witnessOrigins.insert(origin);
auto const ledger = net.ledger(observer, seq);
if (!BEAST_EXPECT(ledger))
return std::nullopt;
std::uint32_t hits = 0;
for (auto const& [tx, meta] : ledger->txs)
{
++witnessed;
witnessOrigins.insert(origin);
auto const ledger = net.ledger(observer, seq);
if (!BEAST_EXPECT(ledger))
return std::nullopt;
std::uint32_t hits = 0;
for (auto const& [tx, meta] : ledger->txs)
{
if (tx->getTxnType() != ttEXPORT_SIGNATURES)
continue;
BEAST_EXPECT(meta != nullptr);
if (tx->getFieldH256(sfTransactionHash) == origin)
++hits;
}
BEAST_EXPECT(hits == 1);
if (tx->getTxnType() != ttEXPORT_SIGNATURES)
continue;
BEAST_EXPECT(meta != nullptr);
if (tx->getFieldH256(sfTransactionHash) == origin)
++hits;
}
BEAST_EXPECT(hits == 1);
}
}
BEAST_EXPECT(witnessed == 2);
BEAST_EXPECT(witnessOrigins.size() == 2);
BEAST_EXPECT(witnessed == origins.size());
BEAST_EXPECT(witnessOrigins.size() == origins.size());
std::vector<uint256> outcome;
for (auto seq = warmLedger; seq <= target; ++seq)
if (auto const ledger = net.ledger(observer, seq))