@@ -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