Skip to content

Commit 5dc8b35

Browse files
authored
Add a randomized test for 'frozen' DEX offers. (#5174)
# Description Removal of offers that would affect frozen entries from DEX has already been covered in a simple smoke test. This is a more robust randomized test that ensures that DEX behavior is the same both with and without 'frozen' offers. # Checklist - [ ] Reviewed the [contributing](https://github.com/stellar/stellar-core/blob/master/CONTRIBUTING.md#submitting-changes) document - [ ] Rebased on top of master (no merge commits) - [ ] Ran `clang-format` v8.0.0 (via `make format` or the Visual Studio extension) - [ ] Compiles - [ ] Ran all tests - [ ] If change impacts performance, include supporting evidence per the [performance document](https://github.com/stellar/stellar-core/blob/master/performance-eval/performance-eval.md)
2 parents 57ee5ed + ba7c780 commit 5dc8b35

File tree

1 file changed

+291
-0
lines changed

1 file changed

+291
-0
lines changed

src/transactions/test/FrozenLedgerKeysTests.cpp

Lines changed: 291 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1999,5 +1999,296 @@ TEST_CASE("frozen ledger keys DEX path payments",
19991999
}
20002000
}
20012001

2002+
TEST_CASE("frozen offers are transparent to DEX matching - randomized",
2003+
"[frozenledgerkeys][offers][acceptance]")
2004+
{
2005+
constexpr int NUM_ITERATIONS = 10;
2006+
constexpr int ACTIVE_MAKERS_PER_PAIR = 10;
2007+
constexpr int FROZEN_MAKERS_PER_PAIR = 5;
2008+
constexpr int NUM_PAIRS = 3;
2009+
constexpr int NUM_OPS = 15;
2010+
constexpr int NUM_ASSETS = 3;
2011+
constexpr int MAX_BATCH_SIZE = 10;
2012+
constexpr int TOTAL_ACTIVE_MAKERS = ACTIVE_MAKERS_PER_PAIR * NUM_PAIRS;
2013+
constexpr int TOTAL_FROZEN_MAKERS = FROZEN_MAKERS_PER_PAIR * NUM_PAIRS;
2014+
2015+
constexpr int NUM_TRACKED = 1 + TOTAL_ACTIVE_MAKERS;
2016+
2017+
struct MarketResult
2018+
{
2019+
std::vector<int64_t> deltas;
2020+
int frozenOffersRemoved;
2021+
int txsSucceeded;
2022+
};
2023+
2024+
int totalFrozenOffersRemoved = 0;
2025+
int totalTxsSucceeded = 0;
2026+
2027+
for (int iter = 0; iter < NUM_ITERATIONS; ++iter)
2028+
{
2029+
INFO("iteration " << iter);
2030+
2031+
auto iterSeed = stellar::uniform_int_distribution<uint32_t>(
2032+
0, UINT32_MAX)(Catch::rng());
2033+
2034+
auto runMarket = [&](uint32_t seed,
2035+
bool withFrozenOffers) -> MarketResult {
2036+
std::mt19937 rng(seed);
2037+
2038+
VirtualClock clock;
2039+
auto cfg = getTestConfig();
2040+
auto app = createTestApplication(clock, cfg);
2041+
auto root = app->getRoot();
2042+
auto const& lm = app->getLedgerManager();
2043+
auto minBalance =
2044+
lm.getLastMinBalance(20) + 100 * lm.getLastTxFee();
2045+
2046+
// Initialize accounts and assets.
2047+
auto issuerUSD = root->create("issuerUSD", minBalance);
2048+
auto issuerEUR = root->create("issuerEUR", minBalance);
2049+
auto xlm = makeNativeAsset();
2050+
auto usd = makeAsset(issuerUSD, "USD");
2051+
auto eur = makeAsset(issuerEUR, "EUR");
2052+
2053+
Asset assets[NUM_ASSETS] = {xlm, usd, eur};
2054+
2055+
struct Pair
2056+
{
2057+
Asset selling;
2058+
Asset buying;
2059+
};
2060+
Pair pairs[NUM_PAIRS] = {{usd, xlm}, {eur, xlm}, {usd, eur}};
2061+
2062+
auto randInt = [&](int lo, int hi) {
2063+
stellar::uniform_int_distribution<int> dist(lo, hi);
2064+
return dist(rng);
2065+
};
2066+
2067+
auto fundNonNativeAssets = [&](TestAccount& account) {
2068+
for (auto const& asset : assets)
2069+
{
2070+
if (asset.type() != ASSET_TYPE_NATIVE)
2071+
{
2072+
account.changeTrust(asset, INT64_MAX);
2073+
auto& issuer = (asset == usd) ? issuerUSD : issuerEUR;
2074+
issuer.pay(account, asset, 100'000);
2075+
}
2076+
}
2077+
};
2078+
2079+
std::vector<TestAccount> activeMakers;
2080+
activeMakers.reserve(TOTAL_ACTIVE_MAKERS);
2081+
for (int i = 0; i < TOTAL_ACTIVE_MAKERS; ++i)
2082+
{
2083+
auto name = fmt::format("maker{}", i);
2084+
activeMakers.emplace_back(root->create(name, minBalance));
2085+
fundNonNativeAssets(activeMakers.back());
2086+
}
2087+
2088+
auto taker = root->create("taker", minBalance);
2089+
fundNonNativeAssets(taker);
2090+
2091+
std::vector<TestAccount> feePayers;
2092+
feePayers.reserve(MAX_BATCH_SIZE);
2093+
for (int i = 0; i < MAX_BATCH_SIZE; ++i)
2094+
{
2095+
auto name = fmt::format("feePayer{}", i);
2096+
feePayers.emplace_back(root->create(name, minBalance));
2097+
}
2098+
2099+
// Initialize market with active offers.
2100+
int makerIdx = 0;
2101+
for (auto const& pair : pairs)
2102+
{
2103+
for (int j = 0; j < ACTIVE_MAKERS_PER_PAIR; ++j, ++makerIdx)
2104+
{
2105+
auto priceN = static_cast<int32_t>(randInt(1, 10));
2106+
auto priceD = static_cast<int32_t>(randInt(1, 10));
2107+
int64_t amount = randInt(100, 5000);
2108+
activeMakers[makerIdx].manageOffer(
2109+
0, pair.selling, pair.buying, Price{priceN, priceD},
2110+
amount);
2111+
}
2112+
}
2113+
2114+
struct FrozenOfferInfo
2115+
{
2116+
TestAccount account;
2117+
int64_t offerID;
2118+
};
2119+
std::vector<FrozenOfferInfo> frozenOffers;
2120+
// Create frozen offers (only in run with frozen offers).
2121+
if (withFrozenOffers)
2122+
{
2123+
int frozenIdx = 0;
2124+
for (auto const& pair : pairs)
2125+
{
2126+
for (int j = 0; j < FROZEN_MAKERS_PER_PAIR;
2127+
++j, ++frozenIdx)
2128+
{
2129+
auto name = fmt::format("frozen{}", frozenIdx);
2130+
auto frozenMaker = root->create(name, minBalance);
2131+
fundNonNativeAssets(frozenMaker);
2132+
2133+
auto priceN = static_cast<int32_t>(randInt(1, 10));
2134+
auto priceD = static_cast<int32_t>(randInt(1, 10));
2135+
int64_t amount = randInt(100, 5000);
2136+
auto offerID = frozenMaker.manageOffer(
2137+
0, pair.selling, pair.buying, Price{priceN, priceD},
2138+
amount);
2139+
freezeKey(*app,
2140+
frozenKeyForAsset(frozenMaker, pair.selling));
2141+
frozenOffers.emplace_back(
2142+
FrozenOfferInfo{std::move(frozenMaker), offerID});
2143+
}
2144+
}
2145+
}
2146+
2147+
auto recordBalances = [&]() {
2148+
std::vector<int64_t> balances(NUM_TRACKED * NUM_ASSETS, 0);
2149+
auto record = [&](int accIdx, TestAccount& acc) {
2150+
for (int a = 0; a < NUM_ASSETS; ++a)
2151+
{
2152+
balances[accIdx * NUM_ASSETS + a] =
2153+
loadDexAssetState(*app, acc, assets[a]).balance;
2154+
}
2155+
};
2156+
record(0, taker);
2157+
for (int i = 0; i < TOTAL_ACTIVE_MAKERS; ++i)
2158+
{
2159+
record(i + 1, activeMakers[i]);
2160+
}
2161+
return balances;
2162+
};
2163+
2164+
auto preBalances = recordBalances();
2165+
2166+
// Execute deterministic operation sequence in random-sized
2167+
// batches. Use a separate RNG stream so frozen offer creation
2168+
// does not change the operations.
2169+
std::mt19937 opsRng(seed + 1000);
2170+
auto opsRandInt = [&](int lo, int hi) {
2171+
stellar::uniform_int_distribution<int> dist(lo, hi);
2172+
return dist(opsRng);
2173+
};
2174+
2175+
int txsSucceeded = 0;
2176+
int opsGenerated = 0;
2177+
while (opsGenerated < NUM_OPS)
2178+
{
2179+
int batchSize = std::min(opsRandInt(1, MAX_BATCH_SIZE),
2180+
NUM_OPS - opsGenerated);
2181+
2182+
std::vector<TransactionFrameBasePtr> batch;
2183+
batch.reserve(batchSize);
2184+
for (int b = 0; b < batchSize; ++b, ++opsGenerated)
2185+
{
2186+
int opType = opsRandInt(0, 4);
2187+
int pairIdx = opsRandInt(0, NUM_PAIRS - 1);
2188+
auto const& pair = pairs[pairIdx];
2189+
2190+
int64_t amount = opsRandInt(50, 2000);
2191+
auto pN = static_cast<int32_t>(opsRandInt(1, 10));
2192+
auto pD = static_cast<int32_t>(opsRandInt(1, 10));
2193+
2194+
Operation dexOp;
2195+
switch (opType)
2196+
{
2197+
case 0:
2198+
dexOp = manageOffer(0, pair.buying, pair.selling,
2199+
Price{pN, pD}, amount);
2200+
break;
2201+
case 1:
2202+
dexOp = manageBuyOffer(0, pair.buying, pair.selling,
2203+
Price{pN, pD}, amount);
2204+
break;
2205+
case 2:
2206+
dexOp = createPassiveOffer(pair.buying, pair.selling,
2207+
Price{pN, pD}, amount);
2208+
break;
2209+
case 3:
2210+
dexOp =
2211+
pathPayment(taker.getPublicKey(), pair.buying,
2212+
amount * 10, pair.selling, amount, {});
2213+
break;
2214+
default:
2215+
dexOp = pathPaymentStrictSend(taker.getPublicKey(),
2216+
pair.buying, amount,
2217+
pair.selling, 1, {});
2218+
break;
2219+
}
2220+
2221+
dexOp.sourceAccount.activate() =
2222+
toMuxedAccount(taker.getPublicKey());
2223+
auto& feePayer = feePayers[b];
2224+
auto tx = transactionFromOperations(
2225+
*app, feePayer.getSecretKey(),
2226+
feePayer.nextSequenceNumber(), {dexOp});
2227+
tx->addSignature(taker.getSecretKey());
2228+
2229+
{
2230+
LedgerSnapshot ls(*app);
2231+
auto result =
2232+
tx->checkValid(app->getAppConnector(), ls, 0, 0, 0);
2233+
REQUIRE(result->isSuccess());
2234+
}
2235+
2236+
batch.emplace_back(std::move(tx));
2237+
}
2238+
2239+
// Subtle: strict order has to be used because ledger hashes
2240+
// are going to be different between frozen and non-frozen
2241+
// runs, which causes different ordering of the exact same
2242+
// transactions.
2243+
auto r = closeLedger(*app, batch, /*strictOrder=*/true);
2244+
REQUIRE(r.results.size() == static_cast<size_t>(batchSize));
2245+
for (int b = 0; b < batchSize; ++b)
2246+
{
2247+
if (r.results[b].result.result.code() == txSUCCESS)
2248+
{
2249+
++txsSucceeded;
2250+
}
2251+
}
2252+
}
2253+
2254+
int frozenRemoved = 0;
2255+
for (auto const& fo : frozenOffers)
2256+
{
2257+
if (!offerExists(*app, fo.account, fo.offerID))
2258+
{
2259+
++frozenRemoved;
2260+
}
2261+
}
2262+
2263+
auto postBalances = recordBalances();
2264+
std::vector<int64_t> deltas(NUM_TRACKED * NUM_ASSETS, 0);
2265+
for (int i = 0; i < NUM_TRACKED * NUM_ASSETS; ++i)
2266+
{
2267+
deltas[i] = postBalances[i] - preBalances[i];
2268+
}
2269+
return MarketResult{deltas, frozenRemoved, txsSucceeded};
2270+
};
2271+
2272+
auto baseline = runMarket(iterSeed, false);
2273+
auto withFrozen = runMarket(iterSeed, true);
2274+
2275+
std::cerr << fmt::format("frozen offers removed: {}/{}, "
2276+
"txs succeeded: {}",
2277+
withFrozen.frozenOffersRemoved,
2278+
TOTAL_FROZEN_MAKERS, baseline.txsSucceeded)
2279+
<< std::endl;
2280+
2281+
REQUIRE(baseline.deltas == withFrozen.deltas);
2282+
REQUIRE(baseline.txsSucceeded == withFrozen.txsSucceeded);
2283+
2284+
totalFrozenOffersRemoved += withFrozen.frozenOffersRemoved;
2285+
totalTxsSucceeded += baseline.txsSucceeded;
2286+
}
2287+
2288+
// We should have enough test iterations and transactions to get at least
2289+
// some frozen offer removals and successful transactions.
2290+
REQUIRE(totalFrozenOffersRemoved > 0);
2291+
REQUIRE(totalTxsSucceeded > 0);
2292+
}
20022293
} // namespace
20032294
} // namespace stellar

0 commit comments

Comments
 (0)