Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 45ab670

Browse files
committedJan 22, 2025··
Added state archival getledgerentry http endpoint
1 parent e258f51 commit 45ab670

File tree

5 files changed

+527
-13
lines changed

5 files changed

+527
-13
lines changed
 

‎src/bucket/BucketSnapshotManager.cpp

+34-6
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,17 @@ BucketSnapshotManager::recordBulkLoadMetrics(std::string const& label,
115115
return iter->second;
116116
}
117117

118+
namespace
119+
{
120+
template <typename T, typename U>
121+
bool
122+
needsUpdate(std::shared_ptr<T const> const& snapshot,
123+
SnapshotPtrT<U> const& curr)
124+
{
125+
return !snapshot || snapshot->getLedgerSeq() < curr->getLedgerSeq();
126+
}
127+
}
128+
118129
void
119130
BucketSnapshotManager::maybeCopySearchableBucketListSnapshot(
120131
SearchableSnapshotConstPtr& snapshot)
@@ -123,9 +134,7 @@ BucketSnapshotManager::maybeCopySearchableBucketListSnapshot(
123134
// modified. Rather, a thread is checking it's copy against the canonical
124135
// snapshot, so use a shared lock.
125136
std::shared_lock<std::shared_mutex> lock(mSnapshotMutex);
126-
127-
if (!snapshot ||
128-
snapshot->getLedgerSeq() < mCurrLiveSnapshot->getLedgerSeq())
137+
if (needsUpdate(snapshot, mCurrLiveSnapshot))
129138
{
130139
snapshot = copySearchableLiveBucketListSnapshot();
131140
}
@@ -139,14 +148,33 @@ BucketSnapshotManager::maybeCopySearchableHotArchiveBucketListSnapshot(
139148
// modified. Rather, a thread is checking it's copy against the canonical
140149
// snapshot, so use a shared lock.
141150
std::shared_lock<std::shared_mutex> lock(mSnapshotMutex);
142-
143-
if (!snapshot ||
144-
snapshot->getLedgerSeq() < mCurrHotArchiveSnapshot->getLedgerSeq())
151+
if (needsUpdate(snapshot, mCurrHotArchiveSnapshot))
145152
{
146153
snapshot = copySearchableHotArchiveBucketListSnapshot();
147154
}
148155
}
149156

157+
void
158+
BucketSnapshotManager::maybeCopyLiveAndHotArchiveSnapshots(
159+
SearchableSnapshotConstPtr& liveSnapshot,
160+
SearchableHotArchiveSnapshotConstPtr& hotArchiveSnapshot)
161+
{
162+
// The canonical snapshot held by the BucketSnapshotManager is not being
163+
// modified. Rather, a thread is checking it's copy against the canonical
164+
// snapshot, so use a shared lock. For consistency we hold the lock while
165+
// updating both snapshots.
166+
std::shared_lock<std::shared_mutex> lock(mSnapshotMutex);
167+
if (needsUpdate(liveSnapshot, mCurrLiveSnapshot))
168+
{
169+
liveSnapshot = copySearchableLiveBucketListSnapshot();
170+
}
171+
172+
if (needsUpdate(hotArchiveSnapshot, mCurrHotArchiveSnapshot))
173+
{
174+
hotArchiveSnapshot = copySearchableHotArchiveBucketListSnapshot();
175+
}
176+
}
177+
150178
void
151179
BucketSnapshotManager::updateCurrentSnapshot(
152180
SnapshotPtrT<LiveBucket>&& liveSnapshot,

‎src/bucket/BucketSnapshotManager.h

+7
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,13 @@ class BucketSnapshotManager : NonMovableOrCopyable
9999
void maybeCopySearchableHotArchiveBucketListSnapshot(
100100
SearchableHotArchiveSnapshotConstPtr& snapshot);
101101

102+
// This function is the same as snapshot refreshers above, but guarantees
103+
// that both snapshots are consistent with the same lcl. This is required
104+
// when querying both snapshot types as part of the same query.
105+
void maybeCopyLiveAndHotArchiveSnapshots(
106+
SearchableSnapshotConstPtr& liveSnapshot,
107+
SearchableHotArchiveSnapshotConstPtr& hotArchiveSnapshot);
108+
102109
// All metric recording functions must only be called by the main thread
103110
void startPointLoadTimer() const;
104111
void endPointLoadTimer(LedgerEntryType t, bool bloomMiss) const;

‎src/main/QueryServer.cpp

+220-5
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
#include "bucket/BucketSnapshotManager.h"
77
#include "bucket/SearchableBucketList.h"
88
#include "ledger/LedgerTxnImpl.h"
9+
#include "ledger/LedgerTypeUtils.h"
910
#include "util/Logging.h"
1011
#include "util/XDRStream.h" // IWYU pragma: keep
12+
#include "util/types.h"
1113
#include <exception>
1214
#include <json/json.h>
1315

@@ -54,7 +56,12 @@ namespace stellar
5456
{
5557
QueryServer::QueryServer(const std::string& address, unsigned short port,
5658
int maxClient, size_t threadPoolSize,
57-
BucketSnapshotManager& bucketSnapshotManager)
59+
BucketSnapshotManager& bucketSnapshotManager
60+
#ifdef BUILD_TESTS
61+
,
62+
bool useMainThreadForTesting
63+
#endif
64+
)
5865
: mServer(address, port, maxClient, threadPoolSize)
5966
, mBucketSnapshotManager(bucketSnapshotManager)
6067
{
@@ -63,12 +70,28 @@ QueryServer::QueryServer(const std::string& address, unsigned short port,
6370

6471
mServer.add404(std::bind(&QueryServer::notFound, this, _1, _2, _3));
6572
addRoute("getledgerentryraw", &QueryServer::getLedgerEntryRaw);
73+
addRoute("getledgerentry", &QueryServer::getLedgerEntry);
6674

67-
auto workerPids = mServer.start();
68-
for (auto pid : workerPids)
75+
#ifdef BUILD_TESTS
76+
if (useMainThreadForTesting)
6977
{
70-
mBucketListSnapshots[pid] = std::move(
71-
bucketSnapshotManager.copySearchableLiveBucketListSnapshot());
78+
mBucketListSnapshots[std::this_thread::get_id()] =
79+
bucketSnapshotManager.copySearchableLiveBucketListSnapshot();
80+
mHotArchiveBucketListSnapshots[std::this_thread::get_id()] =
81+
bucketSnapshotManager.copySearchableHotArchiveBucketListSnapshot();
82+
}
83+
else
84+
#endif
85+
{
86+
auto workerPids = mServer.start();
87+
for (auto pid : workerPids)
88+
{
89+
mBucketListSnapshots[pid] =
90+
bucketSnapshotManager.copySearchableLiveBucketListSnapshot();
91+
mHotArchiveBucketListSnapshots[pid] =
92+
bucketSnapshotManager
93+
.copySearchableHotArchiveBucketListSnapshot();
94+
}
7295
}
7396
}
7497

@@ -190,4 +213,196 @@ QueryServer::getLedgerEntryRaw(std::string const& params,
190213
retStr = Json::FastWriter().write(root);
191214
return true;
192215
}
216+
217+
// This query needs to load all the given ledger entries and their "state"
218+
// (live, archived, evicted, new). This requires a loading entry and TTL from
219+
// the live BucketList and then checking the Hot Archive for any keys we didn't
220+
// find. We do three passes:
221+
// 1. Load all keys from the live BucketList
222+
// 2. For any Soroban keys not in the live BucketList, load them from the Hot
223+
// Archive
224+
// 3. Load TTL keys for any live Soroban entries found in 1.
225+
bool
226+
QueryServer::getLedgerEntry(std::string const& params, std::string const& body,
227+
std::string& retStr)
228+
{
229+
ZoneScoped;
230+
Json::Value root;
231+
232+
std::map<std::string, std::vector<std::string>> paramMap;
233+
httpThreaded::server::server::parsePostParams(body, paramMap);
234+
235+
auto keys = paramMap["key"];
236+
auto snapshotLedger = parseOptionalParam<uint32_t>(paramMap, "ledgerSeq");
237+
238+
if (keys.empty())
239+
{
240+
throw std::invalid_argument(
241+
"Must specify ledger key in POST body: key=<LedgerKey in base64 "
242+
"XDR format>");
243+
}
244+
245+
// Get snapshots for both live and hot archive bucket lists
246+
auto& liveBl = mBucketListSnapshots.at(std::this_thread::get_id());
247+
auto& hotArchiveBl =
248+
mHotArchiveBucketListSnapshots.at(std::this_thread::get_id());
249+
250+
// orderedNotFoundKeys is a set of keys we have not yet found (not in live
251+
// BucketList or in an archived state in the Hot Archive)
252+
LedgerKeySet orderedNotFoundKeys;
253+
for (auto const& key : keys)
254+
{
255+
LedgerKey k;
256+
fromOpaqueBase64(k, key);
257+
258+
// Check for TTL keys which are not allowed
259+
if (k.type() == TTL)
260+
{
261+
retStr = "TTL keys are not allowed";
262+
return false;
263+
}
264+
265+
orderedNotFoundKeys.emplace(k);
266+
}
267+
268+
mBucketSnapshotManager.maybeCopyLiveAndHotArchiveSnapshots(liveBl,
269+
hotArchiveBl);
270+
271+
std::vector<LedgerEntry> liveEntries;
272+
std::vector<HotArchiveBucketEntry> archivedEntries;
273+
uint32_t ledgerSeq =
274+
snapshotLedger ? *snapshotLedger : liveBl->getLedgerSeq();
275+
root["ledgerSeq"] = ledgerSeq;
276+
277+
auto liveEntriesOp =
278+
liveBl->loadKeysFromLedger(orderedNotFoundKeys, ledgerSeq);
279+
280+
// Return 404 if ledgerSeq not found
281+
if (!liveEntriesOp)
282+
{
283+
retStr = "LedgerSeq not found";
284+
return false;
285+
}
286+
287+
liveEntries = std::move(*liveEntriesOp);
288+
289+
// Remove keys found in live bucketList
290+
for (auto const& le : liveEntries)
291+
{
292+
orderedNotFoundKeys.erase(LedgerEntryKey(le));
293+
}
294+
295+
LedgerKeySet hotArchiveKeysToSearch;
296+
for (auto const& lk : orderedNotFoundKeys)
297+
{
298+
if (isSorobanEntry(lk))
299+
{
300+
hotArchiveKeysToSearch.emplace(lk);
301+
}
302+
}
303+
304+
// Only query archive for remaining keys
305+
if (!hotArchiveKeysToSearch.empty())
306+
{
307+
auto archivedEntriesOp =
308+
hotArchiveBl->loadKeysFromLedger(hotArchiveKeysToSearch, ledgerSeq);
309+
if (!archivedEntriesOp)
310+
{
311+
retStr = "LedgerSeq not found";
312+
return false;
313+
}
314+
archivedEntries = std::move(*archivedEntriesOp);
315+
}
316+
317+
// Collect TTL keys for Soroban entries in the live BucketList
318+
LedgerKeySet ttlKeys;
319+
for (auto const& le : liveEntries)
320+
{
321+
if (isSorobanEntry(le.data))
322+
{
323+
ttlKeys.emplace(getTTLKey(le));
324+
}
325+
}
326+
327+
std::vector<LedgerEntry> ttlEntries;
328+
if (!ttlKeys.empty())
329+
{
330+
// We haven't updated the live snapshot so we will never not have the
331+
// requested ledgerSeq and return nullopt.
332+
ttlEntries =
333+
std::move(liveBl->loadKeysFromLedger(ttlKeys, ledgerSeq).value());
334+
}
335+
336+
std::unordered_map<LedgerKey, LedgerEntry> ttlMap;
337+
for (auto const& ttlEntry : ttlEntries)
338+
{
339+
ttlMap.emplace(LedgerEntryKey(ttlEntry), ttlEntry);
340+
}
341+
342+
// Process live entries
343+
for (auto const& le : liveEntries)
344+
{
345+
Json::Value entry;
346+
entry["e"] = toOpaqueBase64(le);
347+
348+
// Check TTL for Soroban entries
349+
if (isSorobanEntry(le.data))
350+
{
351+
auto ttlIter = ttlMap.find(getTTLKey(le));
352+
releaseAssertOrThrow(ttlIter != ttlMap.end());
353+
if (isLive(ttlIter->second, ledgerSeq))
354+
{
355+
entry["state"] = "live";
356+
entry["ttl"] = ttlIter->second.data.ttl().liveUntilLedgerSeq;
357+
}
358+
else
359+
{
360+
entry["state"] = "archived";
361+
}
362+
}
363+
else
364+
{
365+
entry["state"] = "live";
366+
}
367+
368+
root["entries"].append(entry);
369+
}
370+
371+
// Process archived entries - all are evicted since they come from hot
372+
// archive
373+
for (auto const& be : archivedEntries)
374+
{
375+
// If we get to this point, we know the key is not in the live
376+
// BucketList, so if we get a DELETED or RESTORED entry, the entry is
377+
// new wrt ledger state.
378+
if (be.type() != HOT_ARCHIVE_ARCHIVED)
379+
{
380+
continue;
381+
}
382+
383+
auto const& le = be.archivedEntry();
384+
385+
// At this point we've "found" the key and know it's archived, so remove
386+
// it from our search set
387+
orderedNotFoundKeys.erase(LedgerEntryKey(le));
388+
389+
Json::Value entry;
390+
entry["e"] = toOpaqueBase64(le);
391+
entry["state"] = "evicted";
392+
root["entries"].append(entry);
393+
}
394+
395+
// Since we removed entries found in the live BucketList and archived
396+
// entries found in the Hot Archive, any remaining keys must be new.
397+
for (auto const& key : orderedNotFoundKeys)
398+
{
399+
Json::Value entry;
400+
entry["e"] = toOpaqueBase64(key);
401+
entry["state"] = "new";
402+
root["entries"].append(entry);
403+
}
404+
405+
retStr = Json::FastWriter().write(root);
406+
return true;
407+
}
193408
}

‎src/main/QueryServer.h

+16-2
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,9 @@ class QueryServer
2929
std::unordered_map<std::thread::id, SearchableSnapshotConstPtr>
3030
mBucketListSnapshots;
3131

32+
std::unordered_map<std::thread::id, SearchableHotArchiveSnapshotConstPtr>
33+
mHotArchiveBucketListSnapshots;
34+
3235
BucketSnapshotManager& mBucketSnapshotManager;
3336

3437
bool safeRouter(HandlerRoute route, std::string const& params,
@@ -39,14 +42,25 @@ class QueryServer
3942

4043
void addRoute(std::string const& name, HandlerRoute route);
4144

45+
#ifdef BUILD_TESTS
46+
public:
47+
#endif
4248
// Returns raw LedgerKeys for the given keys from the Live BucketList. Does
4349
// not query other BucketLists or reason about archival.
4450
bool getLedgerEntryRaw(std::string const& params, std::string const& body,
4551
std::string& retStr);
4652

53+
bool getLedgerEntry(std::string const& params, std::string const& body,
54+
std::string& retStr);
55+
4756
public:
4857
QueryServer(const std::string& address, unsigned short port, int maxClient,
4958
size_t threadPoolSize,
50-
BucketSnapshotManager& bucketSnapshotManager);
59+
BucketSnapshotManager& bucketSnapshotManager
60+
#ifdef BUILD_TESTS
61+
,
62+
bool useMainThreadForTesting = false
63+
#endif
64+
);
5165
};
52-
}
66+
}

‎src/main/test/QueryServerTests.cpp

+250
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
// Copyright 2025 Stellar Development Foundation and contributors. Licensed
2+
// under the Apache License, Version 2.0. See the COPYING file at the root
3+
// of this distribution or at http://www.apache.org/licenses/LICENSE-2.0
4+
5+
#include "bucket/BucketManager.h"
6+
#include "bucket/test/BucketTestUtils.h"
7+
#include "ledger/LedgerTxnImpl.h"
8+
#include "ledger/LedgerTypeUtils.h"
9+
#include "ledger/test/LedgerTestUtils.h"
10+
#include "lib/catch.hpp"
11+
#include "main/Application.h"
12+
#include "main/Config.h"
13+
#include "main/QueryServer.h"
14+
#include "test/test.h"
15+
#include "util/Math.h"
16+
#include <json/json.h>
17+
18+
using namespace stellar;
19+
20+
#ifdef ENABLE_NEXT_PROTOCOL_VERSION_UNSAFE_FOR_PRODUCTION
21+
// TODO: Better testing of errors, edge cases like an entry being in both live
22+
// and archive bl, etc.
23+
TEST_CASE("getledgerentry", "[queryserver]")
24+
{
25+
VirtualClock clock;
26+
auto cfg = getTestConfig();
27+
cfg.QUERY_SNAPSHOT_LEDGERS = 5;
28+
29+
auto app = createTestApplication<BucketTestUtils::BucketTestApplication>(
30+
clock, cfg);
31+
auto& lm = app->getLedgerManager();
32+
33+
// Query Server is disabled by default in cfg. Instead of enabling it, we're
34+
// going to manage a versio here manually so we can directly call functions
35+
// and avoid sending network requests.
36+
auto qServer = std::make_unique<QueryServer>(
37+
"127.0.0.1", // Address
38+
0, // port (0 = random)
39+
1, // maxClient
40+
1, // threadPoolSize
41+
app->getBucketManager().getBucketSnapshotManager(), true);
42+
43+
std::unordered_map<LedgerKey, LedgerEntry> liveEntryMap;
44+
45+
// Map code/data lk -> ttl value
46+
std::unordered_map<LedgerKey, uint32_t> liveTTLEntryMap;
47+
std::unordered_map<LedgerKey, LedgerEntry> archivedEntryMap;
48+
std::unordered_map<LedgerKey, LedgerEntry> evictedEntryMap;
49+
50+
// Create some test entries
51+
for (auto i = 0; i < 15; ++i)
52+
{
53+
auto lcl = app->getLedgerManager().getLastClosedLedgerNum();
54+
auto liveEntries =
55+
LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes(
56+
{CONTRACT_DATA, CONTRACT_CODE, ACCOUNT}, 5);
57+
58+
std::vector<LedgerEntry> liveEntriesToInsert;
59+
for (auto const& le : liveEntries)
60+
{
61+
if (isSorobanEntry(le.data))
62+
{
63+
LedgerEntry ttl;
64+
ttl.data.type(TTL);
65+
ttl.data.ttl().keyHash = getTTLKey(le).ttl().keyHash;
66+
67+
// Make half of the entries archived on the live BL
68+
if (rand_flip())
69+
{
70+
ttl.data.ttl().liveUntilLedgerSeq = lcl + 100;
71+
}
72+
else
73+
{
74+
ttl.data.ttl().liveUntilLedgerSeq = 0;
75+
}
76+
liveTTLEntryMap[LedgerEntryKey(le)] =
77+
ttl.data.ttl().liveUntilLedgerSeq;
78+
liveEntriesToInsert.push_back(ttl);
79+
}
80+
81+
liveEntriesToInsert.push_back(le);
82+
liveEntryMap[LedgerEntryKey(le)] = le;
83+
}
84+
85+
auto archivedEntries =
86+
LedgerTestUtils::generateValidUniqueLedgerEntriesWithTypes(
87+
{CONTRACT_DATA, CONTRACT_CODE}, 5);
88+
for (auto const& le : archivedEntries)
89+
{
90+
archivedEntryMap[LedgerEntryKey(le)] = le;
91+
}
92+
93+
lm.setNextLedgerEntryBatchForBucketTesting({}, liveEntriesToInsert, {});
94+
lm.setNextArchiveBatchForBucketTesting(archivedEntries, {}, {});
95+
closeLedger(*app);
96+
}
97+
98+
// Lambda to build request body
99+
auto buildRequestBody =
100+
[](std::optional<uint32_t> ledgerSeq,
101+
std::vector<LedgerKey> const& keys) -> std::string {
102+
std::string body;
103+
if (ledgerSeq)
104+
{
105+
body = "ledgerSeq=" + std::to_string(*ledgerSeq);
106+
}
107+
108+
for (auto const& key : keys)
109+
{
110+
body += (body.empty() ? "" : "&") + std::string("key=") +
111+
toOpaqueBase64(key);
112+
}
113+
return body;
114+
};
115+
116+
// Lambda to check entry details in response
117+
auto checkEntry = [](std::string const& retStr, LedgerEntry const& le,
118+
std::optional<uint32_t> expectedTTL,
119+
uint32_t ledgerSeq) -> bool {
120+
Json::Value root;
121+
Json::Reader reader;
122+
REQUIRE(reader.parse(retStr, root));
123+
REQUIRE(root.isMember("entries"));
124+
REQUIRE(root.isMember("ledgerSeq"));
125+
REQUIRE(root["ledgerSeq"].asUInt() == ledgerSeq);
126+
127+
auto const& entries = root["entries"];
128+
for (auto const& entry : entries)
129+
{
130+
REQUIRE(entry.isMember("e"));
131+
REQUIRE(entry.isMember("state"));
132+
133+
LedgerEntry responseLE;
134+
fromOpaqueBase64(responseLE, entry["e"].asString());
135+
if (responseLE == le)
136+
{
137+
std::string expectedState;
138+
if (!isSorobanEntry(le.data))
139+
{
140+
expectedState = "live";
141+
}
142+
else
143+
{
144+
if (expectedTTL)
145+
{
146+
if (ledgerSeq >= *expectedTTL)
147+
{
148+
expectedState = "archived";
149+
}
150+
else
151+
{
152+
expectedState = "live";
153+
}
154+
}
155+
else
156+
{
157+
expectedState = "evicted";
158+
}
159+
}
160+
161+
REQUIRE(entry["state"].asString() == expectedState);
162+
if (isSorobanEntry(le.data) && expectedState == "live")
163+
{
164+
REQUIRE(entry.isMember("ttl"));
165+
REQUIRE(entry["ttl"].asUInt() == *expectedTTL);
166+
}
167+
else
168+
{
169+
REQUIRE(!entry.isMember("ttl"));
170+
}
171+
172+
return true;
173+
}
174+
}
175+
return false;
176+
};
177+
178+
// Lambda to check new entry response
179+
auto checkNewEntry = [](std::string const& retStr, LedgerKey const& key,
180+
uint32_t ledgerSeq) -> bool {
181+
Json::Value root;
182+
Json::Reader reader;
183+
REQUIRE(reader.parse(retStr, root));
184+
REQUIRE(root.isMember("entries"));
185+
REQUIRE(root.isMember("ledgerSeq"));
186+
REQUIRE(root["ledgerSeq"].asUInt() == ledgerSeq);
187+
188+
auto const& entries = root["entries"];
189+
for (auto const& entry : entries)
190+
{
191+
REQUIRE(entry.isMember("e"));
192+
REQUIRE(entry.isMember("state"));
193+
REQUIRE(entry["state"].asString() == "new");
194+
195+
LedgerKey responseKey;
196+
fromOpaqueBase64(responseKey, entry["e"].asString());
197+
if (responseKey == key)
198+
{
199+
REQUIRE(!entry.isMember("ttl"));
200+
return true;
201+
}
202+
}
203+
return false;
204+
};
205+
206+
UnorderedSet<LedgerKey> seenKeys;
207+
for (auto const& [lk, le] : liveEntryMap)
208+
{
209+
auto body = buildRequestBody(std::nullopt, {lk});
210+
std::string retStr;
211+
std::string empty;
212+
REQUIRE(qServer->getLedgerEntry(empty, body, retStr));
213+
214+
auto ttlIter = liveTTLEntryMap.find(lk);
215+
std::optional<uint32_t> expectedTTL =
216+
ttlIter != liveTTLEntryMap.end()
217+
? std::make_optional(ttlIter->second)
218+
: std::nullopt;
219+
REQUIRE(
220+
checkEntry(retStr, le, expectedTTL, lm.getLastClosedLedgerNum()));
221+
222+
// Remove any duplicates we've already found
223+
archivedEntryMap.erase(lk);
224+
seenKeys.insert(lk);
225+
}
226+
227+
for (auto const& [lk, le] : archivedEntryMap)
228+
{
229+
auto body = buildRequestBody(std::nullopt, {lk});
230+
std::string retStr;
231+
std::string empty;
232+
REQUIRE(qServer->getLedgerEntry(empty, body, retStr));
233+
REQUIRE(
234+
checkEntry(retStr, le, std::nullopt, lm.getLastClosedLedgerNum()));
235+
seenKeys.insert(lk);
236+
}
237+
238+
// Now check for new entries
239+
auto newKeys = LedgerTestUtils::generateValidUniqueLedgerKeysWithTypes(
240+
{TRUSTLINE, CONTRACT_DATA, CONTRACT_CODE}, 5, seenKeys);
241+
for (auto const& key : newKeys)
242+
{
243+
auto body = buildRequestBody(std::nullopt, {key});
244+
std::string retStr;
245+
std::string empty;
246+
REQUIRE(qServer->getLedgerEntry(empty, body, retStr));
247+
REQUIRE(checkNewEntry(retStr, key, lm.getLastClosedLedgerNum()));
248+
}
249+
}
250+
#endif

0 commit comments

Comments
 (0)
Please sign in to comment.